├── .cargo └── config.toml ├── .editorconfig ├── .github ├── semantic.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── .gitignore ├── Cargo.toml ├── README.md ├── buffer │ ├── Cargo.toml │ └── src │ │ └── main.rs └── postgres │ ├── Cargo.toml │ └── src │ └── main.rs ├── fastpool ├── Cargo.toml ├── src │ ├── bounded.rs │ ├── common.rs │ ├── lib.rs │ ├── mutex.rs │ ├── retain_spec.rs │ └── unbounded.rs └── tests │ └── replenish_tests.rs ├── licenserc.toml ├── rust-toolchain.toml ├── rustfmt.toml ├── taplo.toml ├── typos.toml └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [alias] 16 | x = "run --package x --" 17 | 18 | [env] 19 | CARGO_WORKSPACE_DIR = { value = "", relative = true } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.toml] 10 | indent_size = tab 11 | tab_width = 2 12 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The pull request's title should be fulfilled the following pattern: 16 | # 17 | # [optional scope]: 18 | # 19 | # ... where valid types and scopes can be found below; for example: 20 | # 21 | # build(maven): One level down for native profile 22 | # 23 | # More about configurations on https://github.com/Ezard/semantic-prs#configuration 24 | 25 | enabled: true 26 | 27 | titleOnly: true 28 | 29 | types: 30 | - feat 31 | - fix 32 | - docs 33 | - style 34 | - refactor 35 | - perf 36 | - test 37 | - build 38 | - ci 39 | - chore 40 | - revert 41 | 42 | targetUrl: https://github.com/fast/fastpool/blob/main/.github/semantic.yml 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: CI 16 | on: 17 | pull_request: 18 | branches: [ main ] 19 | push: 20 | branches: [ main ] 21 | 22 | # Concurrency strategy: 23 | # github.workflow: distinguish this workflow from others 24 | # github.event_name: distinguish `push` event from `pull_request` event 25 | # github.event.number: set to the number of the pull request if `pull_request` event 26 | # github.run_id: otherwise, it's a `push` event, only cancel if we rerun the workflow 27 | # 28 | # Reference: 29 | # https://docs.github.com/en/actions/using-jobs/using-concurrency 30 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 31 | concurrency: 32 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} 33 | cancel-in-progress: true 34 | 35 | jobs: 36 | check: 37 | name: Check 38 | runs-on: ubuntu-22.04 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Install toolchain 42 | uses: dtolnay/rust-toolchain@nightly 43 | with: 44 | components: rustfmt,clippy 45 | - uses: Swatinem/rust-cache@v2 46 | - uses: taiki-e/install-action@v2 47 | with: 48 | tool: typos-cli,taplo-cli,hawkeye 49 | - run: cargo +nightly x lint 50 | 51 | test: 52 | name: Run tests 53 | strategy: 54 | matrix: 55 | os: [ ubuntu-22.04, macos-14, windows-2022 ] 56 | rust-version: [ "1.85.0", "stable" ] 57 | runs-on: ${{ matrix.os }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: Swatinem/rust-cache@v2 61 | - name: Delete rust-toolchain.toml 62 | run: rm rust-toolchain.toml 63 | - name: Install toolchain 64 | uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: ${{ matrix.rust-version }} 67 | - name: Run unit tests 68 | run: cargo x test --no-capture 69 | shell: bash 70 | 71 | required: 72 | name: Required 73 | runs-on: ubuntu-22.04 74 | if: ${{ always() }} 75 | needs: 76 | - check 77 | - test 78 | steps: 79 | - name: Guardian 80 | run: | 81 | if [[ ! ( \ 82 | "${{ needs.check.result }}" == "success" \ 83 | && "${{ needs.test.result }}" == "success" \ 84 | ) ]]; then 85 | echo "Required jobs haven't been completed successfully." 86 | exit -1 87 | fi 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys 0.59.0", 68 | ] 69 | 70 | [[package]] 71 | name = "autocfg" 72 | version = "1.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 75 | 76 | [[package]] 77 | name = "backtrace" 78 | version = "0.3.75" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 81 | dependencies = [ 82 | "addr2line", 83 | "cfg-if", 84 | "libc", 85 | "miniz_oxide", 86 | "object", 87 | "rustc-demangle", 88 | "windows-targets", 89 | ] 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "2.9.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 96 | 97 | [[package]] 98 | name = "bytes" 99 | version = "1.10.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 102 | 103 | [[package]] 104 | name = "cfg-if" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 108 | 109 | [[package]] 110 | name = "clap" 111 | version = "4.5.37" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 114 | dependencies = [ 115 | "clap_builder", 116 | "clap_derive", 117 | ] 118 | 119 | [[package]] 120 | name = "clap_builder" 121 | version = "4.5.37" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 124 | dependencies = [ 125 | "anstream", 126 | "anstyle", 127 | "clap_lex", 128 | "strsim", 129 | ] 130 | 131 | [[package]] 132 | name = "clap_derive" 133 | version = "4.5.32" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 136 | dependencies = [ 137 | "heck", 138 | "proc-macro2", 139 | "quote", 140 | "syn", 141 | ] 142 | 143 | [[package]] 144 | name = "clap_lex" 145 | version = "0.7.4" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 148 | 149 | [[package]] 150 | name = "colorchoice" 151 | version = "1.0.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 154 | 155 | [[package]] 156 | name = "either" 157 | version = "1.15.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 160 | 161 | [[package]] 162 | name = "env_home" 163 | version = "0.1.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" 166 | 167 | [[package]] 168 | name = "errno" 169 | version = "0.3.11" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 172 | dependencies = [ 173 | "libc", 174 | "windows-sys 0.59.0", 175 | ] 176 | 177 | [[package]] 178 | name = "fastpool" 179 | version = "1.0.0" 180 | dependencies = [ 181 | "mea", 182 | "scopeguard", 183 | "tokio", 184 | ] 185 | 186 | [[package]] 187 | name = "gimli" 188 | version = "0.31.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 191 | 192 | [[package]] 193 | name = "heck" 194 | version = "0.5.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 197 | 198 | [[package]] 199 | name = "is_terminal_polyfill" 200 | version = "1.70.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 203 | 204 | [[package]] 205 | name = "libc" 206 | version = "0.2.172" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 209 | 210 | [[package]] 211 | name = "linux-raw-sys" 212 | version = "0.9.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 215 | 216 | [[package]] 217 | name = "lock_api" 218 | version = "0.4.12" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 221 | dependencies = [ 222 | "autocfg", 223 | "scopeguard", 224 | ] 225 | 226 | [[package]] 227 | name = "mea" 228 | version = "0.3.7" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "3dd4c034214a0a3656fafe3a600d6ea4f9329ec9a35168126ff548af33375fc0" 231 | dependencies = [ 232 | "slab", 233 | ] 234 | 235 | [[package]] 236 | name = "memchr" 237 | version = "2.7.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 240 | 241 | [[package]] 242 | name = "miniz_oxide" 243 | version = "0.8.8" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 246 | dependencies = [ 247 | "adler2", 248 | ] 249 | 250 | [[package]] 251 | name = "mio" 252 | version = "1.0.3" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 255 | dependencies = [ 256 | "libc", 257 | "wasi", 258 | "windows-sys 0.52.0", 259 | ] 260 | 261 | [[package]] 262 | name = "object" 263 | version = "0.36.7" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 266 | dependencies = [ 267 | "memchr", 268 | ] 269 | 270 | [[package]] 271 | name = "once_cell" 272 | version = "1.21.3" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 275 | 276 | [[package]] 277 | name = "parking_lot" 278 | version = "0.12.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 281 | dependencies = [ 282 | "lock_api", 283 | "parking_lot_core", 284 | ] 285 | 286 | [[package]] 287 | name = "parking_lot_core" 288 | version = "0.9.10" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 291 | dependencies = [ 292 | "cfg-if", 293 | "libc", 294 | "redox_syscall", 295 | "smallvec", 296 | "windows-targets", 297 | ] 298 | 299 | [[package]] 300 | name = "pin-project-lite" 301 | version = "0.2.16" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 304 | 305 | [[package]] 306 | name = "proc-macro2" 307 | version = "1.0.95" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 310 | dependencies = [ 311 | "unicode-ident", 312 | ] 313 | 314 | [[package]] 315 | name = "quote" 316 | version = "1.0.40" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 319 | dependencies = [ 320 | "proc-macro2", 321 | ] 322 | 323 | [[package]] 324 | name = "redox_syscall" 325 | version = "0.5.12" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 328 | dependencies = [ 329 | "bitflags", 330 | ] 331 | 332 | [[package]] 333 | name = "rustc-demangle" 334 | version = "0.1.24" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 337 | 338 | [[package]] 339 | name = "rustix" 340 | version = "1.0.7" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 343 | dependencies = [ 344 | "bitflags", 345 | "errno", 346 | "libc", 347 | "linux-raw-sys", 348 | "windows-sys 0.59.0", 349 | ] 350 | 351 | [[package]] 352 | name = "scopeguard" 353 | version = "1.2.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 356 | 357 | [[package]] 358 | name = "signal-hook-registry" 359 | version = "1.4.5" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 362 | dependencies = [ 363 | "libc", 364 | ] 365 | 366 | [[package]] 367 | name = "slab" 368 | version = "0.4.9" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 371 | dependencies = [ 372 | "autocfg", 373 | ] 374 | 375 | [[package]] 376 | name = "smallvec" 377 | version = "1.15.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 380 | 381 | [[package]] 382 | name = "socket2" 383 | version = "0.5.9" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 386 | dependencies = [ 387 | "libc", 388 | "windows-sys 0.52.0", 389 | ] 390 | 391 | [[package]] 392 | name = "strsim" 393 | version = "0.11.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 396 | 397 | [[package]] 398 | name = "syn" 399 | version = "2.0.101" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 402 | dependencies = [ 403 | "proc-macro2", 404 | "quote", 405 | "unicode-ident", 406 | ] 407 | 408 | [[package]] 409 | name = "tokio" 410 | version = "1.45.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 413 | dependencies = [ 414 | "backtrace", 415 | "bytes", 416 | "libc", 417 | "mio", 418 | "parking_lot", 419 | "pin-project-lite", 420 | "signal-hook-registry", 421 | "socket2", 422 | "tokio-macros", 423 | "windows-sys 0.52.0", 424 | ] 425 | 426 | [[package]] 427 | name = "tokio-macros" 428 | version = "2.5.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 431 | dependencies = [ 432 | "proc-macro2", 433 | "quote", 434 | "syn", 435 | ] 436 | 437 | [[package]] 438 | name = "unicode-ident" 439 | version = "1.0.18" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 442 | 443 | [[package]] 444 | name = "utf8parse" 445 | version = "0.2.2" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 448 | 449 | [[package]] 450 | name = "wasi" 451 | version = "0.11.0+wasi-snapshot-preview1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 454 | 455 | [[package]] 456 | name = "which" 457 | version = "7.0.3" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" 460 | dependencies = [ 461 | "either", 462 | "env_home", 463 | "rustix", 464 | "winsafe", 465 | ] 466 | 467 | [[package]] 468 | name = "windows-sys" 469 | version = "0.52.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 472 | dependencies = [ 473 | "windows-targets", 474 | ] 475 | 476 | [[package]] 477 | name = "windows-sys" 478 | version = "0.59.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 481 | dependencies = [ 482 | "windows-targets", 483 | ] 484 | 485 | [[package]] 486 | name = "windows-targets" 487 | version = "0.52.6" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 490 | dependencies = [ 491 | "windows_aarch64_gnullvm", 492 | "windows_aarch64_msvc", 493 | "windows_i686_gnu", 494 | "windows_i686_gnullvm", 495 | "windows_i686_msvc", 496 | "windows_x86_64_gnu", 497 | "windows_x86_64_gnullvm", 498 | "windows_x86_64_msvc", 499 | ] 500 | 501 | [[package]] 502 | name = "windows_aarch64_gnullvm" 503 | version = "0.52.6" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 506 | 507 | [[package]] 508 | name = "windows_aarch64_msvc" 509 | version = "0.52.6" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 512 | 513 | [[package]] 514 | name = "windows_i686_gnu" 515 | version = "0.52.6" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 518 | 519 | [[package]] 520 | name = "windows_i686_gnullvm" 521 | version = "0.52.6" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 524 | 525 | [[package]] 526 | name = "windows_i686_msvc" 527 | version = "0.52.6" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 530 | 531 | [[package]] 532 | name = "windows_x86_64_gnu" 533 | version = "0.52.6" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 536 | 537 | [[package]] 538 | name = "windows_x86_64_gnullvm" 539 | version = "0.52.6" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 542 | 543 | [[package]] 544 | name = "windows_x86_64_msvc" 545 | version = "0.52.6" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 548 | 549 | [[package]] 550 | name = "winsafe" 551 | version = "0.0.19" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 554 | 555 | [[package]] 556 | name = "x" 557 | version = "0.0.0" 558 | dependencies = [ 559 | "clap", 560 | "which", 561 | ] 562 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [workspace] 16 | members = ["fastpool", "xtask"] 17 | resolver = "2" 18 | 19 | [workspace.package] 20 | edition = "2024" 21 | homepage = "https://github.com/fast/fastpool" 22 | license = "Apache-2.0" 23 | readme = "README.md" 24 | repository = "https://github.com/fast/fastpool" 25 | rust-version = "1.85.0" 26 | 27 | [workspace.lints.rust] 28 | unknown_lints = "deny" 29 | 30 | [workspace.lints.clippy] 31 | dbg_macro = "deny" 32 | 33 | [workspace.metadata.release] 34 | pre-release-commit-message = "chore: release v{{version}}" 35 | shared-version = true 36 | sign-tag = true 37 | tag-name = "v{{version}}" 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastpool 2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![Documentation][docs-badge]][docs-url] 5 | [![MSRV 1.80][msrv-badge]](https://www.whatrustisit.com) 6 | [![Apache 2.0 licensed][license-badge]][license-url] 7 | [![Build Status][actions-badge]][actions-url] 8 | 9 | [crates-badge]: https://img.shields.io/crates/v/fastpool.svg 10 | [crates-url]: https://crates.io/crates/fastpool 11 | [docs-badge]: https://docs.rs/fastpool/badge.svg 12 | [msrv-badge]: https://img.shields.io/badge/MSRV-1.80-green?logo=rust 13 | [docs-url]: https://docs.rs/fastpool 14 | [license-badge]: https://img.shields.io/crates/l/fastpool 15 | [license-url]: LICENSE 16 | [actions-badge]: https://github.com/fast/fastpool/workflows/CI/badge.svg 17 | [actions-url]:https://github.com/fast/fastpool/actions?query=workflow%3ACI 18 | 19 | ## Overview 20 | 21 | Fastpool provides fast and runtime-agnostic object pools for Async Rust. 22 | 23 | You can read [the docs page](https://docs.rs/fastpool/*/fastpool/) for a complete overview of the library. 24 | 25 | ## Installation 26 | 27 | Add the dependency to your `Cargo.toml` via: 28 | 29 | ```shell 30 | cargo add fastpool 31 | ``` 32 | 33 | ## Documentation 34 | 35 | Read the online documents at https://docs.rs/fastpool. 36 | 37 | ## Minimum Supported Rust Version (MSRV) 38 | 39 | This crate is built against the latest stable release, and its minimum supported rustc version is 1.85.0. 40 | 41 | The policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if Fastpool 1.0 requires Rust 1.20.0, then Fastpool 1.0.z for all values of z will also require Rust 1.20.0 or newer. However, Fastpool 1.y for y > 0 may require a newer minimum version of Rust. 42 | 43 | ## License 44 | 45 | This project is licensed under [Apache License, Version 2.0](LICENSE). 46 | 47 | ## Origins 48 | 49 | This library is derived from the [deadpool](https://docs.rs/deadpool/) crate with several dedicated considerations and a quite different mindset. 50 | 51 | You can read the FAQ section on [the docs page](https://docs.rs/fastpool/*/fastpool/#faq) for detailed discussion on "Why does fastpool have no timeout config?" and "Why does fastpool have no before/after hooks?" 52 | 53 | The [postgres example](examples/postgres) and [this issue thread](https://github.com/launchbadge/sqlx/issues/2276#issuecomment-2687157357) is a significant motivation for this crate: 54 | 55 | * Keeps the crate runtime-agnostic (see also ["Why does fastpool have no timeout config?"](https://docs.rs/fastpool/*/fastpool/#why-does-fastpool-have-no-timeout-config)) 56 | * Keeps the abstraction really dead simple (see also ["Why does fastpool have no before/after hooks?"](https://docs.rs/fastpool/*/fastpool/#why-does-fastpool-have-no-beforeafter-hooks)) 57 | * Returns an `Arc` on creation so that maintenance could be triggered with a weak reference. This helps applications to teardown (drop) the pool easily with Rust's built-in RAII mechanism. See also [this example](https://docs.rs/fastpool/*/fastpool/bounded/struct.Pool.html#method.retain). 58 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [workspace] 16 | members = ["buffer", "postgres"] 17 | resolver = "2" 18 | 19 | [workspace.package] 20 | edition = "2024" 21 | publish = false 22 | rust-version = "1.85.0" 23 | 24 | [workspace.dependencies] 25 | fastpool = { path = "../fastpool" } 26 | 27 | futures = { version = "0.3.31" } 28 | tokio = { version = "1.43.0", features = ["full"] } 29 | 30 | [workspace.lints.rust] 31 | unknown_lints = "deny" 32 | 33 | [workspace.lints.clippy] 34 | dbg_macro = "deny" 35 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples for using Fastpool 2 | 3 | For bounded pools: 4 | 5 | * [postgres](postgres) 6 | 7 | For unbounded pools: 8 | 9 | * [buffer](buffer) 10 | -------------------------------------------------------------------------------- /examples/buffer/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "buffer" 17 | version = "0.1.0" 18 | 19 | edition.workspace = true 20 | publish.workspace = true 21 | rust-version.workspace = true 22 | 23 | [dependencies] 24 | fastpool = { workspace = true } 25 | tokio = { workspace = true } 26 | 27 | [lints] 28 | workspace = true 29 | -------------------------------------------------------------------------------- /examples/buffer/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::convert::Infallible; 16 | use std::future::Future; 17 | use std::io::Write; 18 | use std::time::Instant; 19 | 20 | use fastpool::ObjectStatus; 21 | use fastpool::unbounded::Pool; 22 | use fastpool::unbounded::PoolConfig; 23 | 24 | struct ManageBuffer; 25 | 26 | impl fastpool::ManageObject for ManageBuffer { 27 | type Object = Vec; 28 | type Error = Infallible; 29 | 30 | fn create(&self) -> impl Future> + Send { 31 | std::future::ready(Ok(vec![])) 32 | } 33 | 34 | fn is_recyclable( 35 | &self, 36 | _: &mut Self::Object, 37 | _: &ObjectStatus, 38 | ) -> impl Future> + Send { 39 | std::future::ready(Ok(())) 40 | } 41 | } 42 | 43 | #[tokio::main] 44 | async fn main() { 45 | manual_put_pool().await; 46 | auto_create_pool().await; 47 | } 48 | 49 | async fn manual_put_pool() { 50 | let pool = Pool::>::never_manage(PoolConfig::new()); 51 | pool.extend_one(Vec::with_capacity(1024)); 52 | 53 | let mut buf = pool.get().await.unwrap(); 54 | write!(&mut buf, "{:?} key=manual_put_pool_put", Instant::now()).unwrap(); 55 | println!("{}", String::from_utf8_lossy(&buf)); 56 | 57 | let mut buf = pool 58 | .get_or_create(async || Ok::<_, Infallible>(Vec::with_capacity(512))) 59 | .await 60 | .unwrap(); 61 | write!(&mut buf, "{:?} key=manual_put_pool_create", Instant::now()).unwrap(); 62 | println!("{}", String::from_utf8_lossy(&buf)); 63 | assert_eq!(buf.capacity(), 512); 64 | } 65 | 66 | async fn auto_create_pool() { 67 | let pool = Pool::new(PoolConfig::new(), ManageBuffer); 68 | 69 | let mut buf = pool.get().await.unwrap(); 70 | write!(&mut buf, "{:?} key=auto_create_pool_0", Instant::now()).unwrap(); 71 | println!("{}", String::from_utf8_lossy(&buf)); 72 | 73 | pool.extend_one(Vec::with_capacity(1024)); 74 | let mut buf = pool.get().await.unwrap(); 75 | write!(&mut buf, "{:?} key=auto_create_pool_1", Instant::now()).unwrap(); 76 | println!("{}", String::from_utf8_lossy(&buf)); 77 | assert_eq!(buf.capacity(), 1024); 78 | } 79 | -------------------------------------------------------------------------------- /examples/postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "postgres" 17 | version = "0.1.0" 18 | 19 | edition.workspace = true 20 | publish.workspace = true 21 | rust-version.workspace = true 22 | 23 | [dependencies] 24 | fastpool = { workspace = true } 25 | futures = { workspace = true } 26 | tokio = { workspace = true } 27 | 28 | sqlx = { version = "0.8.3", default-features = false, features = [ 29 | "json", 30 | "macros", 31 | "migrate", 32 | "postgres", 33 | "runtime-tokio-rustls", 34 | "uuid", 35 | ] } 36 | 37 | [lints] 38 | workspace = true 39 | -------------------------------------------------------------------------------- /examples/postgres/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::str::FromStr; 16 | use std::sync::Arc; 17 | use std::time::Duration; 18 | 19 | use fastpool::ObjectStatus; 20 | use fastpool::bounded::Object; 21 | use fastpool::bounded::Pool; 22 | use fastpool::bounded::PoolConfig; 23 | use futures::future::BoxFuture; 24 | use sqlx::Acquire; 25 | use sqlx::ConnectOptions; 26 | use sqlx::Connection; 27 | use sqlx::PgConnection; 28 | use sqlx::TransactionManager; 29 | use sqlx::postgres::PgConnectOptions; 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct ConnectionPool { 33 | pool: Arc>, 34 | } 35 | 36 | impl ConnectionPool { 37 | pub fn new(option: PgConnectOptions, max_size: usize) -> Self { 38 | let pool = Pool::new(PoolConfig::new(max_size), ManageConnection { option }); 39 | 40 | let weak_pool = Arc::downgrade(&pool); 41 | tokio::spawn(async move { 42 | const REAP_IDLE_INTERVAL: Duration = Duration::from_secs(60); 43 | const IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60); 44 | 45 | loop { 46 | tokio::time::sleep(REAP_IDLE_INTERVAL).await; 47 | if let Some(pool) = weak_pool.upgrade() { 48 | pool.retain(|_, m| m.last_used().elapsed() < IDLE_TIMEOUT); 49 | 50 | let status = pool.status(); 51 | let gap = status.max_size - status.current_size; 52 | pool.replenish(gap).await; 53 | } else { 54 | break; 55 | } 56 | } 57 | }); 58 | 59 | Self { pool } 60 | } 61 | 62 | pub async fn acquire(&self) -> Result, sqlx::Error> { 63 | const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60); 64 | 65 | tokio::time::timeout(ACQUIRE_TIMEOUT, self.pool.get()) 66 | .await 67 | .unwrap_or_else(|_| Err(sqlx::Error::PoolTimedOut)) 68 | } 69 | 70 | pub async fn begin(&self) -> Result { 71 | let mut conn = self.acquire().await?; 72 | ::TransactionManager::begin(&mut conn).await?; 73 | Ok(PostgresTransaction { conn, open: true }) 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | pub struct ManageConnection { 79 | option: PgConnectOptions, 80 | } 81 | 82 | impl fastpool::ManageObject for ManageConnection { 83 | type Object = PgConnection; 84 | type Error = sqlx::Error; 85 | 86 | async fn create(&self) -> Result { 87 | self.option.connect().await 88 | } 89 | 90 | async fn is_recyclable( 91 | &self, 92 | conn: &mut Self::Object, 93 | _: &ObjectStatus, 94 | ) -> Result<(), Self::Error> { 95 | conn.ping().await 96 | } 97 | } 98 | 99 | #[derive(Debug)] 100 | pub struct PostgresTransaction { 101 | conn: Object, 102 | open: bool, 103 | } 104 | 105 | impl Drop for PostgresTransaction { 106 | fn drop(&mut self) { 107 | if self.open { 108 | // starts a rollback operation 109 | 110 | // what this does depend on the database but generally this means we queue a rollback 111 | // operation that will happen on the next asynchronous invocation of the underlying 112 | // connection (including if the connection is returned to a pool) 113 | 114 | ::TransactionManager::start_rollback(&mut self.conn); 115 | } 116 | } 117 | } 118 | 119 | impl std::ops::Deref for PostgresTransaction { 120 | type Target = PgConnection; 121 | 122 | #[inline] 123 | fn deref(&self) -> &Self::Target { 124 | &self.conn 125 | } 126 | } 127 | 128 | impl std::ops::DerefMut for PostgresTransaction { 129 | #[inline] 130 | fn deref_mut(&mut self) -> &mut Self::Target { 131 | &mut self.conn 132 | } 133 | } 134 | 135 | impl AsRef for PostgresTransaction { 136 | fn as_ref(&self) -> &PgConnection { 137 | &self.conn 138 | } 139 | } 140 | 141 | impl AsMut for PostgresTransaction { 142 | fn as_mut(&mut self) -> &mut PgConnection { 143 | &mut self.conn 144 | } 145 | } 146 | 147 | impl<'t> Acquire<'t> for &'t mut PostgresTransaction { 148 | type Database = sqlx::Postgres; 149 | 150 | type Connection = &'t mut PgConnection; 151 | 152 | #[inline] 153 | fn acquire(self) -> BoxFuture<'t, Result> { 154 | Box::pin(futures::future::ok(&mut **self)) 155 | } 156 | 157 | #[inline] 158 | fn begin(self) -> BoxFuture<'t, Result, sqlx::Error>> { 159 | sqlx::Transaction::begin(&mut **self) 160 | } 161 | } 162 | 163 | impl PostgresTransaction { 164 | /// Commits this transaction or savepoint. 165 | pub async fn commit(mut self) -> Result<(), sqlx::Error> { 166 | ::TransactionManager::commit(&mut self.conn).await?; 167 | self.open = false; 168 | Ok(()) 169 | } 170 | 171 | /// Aborts this transaction or savepoint. 172 | pub async fn rollback(mut self) -> Result<(), sqlx::Error> { 173 | ::TransactionManager::rollback(&mut self.conn).await?; 174 | self.open = false; 175 | Ok(()) 176 | } 177 | } 178 | 179 | #[tokio::main] 180 | async fn main() { 181 | let option = PgConnectOptions::from_str("postgres://localhost:5432/postgres").unwrap(); 182 | let pool = ConnectionPool::new(option, 12); 183 | 184 | let mut txn = pool.begin().await.unwrap(); 185 | let ret: i64 = sqlx::query_scalar("SELECT 1::INT8") 186 | .fetch_one(&mut *txn) 187 | .await 188 | .unwrap(); 189 | txn.commit().await.unwrap(); 190 | println!("ret: {ret}"); 191 | } 192 | -------------------------------------------------------------------------------- /fastpool/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "fastpool" 17 | 18 | description = "This crates implements a fast object pool for Async Rust." 19 | version = "1.0.0" 20 | 21 | edition.workspace = true 22 | homepage.workspace = true 23 | license.workspace = true 24 | readme.workspace = true 25 | repository.workspace = true 26 | rust-version.workspace = true 27 | 28 | [dependencies] 29 | mea = { version = "0.3.7" } 30 | scopeguard = { version = "1.2.0" } 31 | 32 | [dev-dependencies] 33 | tokio = { version = "1.43.0", features = ["full"] } 34 | 35 | [lints] 36 | workspace = true 37 | -------------------------------------------------------------------------------- /fastpool/src/bounded.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Bounded object pools. 16 | //! 17 | //! A bounded pool creates and recycles objects with full management. You _cannot_ put an object to 18 | //! the pool manually. 19 | //! 20 | //! The pool is bounded by the `max_size` config option of [`PoolConfig`]. If the pool reaches the 21 | //! maximum size, it will block all the [`Pool::get`] calls until an object is returned to the pool 22 | //! or an object is detached from the pool. 23 | //! 24 | //! Typically, a bounded pool is used wrapped in an [`Arc`] in order to call [`Pool::get`]. 25 | //! This is intended so that users can leverage [`Arc::downgrade`] for running background 26 | //! maintenance tasks (e.g., [`Pool::retain`]). 27 | //! 28 | //! Bounded pools are useful for pooling database connections. 29 | //! 30 | //! ## Examples 31 | //! 32 | //! Read the following simple demo or more complex examples in the examples directory. 33 | //! 34 | //! ``` 35 | //! use std::future::Future; 36 | //! 37 | //! use fastpool::ManageObject; 38 | //! use fastpool::ObjectStatus; 39 | //! use fastpool::bounded::Pool; 40 | //! use fastpool::bounded::PoolConfig; 41 | //! 42 | //! struct Compute; 43 | //! impl Compute { 44 | //! async fn do_work(&self) -> i32 { 45 | //! 42 46 | //! } 47 | //! } 48 | //! 49 | //! struct Manager; 50 | //! impl ManageObject for Manager { 51 | //! type Object = Compute; 52 | //! type Error = (); 53 | //! 54 | //! async fn create(&self) -> Result { 55 | //! Ok(Compute) 56 | //! } 57 | //! 58 | //! async fn is_recyclable( 59 | //! &self, 60 | //! o: &mut Self::Object, 61 | //! status: &ObjectStatus, 62 | //! ) -> Result<(), Self::Error> { 63 | //! Ok(()) 64 | //! } 65 | //! } 66 | //! 67 | //! # #[tokio::main] 68 | //! # async fn main() { 69 | //! let pool = Pool::new(PoolConfig::new(16), Manager); 70 | //! let o = pool.get().await.unwrap(); 71 | //! assert_eq!(o.do_work().await, 42); 72 | //! # } 73 | //! ``` 74 | 75 | use std::collections::VecDeque; 76 | use std::ops::Deref; 77 | use std::ops::DerefMut; 78 | use std::sync::Arc; 79 | use std::sync::Weak; 80 | use std::sync::atomic::AtomicUsize; 81 | use std::sync::atomic::Ordering; 82 | 83 | use mea::semaphore::OwnedSemaphorePermit; 84 | use mea::semaphore::Semaphore; 85 | 86 | use crate::ManageObject; 87 | use crate::ObjectStatus; 88 | use crate::QueueStrategy; 89 | use crate::RetainResult; 90 | use crate::mutex::Mutex; 91 | use crate::retain_spec; 92 | 93 | /// The configuration of [`Pool`]. 94 | #[derive(Clone, Copy, Debug)] 95 | #[non_exhaustive] 96 | pub struct PoolConfig { 97 | /// Maximum size of the [`Pool`]. 98 | pub max_size: usize, 99 | 100 | /// Queue strategy of the [`Pool`]. 101 | /// 102 | /// Determines the order of objects being queued and dequeued. 103 | pub queue_strategy: QueueStrategy, 104 | } 105 | 106 | impl PoolConfig { 107 | /// Creates a new [`PoolConfig`]. 108 | pub fn new(max_size: usize) -> Self { 109 | Self { 110 | max_size, 111 | queue_strategy: QueueStrategy::default(), 112 | } 113 | } 114 | 115 | /// Returns a new [`PoolConfig`] with the specified queue strategy. 116 | pub fn with_queue_strategy(mut self, queue_strategy: QueueStrategy) -> Self { 117 | self.queue_strategy = queue_strategy; 118 | self 119 | } 120 | } 121 | 122 | /// The current pool status. 123 | /// 124 | /// See [`Pool::status`]. 125 | #[derive(Clone, Copy, Debug)] 126 | #[non_exhaustive] 127 | pub struct PoolStatus { 128 | /// The maximum size of the pool. 129 | pub max_size: usize, 130 | 131 | /// The current size of the pool. 132 | pub current_size: usize, 133 | 134 | /// The number of idle objects in the pool. 135 | pub idle_count: usize, 136 | 137 | /// The number of futures waiting for an object. 138 | pub wait_count: usize, 139 | } 140 | 141 | /// Generic runtime-agnostic object pool with a maximum size. 142 | /// 143 | /// See the [module level documentation](self) for more. 144 | pub struct Pool { 145 | config: PoolConfig, 146 | manager: M, 147 | 148 | /// A counter that tracks the sum of waiters + obtained objects. 149 | users: AtomicUsize, 150 | /// A semaphore that limits the maximum of users of the pool. 151 | permits: Arc, 152 | /// A deque that holds the objects. 153 | slots: Mutex>>, 154 | } 155 | 156 | #[derive(Debug)] 157 | struct PoolDeque { 158 | deque: VecDeque, 159 | current_size: usize, 160 | max_size: usize, 161 | } 162 | 163 | impl std::fmt::Debug for Pool 164 | where 165 | M: ManageObject, 166 | M::Object: std::fmt::Debug, 167 | { 168 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 | f.debug_struct("Pool") 170 | .field("slots", &self.slots) 171 | .field("config", &self.config) 172 | .field("users", &self.users) 173 | .field("permits", &self.permits) 174 | .finish() 175 | } 176 | } 177 | 178 | impl Pool { 179 | /// Creates a new [`Pool`]. 180 | pub fn new(config: PoolConfig, manager: M) -> Arc { 181 | let users = AtomicUsize::new(0); 182 | let permits = Arc::new(Semaphore::new(config.max_size)); 183 | let slots = Mutex::new(PoolDeque { 184 | deque: VecDeque::with_capacity(config.max_size), 185 | current_size: 0, 186 | max_size: config.max_size, 187 | }); 188 | 189 | Arc::new(Self { 190 | config, 191 | manager, 192 | users, 193 | permits, 194 | slots, 195 | }) 196 | } 197 | 198 | /// Replenishes the pool with at most `most` number of new objects: 199 | /// 200 | /// 1. If the pool has fewer slots to fill than `most`, narrow `most` to the number of slots. 201 | /// 2. If there is already any idle object in the pool, decrease `most` by the number of idle 202 | /// objects. 203 | /// 3. If [`ManageObject::create`] returns `Err`, reduces `most` by 1 and continues to the next. 204 | /// 205 | /// Returns the number of objects that are actually replenished to the pool. This method is 206 | /// suitable to implement functionalities like minimal idle connections in a connection 207 | /// pool. 208 | pub async fn replenish(&self, most: usize) -> usize { 209 | let mut permit = { 210 | let mut n = most; 211 | loop { 212 | match self.permits.try_acquire(n) { 213 | Some(permit) => break permit, 214 | None => { 215 | n = n.min(self.permits.available_permits()); 216 | continue; 217 | } 218 | } 219 | } 220 | }; 221 | 222 | if permit.permits() == 0 { 223 | return 0; 224 | } 225 | 226 | let gap = { 227 | let idles = self.slots.lock().deque.len(); 228 | if idles >= permit.permits() { 229 | return 0; 230 | } 231 | 232 | match permit.split(idles) { 233 | None => unreachable!( 234 | "idles ({}) should be less than permits ({})", 235 | idles, 236 | permit.permits() 237 | ), 238 | Some(p) => { 239 | // reduced by existing idle objects and release the corresponding permits 240 | drop(p); 241 | } 242 | } 243 | 244 | permit.permits() 245 | }; 246 | 247 | let mut replenished = 0; 248 | for _ in 0..gap { 249 | if let Ok(o) = self.manager.create().await { 250 | let status = ObjectStatus::default(); 251 | let state = ObjectState { o, status }; 252 | 253 | let mut slots = self.slots.lock(); 254 | slots.current_size += 1; 255 | slots.deque.push_back(state); 256 | drop(slots); 257 | 258 | replenished += 1; 259 | } 260 | 261 | match permit.split(1) { 262 | None => unreachable!("permit must be greater than 0 at this point"), 263 | Some(p) => { 264 | // always release one permit to unblock other waiters 265 | drop(p); 266 | } 267 | } 268 | } 269 | 270 | replenished 271 | } 272 | 273 | /// Retrieves an [`Object`] from this [`Pool`]. 274 | /// 275 | /// This method should be called with a pool wrapped in an [`Arc`]. If the pool reaches the 276 | /// maximum size, this method would block until an object is returned to the pool or an object 277 | /// is detached from the pool. 278 | pub async fn get(self: &Arc) -> Result, M::Error> { 279 | self.users.fetch_add(1, Ordering::Relaxed); 280 | let guard = scopeguard::guard((), |()| { 281 | self.users.fetch_sub(1, Ordering::Relaxed); 282 | }); 283 | 284 | let permit = self.permits.clone().acquire_owned(1).await; 285 | 286 | let object = loop { 287 | let existing = match self.config.queue_strategy { 288 | QueueStrategy::Fifo => self.slots.lock().deque.pop_front(), 289 | QueueStrategy::Lifo => self.slots.lock().deque.pop_back(), 290 | }; 291 | 292 | match existing { 293 | None => { 294 | let object = self.manager.create().await?; 295 | let state = ObjectState { 296 | o: object, 297 | status: ObjectStatus::default(), 298 | }; 299 | self.slots.lock().current_size += 1; 300 | break Object { 301 | state: Some(state), 302 | permit, 303 | pool: Arc::downgrade(self), 304 | }; 305 | } 306 | Some(object) => { 307 | let mut unready_object = UnreadyObject { 308 | state: Some(object), 309 | pool: Arc::downgrade(self), 310 | }; 311 | 312 | let state = unready_object.state(); 313 | let status = state.status; 314 | if self 315 | .manager 316 | .is_recyclable(&mut state.o, &status) 317 | .await 318 | .is_ok() 319 | { 320 | state.status.recycle_count += 1; 321 | state.status.recycled = Some(std::time::Instant::now()); 322 | break unready_object.ready(permit); 323 | } 324 | } 325 | }; 326 | }; 327 | 328 | scopeguard::ScopeGuard::into_inner(guard); 329 | Ok(object) 330 | } 331 | 332 | /// Retains only the objects that pass the given predicate. 333 | /// 334 | /// This function blocks the entire pool. Therefore, the given function should not block. 335 | /// 336 | /// The following example starts a background task that runs every 30 seconds and removes 337 | /// objects from the pool that have not been used for more than one minute. The task will 338 | /// terminate if the pool is dropped. 339 | /// 340 | /// ```rust,ignore 341 | /// let interval = Duration::from_secs(30); 342 | /// let max_age = Duration::from_secs(60); 343 | /// 344 | /// let weak_pool = Arc::downgrade(&pool); 345 | /// tokio::spawn(async move { 346 | /// loop { 347 | /// tokio::time::sleep(interval).await; 348 | /// if let Some(pool) = weak_pool.upgrade() { 349 | /// pool.retain(|_, status| status.last_used().elapsed() < max_age); 350 | /// } else { 351 | /// break; 352 | /// } 353 | /// } 354 | /// }); 355 | /// ``` 356 | pub fn retain( 357 | &self, 358 | f: impl FnMut(&mut M::Object, ObjectStatus) -> bool, 359 | ) -> RetainResult { 360 | let mut slots = self.slots.lock(); 361 | let result = retain_spec::do_vec_deque_retain(&mut slots.deque, f); 362 | slots.current_size -= result.removed.len(); 363 | result 364 | } 365 | 366 | /// Returns the current status of the pool. 367 | /// 368 | /// The status returned by the pool is not guaranteed to be consistent. 369 | /// 370 | /// While this features provides [eventual consistency], the numbers will be 371 | /// off when accessing the status of a pool under heavy load. These numbers 372 | /// are meant for an overall insight. 373 | /// 374 | /// [eventual consistency]: (https://en.wikipedia.org/wiki/Eventual_consistency) 375 | pub fn status(&self) -> PoolStatus { 376 | let slots = self.slots.lock(); 377 | let (current_size, max_size) = (slots.current_size, slots.max_size); 378 | drop(slots); 379 | 380 | let users = self.users.load(Ordering::Relaxed); 381 | let (idle_count, wait_count) = if users < current_size { 382 | (current_size - users, 0) 383 | } else { 384 | (0, users - current_size) 385 | }; 386 | 387 | PoolStatus { 388 | max_size, 389 | current_size, 390 | idle_count, 391 | wait_count, 392 | } 393 | } 394 | 395 | fn push_back(&self, o: ObjectState) { 396 | let mut slots = self.slots.lock(); 397 | 398 | assert!( 399 | slots.current_size <= slots.max_size, 400 | "invariant broken: current_size <= max_size (actual: {} <= {})", 401 | slots.current_size, 402 | slots.max_size, 403 | ); 404 | 405 | slots.deque.push_back(o); 406 | drop(slots); 407 | 408 | self.users.fetch_sub(1, Ordering::Relaxed); 409 | } 410 | 411 | fn detach_object(&self, o: &mut M::Object, ready: bool) { 412 | let mut slots = self.slots.lock(); 413 | 414 | assert!( 415 | slots.current_size <= slots.max_size, 416 | "invariant broken: current_size <= max_size (actual: {} <= {})", 417 | slots.current_size, 418 | slots.max_size, 419 | ); 420 | 421 | slots.current_size -= 1; 422 | drop(slots); 423 | 424 | if ready { 425 | self.users.fetch_sub(1, Ordering::Relaxed); 426 | } else { 427 | // if the object is not ready, users count decrement is handled in the caller side, 428 | // that is, on exiting the `Pool::get` method. 429 | } 430 | self.manager.on_detached(o); 431 | } 432 | } 433 | 434 | /// A wrapper of the actual pooled object. 435 | /// 436 | /// This object implements [`Deref`] and [`DerefMut`]. You can use it as if it was of type 437 | /// `M::Object`. 438 | /// 439 | /// This object implements [`Drop`] that returns the underlying object to the pool on drop. You may 440 | /// call [`Object::detach`] to detach the object from the pool before dropping it. 441 | pub struct Object { 442 | state: Option>, 443 | permit: OwnedSemaphorePermit, 444 | pool: Weak>, 445 | } 446 | 447 | impl std::fmt::Debug for Object 448 | where 449 | M: ManageObject, 450 | M::Object: std::fmt::Debug, 451 | { 452 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 453 | f.debug_struct("Object") 454 | .field("state", &self.state) 455 | .field("permit", &self.permit) 456 | .finish() 457 | } 458 | } 459 | 460 | impl Drop for Object { 461 | fn drop(&mut self) { 462 | if let Some(state) = self.state.take() { 463 | if let Some(pool) = self.pool.upgrade() { 464 | pool.push_back(state); 465 | } 466 | } 467 | } 468 | } 469 | 470 | impl Deref for Object { 471 | type Target = M::Object; 472 | fn deref(&self) -> &M::Object { 473 | // SAFETY: `state` is always `Some` when `Object` is owned. 474 | &self.state.as_ref().unwrap().o 475 | } 476 | } 477 | 478 | impl DerefMut for Object { 479 | fn deref_mut(&mut self) -> &mut Self::Target { 480 | // SAFETY: `state` is always `Some` when `Object` is owned. 481 | &mut self.state.as_mut().unwrap().o 482 | } 483 | } 484 | 485 | impl AsRef for Object { 486 | fn as_ref(&self) -> &M::Object { 487 | self 488 | } 489 | } 490 | 491 | impl AsMut for Object { 492 | fn as_mut(&mut self) -> &mut M::Object { 493 | self 494 | } 495 | } 496 | 497 | impl Object { 498 | /// Detaches the object from the [`Pool`]. 499 | /// 500 | /// This reduces the size of the pool by one. 501 | pub fn detach(mut self) -> M::Object { 502 | // SAFETY: `state` is always `Some` when `Object` is owned. 503 | let mut o = self.state.take().unwrap().o; 504 | if let Some(pool) = self.pool.upgrade() { 505 | pool.detach_object(&mut o, true); 506 | } 507 | o 508 | } 509 | 510 | /// Returns the status of the object. 511 | pub fn status(&self) -> ObjectStatus { 512 | // SAFETY: `state` is always `Some` when `Object` is owned. 513 | self.state.as_ref().unwrap().status 514 | } 515 | } 516 | 517 | /// A wrapper of ObjectStatus that detaches the object from the pool when dropped. 518 | struct UnreadyObject { 519 | state: Option>, 520 | pool: Weak>, 521 | } 522 | 523 | impl Drop for UnreadyObject { 524 | fn drop(&mut self) { 525 | if let Some(mut state) = self.state.take() { 526 | if let Some(pool) = self.pool.upgrade() { 527 | pool.detach_object(&mut state.o, false); 528 | } 529 | } 530 | } 531 | } 532 | 533 | impl UnreadyObject { 534 | fn ready(mut self, permit: OwnedSemaphorePermit) -> Object { 535 | // SAFETY: `state` is always `Some` when `UnreadyObject` is owned. 536 | let state = Some(self.state.take().unwrap()); 537 | let pool = self.pool.clone(); 538 | Object { 539 | state, 540 | permit, 541 | pool, 542 | } 543 | } 544 | 545 | fn state(&mut self) -> &mut ObjectState { 546 | // SAFETY: `state` is always `Some` when `UnreadyObject` is owned. 547 | self.state.as_mut().unwrap() 548 | } 549 | } 550 | 551 | #[derive(Debug)] 552 | struct ObjectState { 553 | o: T, 554 | status: ObjectStatus, 555 | } 556 | 557 | impl retain_spec::SealedState for ObjectState { 558 | type Object = T; 559 | 560 | fn status(&self) -> ObjectStatus { 561 | self.status 562 | } 563 | 564 | fn mut_object(&mut self) -> &mut Self::Object { 565 | &mut self.o 566 | } 567 | 568 | fn take_object(self) -> Self::Object { 569 | self.o 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /fastpool/src/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::future::Future; 16 | use std::time::Instant; 17 | 18 | /// Statistics regarding an object returned by the pool. 19 | #[derive(Debug, Clone, Copy)] 20 | pub struct ObjectStatus { 21 | created: Instant, 22 | pub(crate) recycled: Option, 23 | pub(crate) recycle_count: usize, 24 | } 25 | 26 | impl Default for ObjectStatus { 27 | fn default() -> Self { 28 | Self { 29 | created: Instant::now(), 30 | recycled: None, 31 | recycle_count: 0, 32 | } 33 | } 34 | } 35 | 36 | impl ObjectStatus { 37 | /// Returns the instant when this object was created. 38 | pub fn created(&self) -> Instant { 39 | self.created 40 | } 41 | 42 | /// Returns the instant when this object was last used. 43 | pub fn last_used(&self) -> Instant { 44 | self.recycled.unwrap_or(self.created) 45 | } 46 | 47 | /// Returns the number of times the object was recycled. 48 | pub fn recycle_count(&self) -> usize { 49 | self.recycle_count 50 | } 51 | } 52 | 53 | /// A trait whose instance creates new objects and recycles existing ones. 54 | pub trait ManageObject: Send + Sync { 55 | /// The type of objects that this instance creates and recycles. 56 | type Object: Send; 57 | 58 | /// The type of errors that this instance can return. 59 | type Error: Send; 60 | 61 | /// Creates a new object. 62 | fn create(&self) -> impl Future> + Send; 63 | 64 | /// Whether the object `o` is recyclable. 65 | /// 66 | /// Returns `Ok(())` if the object is recyclable; otherwise, returns an error. 67 | fn is_recyclable( 68 | &self, 69 | o: &mut Self::Object, 70 | status: &ObjectStatus, 71 | ) -> impl Future> + Send; 72 | 73 | /// A callback invoked when an object is detached from the pool. 74 | /// 75 | /// If this instance does not hold any references to the object, then the default 76 | /// implementation can be used which does nothing. 77 | fn on_detached(&self, _o: &mut Self::Object) {} 78 | } 79 | 80 | /// Queue strategy when deque objects from the object pool. 81 | #[derive(Debug, Default, Clone, Copy)] 82 | pub enum QueueStrategy { 83 | /// First in first out. 84 | /// 85 | /// This strategy behaves like a queue. 86 | #[default] 87 | Fifo, 88 | /// Last in first out. 89 | /// 90 | /// This strategy behaves like a stack. 91 | Lifo, 92 | } 93 | -------------------------------------------------------------------------------- /fastpool/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![deny(missing_docs)] 16 | 17 | //! Fastpool provides fast and runtime-agnostic object pools for Async Rust. 18 | //! 19 | //! This crate provides two implementations: [bounded pool](bounded::Pool) and 20 | //! [unbounded pool](unbounded::Pool). 21 | //! 22 | //! # Bounded pool 23 | //! 24 | //! A bounded pool creates and recycles objects with full management. You _cannot_ put an object to 25 | //! the pool manually. 26 | //! 27 | //! The pool is bounded by the `max_size` config option of [`PoolConfig`](bounded::PoolConfig). If 28 | //! the pool reaches the maximum size, it will block all the [`Pool::get`](bounded::Pool::get) calls 29 | //! until an object is returned to the pool or an object is detached from the pool. 30 | //! 31 | //! Bounded pools are useful for pooling database connections. 32 | //! 33 | //! ## Examples 34 | //! 35 | //! Read the following simple demo or more complex examples in the examples directory. 36 | //! 37 | //! ``` 38 | //! use std::future::Future; 39 | //! 40 | //! use fastpool::ManageObject; 41 | //! use fastpool::ObjectStatus; 42 | //! use fastpool::bounded::Pool; 43 | //! use fastpool::bounded::PoolConfig; 44 | //! 45 | //! struct Compute; 46 | //! impl Compute { 47 | //! async fn do_work(&self) -> i32 { 48 | //! 42 49 | //! } 50 | //! } 51 | //! 52 | //! struct Manager; 53 | //! impl ManageObject for Manager { 54 | //! type Object = Compute; 55 | //! type Error = (); 56 | //! 57 | //! async fn create(&self) -> Result { 58 | //! Ok(Compute) 59 | //! } 60 | //! 61 | //! async fn is_recyclable( 62 | //! &self, 63 | //! o: &mut Self::Object, 64 | //! status: &ObjectStatus, 65 | //! ) -> Result<(), Self::Error> { 66 | //! Ok(()) 67 | //! } 68 | //! } 69 | //! 70 | //! # #[tokio::main] 71 | //! # async fn main() { 72 | //! let pool = Pool::new(PoolConfig::new(16), Manager); 73 | //! let o = pool.get().await.unwrap(); 74 | //! assert_eq!(o.do_work().await, 42); 75 | //! # } 76 | //! ``` 77 | //! 78 | //! # Unbounded pool 79 | //! 80 | //! An unbounded pool, on the other hand, allows you to put objects to the pool manually. You can 81 | //! use it like Go's [`sync.Pool`](https://pkg.go.dev/sync#Pool). 82 | //! 83 | //! To configure a factory for creating objects when the pool is empty, like `sync.Pool`'s `New`, 84 | //! you can create the unbounded pool via [`Pool::new`](unbounded::Pool::new) with an 85 | //! implementation of [`ManageObject`]. 86 | //! 87 | //! ## Examples 88 | //! 89 | //! Read the following simple demo or more complex examples in the examples directory. 90 | //! 91 | //! ``` 92 | //! use fastpool::unbounded::Pool; 93 | //! use fastpool::unbounded::PoolConfig; 94 | //! 95 | //! # #[tokio::main] 96 | //! # async fn main() { 97 | //! let pool = Pool::>::never_manage(PoolConfig::default()); 98 | //! 99 | //! let result = pool.get().await; 100 | //! assert_eq!(result.unwrap_err().to_string(), "unbounded pool is empty"); 101 | //! 102 | //! pool.extend_one(Vec::with_capacity(1024)); 103 | //! let o = pool.get().await.unwrap(); 104 | //! assert_eq!(o.capacity(), 1024); 105 | //! # } 106 | //! ``` 107 | //! 108 | //! # FAQ 109 | //! 110 | //! ## Why does fastpool have no timeout config? 111 | //! 112 | //! Many async object pool implementations allow you to configure multiple timeout, like "wait 113 | //! timeout", "create timeout", "recycle timeout", etc. 114 | //! 115 | //! This introduces two major problems: 116 | //! 117 | //! First, to support timeouts, the pool must use with a timer implementation like `tokio::time`. 118 | //! This would prevent the pool from being runtime-agnostic. Theoretically, the pool can depend 119 | //! on a timer trait, but there is no such a standard trait in the Rust ecosystem yet. 120 | //! 121 | //! Second, timeouts options not only add complexity for configuration, but also the value itself 122 | //! cannot be configured properly at all. For example, end users often care about the total time 123 | //! used to obtain an object. This is not solely "wait timeout", "create timeout", or 124 | //! "recycle timeout", but a conditional composition of all internal operations. 125 | //! 126 | //! Thus, we propose a caller-side timeout solution: 127 | //! 128 | //! ```rust,ignore 129 | //! use std::sync::Arc; 130 | //! use std::time::Duration; 131 | //! 132 | //! use fastpool::bounded::Object; 133 | //! use fastpool::bounded::Pool; 134 | //! 135 | //! #[derive(Debug, Clone)] 136 | //! pub struct ConnectionPool { 137 | //! pool: Arc>, 138 | //! } 139 | //! 140 | //! impl ConnectionPool { 141 | //! pub async fn acquire(&self) -> Result, Error> { 142 | //! const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60); 143 | //! 144 | //! // note that users can choose any timer implementation here 145 | //! let result = tokio::time::timeout(ACQUIRE_TIMEOUT, self.pool.get()).await; 146 | //! 147 | //! // ... processing the result 148 | //! } 149 | //! } 150 | //! ``` 151 | //! 152 | //! Check out the postgres example in the examples directory for the complete code. 153 | //! 154 | //! ## Why does fastpool have no before/after hooks? 155 | //! 156 | //! Similar to the second point of the previous question, the before/after hooks (closures) are 157 | //! very hard to configure properly. Specific to closures, many Rust code can be easily written 158 | //! in place, but if you'd like to pass a code block as a closure, then you may encounter a lot of 159 | //! lifetime and ownership issues. Besides, how to handle `Result` in the closure is also a 160 | //! headache. 161 | //! 162 | //! Fastpool provides an ordinary interface of object pools, so that you should be able to add 163 | //! any before/after logic in the implementation or using a wrapper. 164 | //! 165 | //! For example, all the "post-create", "pre-recycle", and "post-recycle" hooks can be implemented 166 | //! as: 167 | //! 168 | //! ``` 169 | //! use std::future::Future; 170 | //! 171 | //! use fastpool::ManageObject; 172 | //! use fastpool::ObjectStatus; 173 | //! 174 | //! struct Manager; 175 | //! impl ManageObject for Manager { 176 | //! type Object = i32; 177 | //! type Error = std::convert::Infallible; 178 | //! 179 | //! async fn create(&self) -> Result { 180 | //! let o = 42; 181 | //! // any post-create hooks 182 | //! Ok(o) 183 | //! } 184 | //! 185 | //! async fn is_recyclable( 186 | //! &self, 187 | //! _o: &mut Self::Object, 188 | //! _status: &ObjectStatus, 189 | //! ) -> Result<(), Self::Error> { 190 | //! // any pre-recycle hooks 191 | //! // determinate if is_recyclable 192 | //! // any post-recycle hooks 193 | //! Ok(()) 194 | //! } 195 | //! } 196 | //! ``` 197 | 198 | pub use common::ManageObject; 199 | pub use common::ObjectStatus; 200 | pub use common::QueueStrategy; 201 | pub use retain_spec::RetainResult; 202 | 203 | mod common; 204 | mod mutex; 205 | mod retain_spec; 206 | 207 | pub mod bounded; 208 | pub mod unbounded; 209 | -------------------------------------------------------------------------------- /fastpool/src/mutex.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::sync::PoisonError; 17 | 18 | pub(crate) struct Mutex(std::sync::Mutex); 19 | 20 | impl fmt::Debug for Mutex { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | self.0.fmt(f) 23 | } 24 | } 25 | 26 | impl Mutex { 27 | pub(crate) const fn new(t: T) -> Self { 28 | Self(std::sync::Mutex::new(t)) 29 | } 30 | 31 | pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, T> { 32 | self.0.lock().unwrap_or_else(PoisonError::into_inner) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use std::sync::Arc; 39 | 40 | use super::*; 41 | 42 | #[test] 43 | fn test_poison_mutex() { 44 | let mutex = Arc::new(Mutex::new(42)); 45 | let m = mutex.clone(); 46 | let handle = std::thread::spawn(move || { 47 | let _guard = m.lock(); 48 | panic!("poison"); 49 | }); 50 | let _ = handle.join(); 51 | let guard = mutex.lock(); 52 | assert_eq!(*guard, 42); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /fastpool/src/retain_spec.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::collections::VecDeque; 16 | 17 | use crate::ObjectStatus; 18 | 19 | /// The result returned by `Pool::retain`. 20 | #[derive(Debug)] 21 | #[non_exhaustive] 22 | pub struct RetainResult { 23 | /// The number of retained objects. 24 | pub retained: usize, 25 | /// The objects removed from the pool. 26 | pub removed: Vec, 27 | } 28 | 29 | /// An internal trait that abstracts over unbounded and bounded `ObjectState`. 30 | pub(crate) trait SealedState { 31 | /// The type of the object. 32 | type Object; 33 | 34 | /// Returns the status of the object. 35 | fn status(&self) -> ObjectStatus; 36 | /// Returns a mutable reference to the object. 37 | fn mut_object(&mut self) -> &mut Self::Object; 38 | /// Returns the owned object, consuming the state. 39 | fn take_object(self) -> Self::Object; 40 | } 41 | 42 | /// Shared `VecDeque`'s retain (`extract_if`) implementation for both bounded and unbounded pools. 43 | pub(crate) fn do_vec_deque_retain>( 44 | deque: &mut VecDeque, 45 | mut f: impl FnMut(&mut T, ObjectStatus) -> bool, 46 | ) -> RetainResult { 47 | let len = deque.len(); 48 | let mut idx = 0; 49 | let mut cur = 0; 50 | 51 | // Stage 1: All values are retained. 52 | while cur < len { 53 | let state = &mut deque[cur]; 54 | let status = state.status(); 55 | if !f(state.mut_object(), status) { 56 | cur += 1; 57 | break; 58 | } 59 | cur += 1; 60 | idx += 1; 61 | } 62 | 63 | // Stage 2: Swap retained value into current idx. 64 | while cur < len { 65 | let state = &mut deque[cur]; 66 | let status = state.status(); 67 | if !f(state.mut_object(), status) { 68 | cur += 1; 69 | continue; 70 | } 71 | 72 | deque.swap(idx, cur); 73 | cur += 1; 74 | idx += 1; 75 | } 76 | 77 | // Stage 3: Truncate all values after idx. 78 | let removed = if cur != idx { 79 | let removed = deque.split_off(idx); 80 | removed.into_iter().map(State::take_object).collect() 81 | } else { 82 | Vec::new() 83 | }; 84 | 85 | RetainResult { 86 | retained: idx, 87 | removed, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /fastpool/src/unbounded.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Unbounded object pools. 16 | //! 17 | //! An unbounded pool, on the other hand, allows you to put objects to the pool manually. You can 18 | //! use it like Go's [`sync.Pool`](https://pkg.go.dev/sync#Pool). 19 | //! 20 | //! To configure a factory for creating objects when the pool is empty, like `sync.Pool`'s `New`, 21 | //! you can create the unbounded pool via [`Pool::new`](Pool::new) with an 22 | //! implementation of [`ManageObject`]. 23 | //! 24 | //! ## Examples 25 | //! 26 | //! Read the following simple demos or more complex examples in the examples directory. 27 | //! 28 | //! 1. Create an unbounded pool with [`NeverManageObject`]: 29 | //! 30 | //! ``` 31 | //! use fastpool::unbounded::Pool; 32 | //! use fastpool::unbounded::PoolConfig; 33 | //! 34 | //! # #[tokio::main] 35 | //! # async fn main() { 36 | //! let pool = Pool::>::never_manage(PoolConfig::default()); 37 | //! 38 | //! let result = pool.get().await; 39 | //! assert_eq!(result.unwrap_err().to_string(), "unbounded pool is empty"); 40 | //! 41 | //! pool.extend_one(Vec::with_capacity(1024)); 42 | //! let o = pool.get().await.unwrap(); 43 | //! assert_eq!(o.capacity(), 1024); 44 | //! drop(o); 45 | //! let o = pool.get().await.unwrap(); 46 | //! assert_eq!(o.capacity(), 1024); 47 | //! let result = pool.get().await; 48 | //! assert_eq!(result.unwrap_err().to_string(), "unbounded pool is empty"); 49 | //! # } 50 | //! ``` 51 | //! 52 | //! 2. Create an unbounded pool with a custom [`ManageObject`] (object factory): 53 | //! 54 | //! ``` 55 | //! use std::future::Future; 56 | //! 57 | //! use fastpool::ManageObject; 58 | //! use fastpool::ObjectStatus; 59 | //! use fastpool::unbounded::Pool; 60 | //! use fastpool::unbounded::PoolConfig; 61 | //! 62 | //! struct Compute; 63 | //! impl Compute { 64 | //! async fn do_work(&self) -> i32 { 65 | //! 42 66 | //! } 67 | //! } 68 | //! 69 | //! struct Manager; 70 | //! impl ManageObject for Manager { 71 | //! type Object = Compute; 72 | //! type Error = (); 73 | //! 74 | //! async fn create(&self) -> Result { 75 | //! Ok(Compute) 76 | //! } 77 | //! 78 | //! async fn is_recyclable( 79 | //! &self, 80 | //! o: &mut Self::Object, 81 | //! status: &ObjectStatus, 82 | //! ) -> Result<(), Self::Error> { 83 | //! Ok(()) 84 | //! } 85 | //! } 86 | //! 87 | //! # #[tokio::main] 88 | //! # async fn main() { 89 | //! let pool = Pool::new(PoolConfig::default(), Manager); 90 | //! let o = pool.get().await.unwrap(); 91 | //! assert_eq!(o.do_work().await, 42); 92 | //! # } 93 | //! ``` 94 | 95 | use std::collections::VecDeque; 96 | use std::future::Future; 97 | use std::ops::Deref; 98 | use std::ops::DerefMut; 99 | use std::sync::Arc; 100 | use std::sync::Weak; 101 | 102 | use crate::ManageObject; 103 | use crate::ObjectStatus; 104 | use crate::QueueStrategy; 105 | use crate::RetainResult; 106 | use crate::mutex::Mutex; 107 | use crate::retain_spec; 108 | 109 | /// The configuration of [`Pool`]. 110 | #[derive(Clone, Copy, Debug)] 111 | #[non_exhaustive] 112 | pub struct PoolConfig { 113 | /// Queue strategy of the [`Pool`]. 114 | /// 115 | /// Determines the order of objects being queued and dequeued. 116 | pub queue_strategy: QueueStrategy, 117 | } 118 | 119 | impl Default for PoolConfig { 120 | fn default() -> Self { 121 | Self::new() 122 | } 123 | } 124 | 125 | impl PoolConfig { 126 | /// Creates a new [`PoolConfig`]. 127 | pub fn new() -> Self { 128 | Self { 129 | queue_strategy: QueueStrategy::default(), 130 | } 131 | } 132 | 133 | /// Returns a new [`PoolConfig`] with the specified queue strategy. 134 | pub fn with_queue_strategy(mut self, queue_strategy: QueueStrategy) -> Self { 135 | self.queue_strategy = queue_strategy; 136 | self 137 | } 138 | } 139 | 140 | /// The current pool status. 141 | /// 142 | /// See [`Pool::status`]. 143 | #[derive(Clone, Copy, Debug)] 144 | #[non_exhaustive] 145 | pub struct PoolStatus { 146 | /// The current size of the pool. 147 | pub current_size: usize, 148 | 149 | /// The number of idle objects in the pool. 150 | pub idle_count: usize, 151 | } 152 | 153 | /// The default [`ManageObject`] implementation for unbounded pool. 154 | /// 155 | /// * [`NeverManageObject::create`] always returns [`PoolIsEmpty`] so that [`Pool::get`] would get 156 | /// the error if no object is in the pool. 157 | /// * [`NeverManageObject::is_recyclable`] always returns `Ok(())` so that any object is always 158 | /// recyclable. 159 | #[derive(Debug, Copy, Clone)] 160 | pub struct NeverManageObject { 161 | _marker: std::marker::PhantomData, 162 | } 163 | 164 | impl Default for NeverManageObject { 165 | fn default() -> Self { 166 | Self { 167 | _marker: std::marker::PhantomData, 168 | } 169 | } 170 | } 171 | 172 | /// The error returned by [`NeverManageObject::create`]. 173 | pub struct PoolIsEmpty(()); 174 | 175 | impl std::fmt::Debug for PoolIsEmpty { 176 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 177 | write!(f, "unbounded pool is empty") 178 | } 179 | } 180 | 181 | impl std::fmt::Display for PoolIsEmpty { 182 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 183 | std::fmt::Debug::fmt(self, f) 184 | } 185 | } 186 | 187 | impl std::error::Error for PoolIsEmpty {} 188 | 189 | impl ManageObject for NeverManageObject { 190 | type Object = T; 191 | type Error = PoolIsEmpty; 192 | 193 | fn create(&self) -> impl Future> + Send { 194 | std::future::ready(Err(PoolIsEmpty(()))) 195 | } 196 | 197 | fn is_recyclable( 198 | &self, 199 | _: &mut Self::Object, 200 | _: &ObjectStatus, 201 | ) -> impl Future> + Send { 202 | std::future::ready(Ok(())) 203 | } 204 | } 205 | 206 | /// Generic runtime-agnostic unbounded object pool. 207 | /// 208 | /// See the [module level documentation](self) for more. 209 | pub struct Pool = NeverManageObject> { 210 | config: PoolConfig, 211 | manager: M, 212 | 213 | /// A deque that holds the objects. 214 | slots: Mutex>>, 215 | } 216 | 217 | #[derive(Debug)] 218 | struct PoolDeque { 219 | deque: VecDeque, 220 | current_size: usize, 221 | } 222 | 223 | impl std::fmt::Debug for Pool 224 | where 225 | T: std::fmt::Debug, 226 | M: ManageObject, 227 | { 228 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 229 | f.debug_struct("Pool") 230 | .field("slots", &self.slots) 231 | .field("config", &self.config) 232 | .finish() 233 | } 234 | } 235 | 236 | // Methods for `Pool` with `NeverManageObject`. 237 | impl Pool { 238 | /// Creates a new [`Pool`] from config and the [`NeverManageObject`]. 239 | pub fn never_manage(config: PoolConfig) -> Arc { 240 | Self::new(config, NeverManageObject::::default()) 241 | } 242 | 243 | /// Retrieves an [`Object`] from this [`Pool`], or creates a new one with the passed-in async 244 | /// closure, if the pool is empty. 245 | /// 246 | /// This method should be called with a pool wrapped in an [`Arc`]. 247 | /// 248 | /// This method only exists for [`NeverManageObject`] pools. If you provide a custom 249 | /// [`ManageObject`] implementation, you should use [`Pool::get`] instead, and it will call 250 | /// [`ManageObject::create`] to create a new object if the pool is empty. 251 | pub async fn get_or_create(self: &Arc, f: F) -> Result, E> 252 | where 253 | F: AsyncFnOnce() -> Result + Send, 254 | { 255 | let existing = match self.config.queue_strategy { 256 | QueueStrategy::Fifo => self.slots.lock().deque.pop_front(), 257 | QueueStrategy::Lifo => self.slots.lock().deque.pop_back(), 258 | }; 259 | 260 | match existing { 261 | None => { 262 | let object = f().await?; 263 | let state = ObjectState { 264 | o: object, 265 | status: ObjectStatus::default(), 266 | }; 267 | self.slots.lock().current_size += 1; 268 | Ok(Object { 269 | state: Some(state), 270 | pool: Arc::downgrade(self), 271 | }) 272 | } 273 | Some(mut state) => { 274 | state.status.recycle_count += 1; 275 | state.status.recycled = Some(std::time::Instant::now()); 276 | Ok(Object { 277 | state: Some(state), 278 | pool: Arc::downgrade(self), 279 | }) 280 | } 281 | } 282 | } 283 | } 284 | 285 | impl> Pool { 286 | /// Creates a new [`Pool`] with config and the specified [`ManageObject`]. 287 | pub fn new(config: PoolConfig, manager: M) -> Arc { 288 | let slots = Mutex::new(PoolDeque { 289 | deque: VecDeque::new(), 290 | current_size: 0, 291 | }); 292 | 293 | Arc::new(Self { 294 | config, 295 | manager, 296 | slots, 297 | }) 298 | } 299 | 300 | /// Retrieves an [`Object`] from this [`Pool`]. 301 | /// 302 | /// This method should be called with a pool wrapped in an [`Arc`]. 303 | pub async fn get(self: &Arc) -> Result, M::Error> { 304 | let object = loop { 305 | let existing = match self.config.queue_strategy { 306 | QueueStrategy::Fifo => self.slots.lock().deque.pop_front(), 307 | QueueStrategy::Lifo => self.slots.lock().deque.pop_back(), 308 | }; 309 | 310 | match existing { 311 | None => { 312 | let object = self.manager.create().await?; 313 | let state = ObjectState { 314 | o: object, 315 | status: ObjectStatus::default(), 316 | }; 317 | self.slots.lock().current_size += 1; 318 | break Object { 319 | state: Some(state), 320 | pool: Arc::downgrade(self), 321 | }; 322 | } 323 | Some(object) => { 324 | let mut unready_object = UnreadyObject { 325 | state: Some(object), 326 | pool: Arc::downgrade(self), 327 | }; 328 | 329 | let state = unready_object.state(); 330 | let status = state.status; 331 | if self 332 | .manager 333 | .is_recyclable(&mut state.o, &status) 334 | .await 335 | .is_ok() 336 | { 337 | state.status.recycle_count += 1; 338 | state.status.recycled = Some(std::time::Instant::now()); 339 | break unready_object.ready(); 340 | } 341 | } 342 | }; 343 | }; 344 | 345 | Ok(object) 346 | } 347 | 348 | /// Extends the pool with exactly one object. 349 | /// 350 | /// # Examples 351 | /// 352 | /// ``` 353 | /// use fastpool::unbounded::Pool; 354 | /// use fastpool::unbounded::PoolConfig; 355 | /// 356 | /// let config = PoolConfig::default(); 357 | /// let pool = Pool::never_manage(config); 358 | /// 359 | /// pool.extend_one(Vec::::with_capacity(1024)); 360 | /// ``` 361 | pub fn extend_one(&self, o: T) { 362 | self.extend(Some(o)); 363 | } 364 | 365 | /// Extends the pool with the objects of an iterator. 366 | /// 367 | /// # Examples 368 | /// 369 | /// ``` 370 | /// use fastpool::unbounded::Pool; 371 | /// use fastpool::unbounded::PoolConfig; 372 | /// 373 | /// let config = PoolConfig::default(); 374 | /// let pool = Pool::never_manage(config); 375 | /// 376 | /// pool.extend([ 377 | /// Vec::::with_capacity(1024), 378 | /// Vec::::with_capacity(512), 379 | /// Vec::::with_capacity(256), 380 | /// ]); 381 | /// ``` 382 | pub fn extend(&self, iter: impl IntoIterator) { 383 | let mut slots = self.slots.lock(); 384 | for o in iter { 385 | slots.current_size += 1; 386 | slots.deque.push_back(ObjectState { 387 | o, 388 | status: ObjectStatus::default(), 389 | }); 390 | } 391 | } 392 | 393 | /// Retains only the objects that pass the given predicate. 394 | /// 395 | /// This function blocks the entire pool. Therefore, the given function should not block. 396 | /// 397 | /// The following example starts a background task that runs every 30 seconds and removes 398 | /// objects from the pool that have not been used for more than one minute. The task will 399 | /// terminate if the pool is dropped. 400 | /// 401 | /// ```rust,ignore 402 | /// let interval = Duration::from_secs(30); 403 | /// let max_age = Duration::from_secs(60); 404 | /// 405 | /// let weak_pool = Arc::downgrade(&pool); 406 | /// tokio::spawn(async move { 407 | /// loop { 408 | /// tokio::time::sleep(interval).await; 409 | /// if let Some(pool) = weak_pool.upgrade() { 410 | /// pool.retain(|_, status| status.last_used().elapsed() < max_age); 411 | /// } else { 412 | /// break; 413 | /// } 414 | /// } 415 | /// }); 416 | /// ``` 417 | pub fn retain( 418 | &self, 419 | f: impl FnMut(&mut M::Object, ObjectStatus) -> bool, 420 | ) -> RetainResult { 421 | let mut slots = self.slots.lock(); 422 | let result = retain_spec::do_vec_deque_retain(&mut slots.deque, f); 423 | slots.current_size -= result.removed.len(); 424 | result 425 | } 426 | 427 | /// Returns the current status of the pool. 428 | /// 429 | /// The status returned by the pool is not guaranteed to be consistent. 430 | /// 431 | /// While this features provides [eventual consistency], the numbers will be 432 | /// off when accessing the status of a pool under heavy load. These numbers 433 | /// are meant for an overall insight. 434 | /// 435 | /// [eventual consistency]: (https://en.wikipedia.org/wiki/Eventual_consistency) 436 | pub fn status(&self) -> PoolStatus { 437 | let slots = self.slots.lock(); 438 | let (current_size, idle_count) = (slots.current_size, slots.deque.len()); 439 | drop(slots); 440 | 441 | PoolStatus { 442 | current_size, 443 | idle_count, 444 | } 445 | } 446 | 447 | fn push_back(&self, o: ObjectState) { 448 | let mut slots = self.slots.lock(); 449 | slots.deque.push_back(o); 450 | drop(slots); 451 | } 452 | 453 | fn detach_object(&self, o: &mut T) { 454 | let mut slots = self.slots.lock(); 455 | slots.current_size -= 1; 456 | drop(slots); 457 | self.manager.on_detached(o); 458 | } 459 | } 460 | 461 | /// A wrapper of the actual pooled object. 462 | /// 463 | /// This object implements [`Deref`] and [`DerefMut`]. You can use it as if it was of type `T`. 464 | /// 465 | /// This object implements [`Drop`] that returns the underlying object to the pool on drop. You may 466 | /// call [`Object::detach`] to detach the object from the pool before dropping it. 467 | pub struct Object = NeverManageObject> { 468 | state: Option>, 469 | pool: Weak>, 470 | } 471 | 472 | impl std::fmt::Debug for Object 473 | where 474 | T: std::fmt::Debug, 475 | M: ManageObject, 476 | { 477 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 478 | f.debug_struct("Object") 479 | .field("state", &self.state) 480 | .finish() 481 | } 482 | } 483 | 484 | impl> Drop for Object { 485 | fn drop(&mut self) { 486 | if let Some(state) = self.state.take() { 487 | if let Some(pool) = self.pool.upgrade() { 488 | pool.push_back(state); 489 | } 490 | } 491 | } 492 | } 493 | 494 | impl> Deref for Object { 495 | type Target = T; 496 | fn deref(&self) -> &T { 497 | // SAFETY: `state` is always `Some` when `Object` is owned. 498 | &self.state.as_ref().unwrap().o 499 | } 500 | } 501 | 502 | impl> DerefMut for Object { 503 | fn deref_mut(&mut self) -> &mut Self::Target { 504 | // SAFETY: `state` is always `Some` when `Object` is owned. 505 | &mut self.state.as_mut().unwrap().o 506 | } 507 | } 508 | 509 | impl> AsRef for Object { 510 | fn as_ref(&self) -> &M::Object { 511 | self 512 | } 513 | } 514 | 515 | impl> AsMut for Object { 516 | fn as_mut(&mut self) -> &mut M::Object { 517 | self 518 | } 519 | } 520 | 521 | impl> Object { 522 | /// Detaches the object from the [`Pool`]. 523 | /// 524 | /// This reduces the size of the pool by one. 525 | pub fn detach(mut self) -> M::Object { 526 | // SAFETY: `state` is always `Some` when `Object` is owned. 527 | let mut o = self.state.take().unwrap().o; 528 | if let Some(pool) = self.pool.upgrade() { 529 | pool.detach_object(&mut o); 530 | } 531 | o 532 | } 533 | 534 | /// Returns the status of the object. 535 | pub fn status(&self) -> ObjectStatus { 536 | // SAFETY: `state` is always `Some` when `Object` is owned. 537 | self.state.as_ref().unwrap().status 538 | } 539 | } 540 | 541 | /// A wrapper of ObjectStatus that detaches the object from the pool when dropped. 542 | struct UnreadyObject = NeverManageObject> { 543 | state: Option>, 544 | pool: Weak>, 545 | } 546 | 547 | impl> Drop for UnreadyObject { 548 | fn drop(&mut self) { 549 | if let Some(mut state) = self.state.take() { 550 | if let Some(pool) = self.pool.upgrade() { 551 | pool.detach_object(&mut state.o); 552 | } 553 | } 554 | } 555 | } 556 | 557 | impl> UnreadyObject { 558 | fn ready(mut self) -> Object { 559 | // SAFETY: `state` is always `Some` when `UnreadyObject` is owned. 560 | let state = Some(self.state.take().unwrap()); 561 | let pool = self.pool.clone(); 562 | Object { state, pool } 563 | } 564 | 565 | fn state(&mut self) -> &mut ObjectState { 566 | // SAFETY: `state` is always `Some` when `UnreadyObject` is owned. 567 | self.state.as_mut().unwrap() 568 | } 569 | } 570 | 571 | #[derive(Debug)] 572 | struct ObjectState { 573 | o: T, 574 | status: ObjectStatus, 575 | } 576 | 577 | impl retain_spec::SealedState for ObjectState { 578 | type Object = T; 579 | 580 | fn status(&self) -> ObjectStatus { 581 | self.status 582 | } 583 | 584 | fn mut_object(&mut self) -> &mut Self::Object { 585 | &mut self.o 586 | } 587 | 588 | fn take_object(self) -> Self::Object { 589 | self.o 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /fastpool/tests/replenish_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::convert::Infallible; 16 | use std::sync::Arc; 17 | 18 | use fastpool::ManageObject; 19 | use fastpool::ObjectStatus; 20 | use fastpool::bounded::Pool; 21 | use fastpool::bounded::PoolConfig; 22 | 23 | #[tokio::test] 24 | async fn test_replenish() { 25 | #[derive(Default)] 26 | struct Manager; 27 | 28 | impl ManageObject for Manager { 29 | type Object = (); 30 | type Error = Infallible; 31 | 32 | async fn create(&self) -> Result { 33 | Ok(()) 34 | } 35 | 36 | async fn is_recyclable( 37 | &self, 38 | _o: &mut Self::Object, 39 | _status: &ObjectStatus, 40 | ) -> Result<(), Self::Error> { 41 | Ok(()) 42 | } 43 | } 44 | 45 | const MAX_SIZE: usize = 2; 46 | 47 | fn make_default() -> Arc> { 48 | Pool::new(PoolConfig::new(MAX_SIZE), Manager) 49 | } 50 | 51 | for i in 0..5 { 52 | let pool = make_default(); 53 | let n = pool.replenish(i).await; 54 | assert_eq!(n, i.min(MAX_SIZE)); 55 | } 56 | 57 | // stage one idle object 58 | { 59 | let pool = make_default(); 60 | pool.get().await.unwrap(); 61 | let n = pool.replenish(2).await; 62 | assert_eq!(n, 1); 63 | } 64 | 65 | // stage two idle objects 66 | { 67 | let pool = make_default(); 68 | let o1 = pool.get().await.unwrap(); 69 | let o2 = pool.get().await.unwrap(); 70 | drop((o1, o2)); 71 | 72 | let n = pool.replenish(2).await; 73 | assert_eq!(n, 0); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | headerPath = "Apache-2.0.txt" 16 | 17 | includes = ['**/*.proto', '**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml'] 18 | 19 | [properties] 20 | copyrightOwner = "FastLabs Developers" 21 | inceptionYear = 2025 22 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [toolchain] 16 | channel = "stable" 17 | components = ["cargo", "rustfmt", "clippy", "rust-analyzer"] 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | comment_width = 120 16 | format_code_in_doc_comments = true 17 | group_imports = "StdExternalCrate" 18 | imports_granularity = "Item" 19 | wrap_comments = true 20 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | include = ["Cargo.toml", "**/*.toml"] 16 | 17 | [formatting] 18 | # Align consecutive entries vertically. 19 | align_entries = false 20 | # Append trailing commas for multi-line arrays. 21 | array_trailing_comma = true 22 | # Expand arrays to multiple lines that exceed the maximum column width. 23 | array_auto_expand = true 24 | # Collapse arrays that don't exceed the maximum column width and don't contain comments. 25 | array_auto_collapse = true 26 | # Omit white space padding from single-line arrays 27 | compact_arrays = true 28 | # Omit white space padding from the start and end of inline tables. 29 | compact_inline_tables = false 30 | # Maximum column width in characters, affects array expansion and collapse, this doesn't take whitespace into account. 31 | # Note that this is not set in stone, and works on a best-effort basis. 32 | column_width = 80 33 | # Indent based on tables and arrays of tables and their subtables, subtables out of order are not indented. 34 | indent_tables = false 35 | # The substring that is used for indentation, should be tabs or spaces (but technically can be anything). 36 | indent_string = ' ' 37 | # Add trailing newline at the end of the file if not present. 38 | trailing_newline = true 39 | # Alphabetically reorder keys that are not separated by empty lines. 40 | reorder_keys = true 41 | # Maximum amount of allowed consecutive blank lines. This does not affect the whitespace at the end of the document, as it is always stripped. 42 | allowed_blank_lines = 1 43 | # Use CRLF for line endings. 44 | crlf = false 45 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [default.extend-words] 16 | 17 | [files] 18 | extend-exclude = [] 19 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 FastLabs Developers 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "x" 17 | publish = false 18 | 19 | edition.workspace = true 20 | homepage.workspace = true 21 | license.workspace = true 22 | readme.workspace = true 23 | repository.workspace = true 24 | rust-version.workspace = true 25 | 26 | [package.metadata.release] 27 | release = false 28 | 29 | [dependencies] 30 | clap = { version = "4.5.23", features = ["derive"] } 31 | which = { version = "7.0.1" } 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 FastLabs Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::process::Command as StdCommand; 16 | 17 | use clap::Parser; 18 | use clap::Subcommand; 19 | 20 | #[derive(Parser)] 21 | struct Command { 22 | #[clap(subcommand)] 23 | sub: SubCommand, 24 | } 25 | 26 | impl Command { 27 | fn run(self) { 28 | match self.sub { 29 | SubCommand::Build(cmd) => cmd.run(), 30 | SubCommand::Lint(cmd) => cmd.run(), 31 | SubCommand::Test(cmd) => cmd.run(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Subcommand)] 37 | enum SubCommand { 38 | #[clap(about = "Compile workspace packages.")] 39 | Build(CommandBuild), 40 | #[clap(about = "Run format and clippy checks.")] 41 | Lint(CommandLint), 42 | #[clap(about = "Run unit tests.")] 43 | Test(CommandTest), 44 | } 45 | 46 | #[derive(Parser)] 47 | struct CommandBuild { 48 | #[arg(long, help = "Assert that `Cargo.lock` will remain unchanged.")] 49 | locked: bool, 50 | } 51 | 52 | impl CommandBuild { 53 | fn run(self) { 54 | run_command(make_build_cmd(self.locked)); 55 | } 56 | } 57 | 58 | #[derive(Parser)] 59 | struct CommandTest { 60 | #[arg(long, help = "Run tests serially and do not capture output.")] 61 | no_capture: bool, 62 | } 63 | 64 | impl CommandTest { 65 | fn run(self) { 66 | run_command(make_test_cmd(self.no_capture, true, &[])); 67 | } 68 | } 69 | 70 | #[derive(Parser)] 71 | #[clap(name = "lint")] 72 | struct CommandLint { 73 | #[arg(long, help = "Automatically apply lint suggestions.")] 74 | fix: bool, 75 | } 76 | 77 | impl CommandLint { 78 | fn run(self) { 79 | run_command(make_clippy_cmd(self.fix)); 80 | run_command(make_format_cmd(self.fix)); 81 | run_command(make_taplo_cmd(self.fix)); 82 | run_command(make_typos_cmd()); 83 | run_command(make_hawkeye_cmd(self.fix)); 84 | } 85 | } 86 | 87 | fn find_command(cmd: &str) -> StdCommand { 88 | match which::which(cmd) { 89 | Ok(exe) => { 90 | let mut cmd = StdCommand::new(exe); 91 | cmd.current_dir(env!("CARGO_WORKSPACE_DIR")); 92 | cmd 93 | } 94 | Err(err) => { 95 | panic!("{cmd} not found: {err}"); 96 | } 97 | } 98 | } 99 | 100 | fn ensure_installed(bin: &str, crate_name: &str) { 101 | if which::which(bin).is_err() { 102 | let mut cmd = find_command("cargo"); 103 | cmd.args(["install", crate_name]); 104 | run_command(cmd); 105 | } 106 | } 107 | 108 | fn run_command(mut cmd: StdCommand) { 109 | println!("{cmd:?}"); 110 | let status = cmd.status().expect("failed to execute process"); 111 | assert!(status.success(), "command failed: {status}"); 112 | } 113 | 114 | fn make_build_cmd(locked: bool) -> StdCommand { 115 | let mut cmd = find_command("cargo"); 116 | cmd.args([ 117 | "build", 118 | "--workspace", 119 | "--all-features", 120 | "--tests", 121 | "--examples", 122 | "--benches", 123 | "--bins", 124 | ]); 125 | if locked { 126 | cmd.arg("--locked"); 127 | } 128 | cmd 129 | } 130 | 131 | fn make_test_cmd(no_capture: bool, default_features: bool, features: &[&str]) -> StdCommand { 132 | let mut cmd = find_command("cargo"); 133 | cmd.args(["test", "--workspace"]); 134 | if !default_features { 135 | cmd.arg("--no-default-features"); 136 | } 137 | if !features.is_empty() { 138 | cmd.args(["--features", features.join(",").as_str()]); 139 | } 140 | if no_capture { 141 | cmd.args(["--", "--nocapture"]); 142 | } 143 | cmd 144 | } 145 | 146 | fn make_format_cmd(fix: bool) -> StdCommand { 147 | let mut cmd = find_command("cargo"); 148 | cmd.args(["fmt", "--all"]); 149 | if !fix { 150 | cmd.arg("--check"); 151 | } 152 | cmd 153 | } 154 | 155 | fn make_clippy_cmd(fix: bool) -> StdCommand { 156 | let mut cmd = find_command("cargo"); 157 | cmd.args([ 158 | "clippy", 159 | "--tests", 160 | "--all-features", 161 | "--all-targets", 162 | "--workspace", 163 | ]); 164 | if fix { 165 | cmd.args(["--allow-staged", "--allow-dirty", "--fix"]); 166 | } else { 167 | cmd.args(["--", "-D", "warnings"]); 168 | } 169 | cmd 170 | } 171 | 172 | fn make_hawkeye_cmd(fix: bool) -> StdCommand { 173 | ensure_installed("hawkeye", "hawkeye"); 174 | let mut cmd = find_command("hawkeye"); 175 | if fix { 176 | cmd.args(["format", "--fail-if-updated=false"]); 177 | } else { 178 | cmd.args(["check"]); 179 | } 180 | cmd 181 | } 182 | 183 | fn make_typos_cmd() -> StdCommand { 184 | ensure_installed("typos", "typos-cli"); 185 | find_command("typos") 186 | } 187 | 188 | fn make_taplo_cmd(fix: bool) -> StdCommand { 189 | ensure_installed("taplo", "taplo-cli"); 190 | let mut cmd = find_command("taplo"); 191 | if fix { 192 | cmd.args(["format"]); 193 | } else { 194 | cmd.args(["format", "--check"]); 195 | } 196 | cmd 197 | } 198 | 199 | fn main() { 200 | let cmd = Command::parse(); 201 | cmd.run() 202 | } 203 | --------------------------------------------------------------------------------