diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fd85871 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish + +# House scheme: a version bump on the default branch is the release. No tags, +# no manual step. Publishing is irreversible, so the trigger has to be +# something you cannot do by accident, and editing the version field is that. +# +# This crate's default branch is `master`, unlike ps-st3's `main`. Match the +# repository rather than copying the sibling, or the workflow silently never +# runs and the release looks like it worked. +on: + push: + branches: [master] + paths: ['Cargo.toml'] + +jobs: + publish: + runs-on: ubicloud-standard-2 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - uses: dtolnay/rust-toolchain@stable + + - id: version + run: | + new=$(sed -nE '0,/^version = "(.*)"/s//\1/p' Cargo.toml) + old=$(git show HEAD^:Cargo.toml | sed -nE '0,/^version = "(.*)"/s//\1/p') + if [ "$new" = "$old" ]; then + echo "version unchanged at $new, nothing to publish" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "version $old -> $new" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + # The gate runs on this commit too, but a release must not depend on job + # ordering, so it is re-run here rather than assumed. + - if: steps.version.outputs.changed == 'true' + run: cargo test --release + + # `no_std` is the reason this crate exists, so a release that only builds + # with an operating system underneath it is not a release. + - if: steps.version.outputs.changed == 'true' + run: cargo check --no-default-features + + - if: steps.version.outputs.changed == 'true' + run: cargo publish --token "$CARGO_REGISTRY_TOKEN" + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.gitignore b/.gitignore index 4fffb2f..1193c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +/target-* /Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index b0c9b80..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,668 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic-wait" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55b94919229f2c42292fd71ffa4b75e83193bffdd77b1e858cd55fd2d0b0ea8" -dependencies = [ - "libc", - "windows-sys 0.42.0", -] - -[[package]] -name = "cc" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "crossbeam-deque" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - -[[package]] -name = "either" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "find-msvc-tools" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" - -[[package]] -name = "forte" -version = "1.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601ae23b96d31d75060b65ea0eeb4d6358655cbdec328641a9416fd357e8fdfc" -dependencies = [ - "async-task", - "atomic-wait", - "crossbeam-queue", - "crossbeam-utils", - "futures-lite", - "lazy_static", - "st3", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "nagoya" -version = "0.1.0" -dependencies = [ - "async-task", - "forte", - "ps-st3", - "rayon", - "serde", - "serde_json", - "spin", - "tokio", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ps-st3" -version = "0.5.0" -dependencies = [ - "atomic-wait", - "crossbeam-queue", - "crossbeam-utils", - "spin", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "smallvec" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - -[[package]] -name = "st3" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a1d2cec4c9904d238075cb3a212615e67aee6acce849e4e565acf2320a7bf1" -dependencies = [ - "crossbeam-utils", - "loom", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index c286190..4fdfdd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,17 +14,11 @@ categories = ["asynchronous", "concurrency", "no-std"] default = ["std"] # Off, this crate does not link `std`. What it costs: `Runtime`, which owns # threads, and the parker `block_on` uses when there is one to park. -std = ["ps-st3/host"] +std = ["ps-st3/host", "dep:futures-io", "futures-io/std", "dep:futures-util", "futures-util/std", "futures-util/io"] -# Pointed at the local checkout while pathscale/ps-st3#4 is open: the published -# 0.5.0 does not have the submit fix, and the numbers in examples/ were taken -# with it. Drop this line once that lands and 0.5.1 is out, or the benchmarks -# quietly measure the wrong thing. -[patch.crates-io] -ps-st3 = { path = "/Users/revenge/code/ps-st3" } [dependencies] -ps-st3 = { version = "0.5.0", default-features = false, features = ["fanout"] } +ps-st3 = { version = "^0.6", default-features = false, features = ["fanout"] } spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"] } # The task itself: one allocation holding the future, its state machine, its # waker vtable and its output, with `Runnable::into_raw` handing the pointer @@ -32,9 +26,14 @@ spin = { version = "0.9", default-features = false, features = ["mutex", "spin_m # design done by people who have been at it for years, it is what `forte` uses, # and with default features off it links no `std` and pulls in nothing. async-task = { version = "4.7", default-features = false } +# The file traits `io` builds on. They belong to no runtime and link no `std` +# with their own feature off, which is why the abstraction can exist here at +# all rather than in whatever crate happens to own the storage engine. +futures-io = { version = "0.3", default-features = false, optional = true } +futures-util = { version = "0.3", default-features = false, optional = true } [dev-dependencies] -ps-st3 = { version = "0.5.0", features = ["fanout", "host"] } +ps-st3 = { version = "^0.6", features = ["fanout", "host"] } # The comparison arm. Only the scheduler is used; nothing here needs its I/O. tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # The other comparison arm. Not an async runtime: a data-parallel pool, so it diff --git a/examples/counter_floor.rs b/examples/counter_floor.rs index 02aba81..26308df 100644 --- a/examples/counter_floor.rs +++ b/examples/counter_floor.rs @@ -8,8 +8,8 @@ //! //! No pool, no tasks: threads and the counter alone. -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::thread; use std::time::Instant; diff --git a/examples/profile_me.rs b/examples/profile_me.rs index bd3073e..1ec8ae6 100644 --- a/examples/profile_me.rs +++ b/examples/profile_me.rs @@ -3,15 +3,18 @@ //! Batched rather than one huge spawn: the spawn loop outruns the workers, so a //! single batch of the size this needs would hold millions of live tasks and //! measure the allocator instead of the scheduler. -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; use st3::fanout::{Pool, StdHost}; fn main() { - let workers: usize = std::env::var("W").ok().and_then(|v| v.parse().ok()).unwrap_or(8); + let workers: usize = std::env::var("W") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(8); let host = Arc::new(StdHost::new(workers)); let pool = Pool::new(workers, 1024, host); let threads: Vec<_> = (0..workers) diff --git a/examples/profile_tokio.rs b/examples/profile_tokio.rs index aec6b6a..832b31f 100644 --- a/examples/profile_tokio.rs +++ b/examples/profile_tokio.rs @@ -1,6 +1,6 @@ //! The same batched first-poll workload on tokio, so the two profiles compare. -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; fn main() { diff --git a/examples/submit_only.rs b/examples/submit_only.rs index 546164d..afaa289 100644 --- a/examples/submit_only.rs +++ b/examples/submit_only.rs @@ -7,8 +7,8 @@ //! //! No workers are started. The pool fills up and nothing runs. -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::time::Instant; use st3::fanout::{Job, Pool, StdHost}; @@ -50,9 +50,12 @@ fn main() { let pool = Pool::new(8, 1024, Arc::new(StdHost::new(8))); for n in 0..TASKS { let counter = counter.clone(); - pool.submit(n % 8, Box::new(move || { - counter.fetch_add(1, Ordering::Relaxed); - })); + pool.submit( + n % 8, + Box::new(move || { + counter.fetch_add(1, Ordering::Relaxed); + }), + ); } }); let by_fn = time(|| { diff --git a/examples/three_pools.rs b/examples/three_pools.rs index 569578c..89888b3 100644 --- a/examples/three_pools.rs +++ b/examples/three_pools.rs @@ -15,8 +15,8 @@ //! CPU is user + system across every thread, so an arm that finishes quickly by //! burning eight cores cannot hide it. -use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::sync::{Condvar, Mutex}; use std::thread; use std::time::Instant; @@ -26,7 +26,10 @@ use st3::fanout::{Pool, StdHost}; const TASKS: usize = 100_000; const REPS: usize = 5; fn workers() -> usize { - std::env::var("W").ok().and_then(|v| v.parse().ok()).unwrap_or(8) + std::env::var("W") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(8) } fn cpu_seconds() -> f64 { diff --git a/examples/vs_tokio.rs b/examples/vs_tokio.rs index 5ee6f0f..bb78c2b 100644 --- a/examples/vs_tokio.rs +++ b/examples/vs_tokio.rs @@ -59,7 +59,12 @@ use std::time::Instant; use st3::fanout::{Pool, StdHost, Tuning}; -fn workers() -> usize { std::env::var("W").ok().and_then(|v| v.parse().ok()).unwrap_or(8) } +fn workers() -> usize { + std::env::var("W") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(8) +} const TASKS: usize = 100_000; const REPS: usize = 5; @@ -85,12 +90,34 @@ impl std::future::Future for Yields { fn nagoya_run(yields: usize) -> f64 { let host = Arc::new(StdHost::new(workers())); - let tuning = Tuning { - rounds_before_park: std::env::var("ROUNDS").ok().and_then(|v| v.parse().ok()).unwrap_or(64), - backoff_spins: std::env::var("BACKOFF").ok().and_then(|v| v.parse().ok()).unwrap_or(1024), - promote_every: std::env::var("PROMOTE").ok().and_then(|v| v.parse().ok()).unwrap_or(64), - injector_batch: std::env::var("BATCH").ok().and_then(|v| v.parse().ok()).unwrap_or(32), - }; + // Built from a preset rather than as a literal: `Tuning` is + // `#[non_exhaustive]` from ps-st3 0.6, so a field it gains later is not a + // breaking change and this example does not have to be edited again. + let tuning = Tuning::locality() + .with_rounds_before_park( + std::env::var("ROUNDS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(64), + ) + .with_backoff_spins( + std::env::var("BACKOFF") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1024), + ) + .with_promote_every( + std::env::var("PROMOTE") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(64), + ) + .with_injector_batch( + std::env::var("BATCH") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(32), + ); let pool = Pool::with_tuning(workers(), 1024, host, tuning); let threads: Vec<_> = (0..workers()) .map(|id| { @@ -190,7 +217,10 @@ fn measure(body: impl FnOnce() -> f64) -> (f64, f64) { } fn main() { - println!("{TASKS} tasks, {} workers, median of {REPS}, arms interleaved\n", workers()); + println!( + "{TASKS} tasks, {} workers, median of {REPS}, arms interleaved\n", + workers() + ); for (name, yields) in [("complete on first poll", 0), ("yield 4 times first", 4)] { println!("{name}:"); diff --git a/examples/wake_latency.rs b/examples/wake_latency.rs index 471e14a..33d57e9 100644 --- a/examples/wake_latency.rs +++ b/examples/wake_latency.rs @@ -13,8 +13,8 @@ //! //! One task at a time, submitted to an idle pool, timed to completion. -use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; use std::sync::{Condvar, Mutex}; use std::thread; use std::time::Instant; @@ -24,16 +24,31 @@ use st3::fanout::{Pool, StdHost, Tuning}; const SAMPLES: usize = 2_000; fn tuning() -> Tuning { - Tuning { - rounds_before_park: std::env::var("ROUNDS").ok().and_then(|v| v.parse().ok()).unwrap_or(64), - backoff_spins: std::env::var("BACKOFF").ok().and_then(|v| v.parse().ok()).unwrap_or(1024), - promote_every: 64, - injector_batch: 32, - } + // Built from a preset rather than as a literal: `Tuning` is + // `#[non_exhaustive]` from ps-st3 0.6, so a field it gains later is not a + // breaking change and this example does not have to be edited again. + Tuning::locality() + .with_rounds_before_park( + std::env::var("ROUNDS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(64), + ) + .with_backoff_spins( + std::env::var("BACKOFF") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1024), + ) + .with_promote_every(64) + .with_injector_batch(32) } fn workers() -> usize { - std::env::var("W").ok().and_then(|v| v.parse().ok()).unwrap_or(8) + std::env::var("W") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(8) } struct Done { diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..4e17cde --- /dev/null +++ b/src/io.rs @@ -0,0 +1,717 @@ +//! Files, for consumers that need one and do not want a runtime attached. +//! +//! # Why this is here and not a reactor +//! +//! The crate documentation says there will be no I/O driver, and that still +//! holds: an epoll or io_uring reactor is a large amount of platform, and not +//! linking one is the point of this crate. +//! +//! **A file abstraction is not a reactor.** It is a set of traits plus one +//! implementation over `std::fs`, exactly the shape [`Host`] already has for +//! parking: a consumer that does not want the `std` one supplies its own. +//! +//! [`Host`]: st3::fanout::Host +//! +//! # Why these traits and not `futures-io` +//! +//! Because `futures-io` cannot be used without `std`, and it is easy to +//! conclude otherwise. **Every one of its traits sits behind its own `std` +//! feature.** Turn that off and the crate compiles to nothing at all: no +//! `AsyncRead`, no `AsyncWrite`, no `AsyncSeek`. They take `std::io::Error` and +//! `IoSlice`, so there was nowhere else for them to go. +//! +//! That is worth stating plainly because checking it the obvious way gives the +//! wrong answer: the crate still *compiles* with the feature off, so a probe +//! that only builds it reports success, and it is the exports that vanish. +//! Anything written on top of those three is a `std` trait wearing a portable +//! name. +//! +//! So the traits below are the same shape with two differences that matter: the +//! error is [`Error`] rather than `std::io::Error`, and the seek origin is +//! [`SeekFrom`] rather than `std::io::SeekFrom`. With `std` on, the two +//! interoperate: [`Compat`] wraps any `futures-io` type, and [`HostFile`] is a +//! plain `std::fs::File` that already implements all of it. +//! +//! # Why the calls block +//! +//! They block the caller, because that is what the platform offers. Neither +//! `tokio::fs` nor `async-fs` performs asynchronous file I/O either: both hand +//! a blocking `std::fs` call to a thread pool, and what that buys is not +//! occupying a runtime worker rather than any actual overlap. It is not free. +//! Measured on WorkTable's scattered-update path, `tokio::fs` ran 12,316 rows +//! per second against 74,728 for the same code on blocking `std::fs`. +//! +//! So the right place for this work is a thread that owns it, not a worker +//! shared with everything else. This module gives you the file; where you run +//! it is yours to decide. + +use core::future::Future; + +/// Where a seek counts from. +/// +/// `std::io::SeekFrom` in all but name, restated because that one is in `std` +/// and this is not. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SeekFrom { + /// Bytes from the beginning. + Start(u64), + /// Bytes from the end, where negative moves backwards into the file. + End(i64), + /// Bytes from the current position, where negative moves backwards. + Current(i64), +} + +/// Reading bytes from something. +pub trait Read { + /// Read into `buffer`, returning how many bytes arrived. + /// + /// A return of `Ok(0)` means the end, and is not an error. + fn read(&mut self, buffer: &mut [u8]) -> impl Future> + Send; + + /// Fill `buffer` completely, or fail. + /// + /// A short read is not an end condition here: the caller asked for a fixed + /// number of bytes, so running out is [`ErrorKind::UnexpectedEof`]. + fn read_exact(&mut self, buffer: &mut [u8]) -> impl Future> + Send + where + Self: Send, + { + async move { + let mut filled = 0; + while filled < buffer.len() { + match self.read(&mut buffer[filled..]).await? { + 0 => return Err(Error::new(ErrorKind::UnexpectedEof)), + n => filled += n, + } + } + Ok(()) + } + } + + /// Append everything left to `buffer`, returning how many bytes were added. + /// + /// Grows in chunks rather than one byte at a time, and only keeps what was + /// actually read, so a source that ends early does not leave the caller + /// holding a tail of zeros it cannot distinguish from data. + fn read_to_end( + &mut self, + buffer: &mut alloc::vec::Vec, + ) -> impl Future> + Send + where + Self: Send, + { + /// Big enough that a whole page arrives in one or two reads, small + /// enough not to over-allocate for a short file. + const CHUNK: usize = 8192; + async move { + let start = buffer.len(); + loop { + let filled = buffer.len(); + buffer.resize(filled + CHUNK, 0); + match self.read(&mut buffer[filled..]).await { + Ok(0) => { + buffer.truncate(filled); + return Ok(filled - start); + } + Ok(n) => buffer.truncate(filled + n), + Err(error) => { + buffer.truncate(filled); + return Err(error); + } + } + } + } + } +} + +/// Writing bytes to something. +pub trait Write { + /// Write some of `buffer`, returning how many bytes were taken. + fn write(&mut self, buffer: &[u8]) -> impl Future> + Send; + + /// Push whatever is buffered towards its destination. + /// + /// This is not durability. A write that has been flushed has left this + /// process; it has not necessarily reached the storage device. For that, + /// see [`File::sync_all`]. + fn flush(&mut self) -> impl Future> + Send; + + /// Write all of `buffer`, or fail. + fn write_all(&mut self, buffer: &[u8]) -> impl Future> + Send + where + Self: Send, + { + async move { + let mut written = 0; + while written < buffer.len() { + match self.write(&buffer[written..]).await? { + // Nothing taken and no error: the destination will not + // accept more, and looping would spin forever. + 0 => return Err(Error::new(ErrorKind::WriteZero)), + n => written += n, + } + } + Ok(()) + } + } +} + +/// Moving the cursor of something that has one. +pub trait Seek { + /// Move the cursor, returning where it landed, counted from the start. + fn seek(&mut self, from: SeekFrom) -> impl Future> + Send; + + /// Where the cursor is, counted from the start. + fn stream_position(&mut self) -> impl Future> + Send + where + Self: Send, + { + async move { self.seek(SeekFrom::Current(0)).await } + } +} + +/// A file: readable, writable, seekable, and able to answer the handful of +/// questions no I/O trait covers. +/// +/// The four methods below are the reason this trait exists. Every storage crate +/// needs them and none of `futures-io`, `tokio::io` or `std::io` puts them in a +/// trait, so each one reinvents them privately and no two agree. +pub trait File: Read + Write + Seek { + /// Bytes currently in the file. + fn length(&mut self) -> impl Future> + Send; + + /// Truncate or extend to `length`, zero-filling any new bytes. + fn set_length(&mut self, length: u64) -> impl Future> + Send; + + /// Flush this file's contents *and* its metadata to the storage device. + /// + /// Unlike [`Write::flush`], this is the durability boundary. + fn sync_all(&mut self) -> impl Future> + Send; + + /// Flush this file's contents, leaving metadata to the platform. + /// + /// Cheaper than [`File::sync_all`] where the platform distinguishes them, + /// and identical to it where it does not. + fn sync_data(&mut self) -> impl Future> + Send; +} + +/// What went wrong. +/// +/// Deliberately not `std::io::Error`: these traits have to be implementable on +/// a target that has no `std`. With `std` on, the two convert. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Error { + kind: ErrorKind, +} + +impl Error { + /// An error of this kind. + #[must_use] + pub fn new(kind: ErrorKind) -> Self { + Self { kind } + } + + /// What kind of failure this was. + #[must_use] + pub fn kind(&self) -> ErrorKind { + self.kind + } +} + +/// The failures a file operation can report. +/// +/// Coarse on purpose. A caller either retries, gives up, or creates what was +/// missing, and a longer list would not change which of those it picks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ErrorKind { + /// The path names nothing. + NotFound, + /// The path names something that cannot be opened this way. + PermissionDenied, + /// The read wanted more bytes than the file had left. + UnexpectedEof, + /// The destination accepted no bytes and reported no error. + WriteZero, + /// The operation was cut short and can be retried as-is. + Interrupted, + /// Anything else the platform reported. + Other, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let text = match self.kind { + ErrorKind::NotFound => "no such file", + ErrorKind::PermissionDenied => "permission denied", + ErrorKind::UnexpectedEof => "unexpected end of file", + ErrorKind::WriteZero => "the destination accepted no bytes", + ErrorKind::Interrupted => "interrupted", + ErrorKind::Other => "file operation failed", + }; + f.write_str(text) + } +} + +// `core::error::Error`, not `std::error::Error`, and not behind the `std` +// feature. A consumer building without `std` still wants `?` to work against +// its own error type, and gating this would have made the trait unusable in +// exactly the place it exists for. +impl core::error::Error for Error {} + +#[cfg(feature = "std")] +impl From for Error { + fn from(error: std::io::Error) -> Self { + Self::new(match error.kind() { + std::io::ErrorKind::NotFound => ErrorKind::NotFound, + std::io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied, + std::io::ErrorKind::UnexpectedEof => ErrorKind::UnexpectedEof, + std::io::ErrorKind::WriteZero => ErrorKind::WriteZero, + std::io::ErrorKind::Interrupted => ErrorKind::Interrupted, + _ => ErrorKind::Other, + }) + } +} + +#[cfg(feature = "std")] +impl From for std::io::Error { + fn from(error: Error) -> Self { + std::io::Error::new( + match error.kind { + ErrorKind::NotFound => std::io::ErrorKind::NotFound, + ErrorKind::PermissionDenied => std::io::ErrorKind::PermissionDenied, + ErrorKind::UnexpectedEof => std::io::ErrorKind::UnexpectedEof, + ErrorKind::WriteZero => std::io::ErrorKind::WriteZero, + ErrorKind::Interrupted => std::io::ErrorKind::Interrupted, + ErrorKind::Other => std::io::ErrorKind::Other, + }, + error, + ) + } +} + +#[cfg(feature = "std")] +impl From for std::io::SeekFrom { + fn from(from: SeekFrom) -> Self { + match from { + SeekFrom::Start(n) => std::io::SeekFrom::Start(n), + SeekFrom::End(n) => std::io::SeekFrom::End(n), + SeekFrom::Current(n) => std::io::SeekFrom::Current(n), + } + } +} + +#[cfg(feature = "std")] +pub use host::{ + append, create, create_dir_all, metadata, open, open_or_create, read, remove_dir_all, + remove_file, rename, write, Compat, HostFile, +}; + +#[cfg(feature = "std")] +mod host { + //! The implementation for a target that has a filesystem. + //! + //! Blocking `std::fs`, reached through `async fn`s that do not await + //! anything. That is not a pretence of asynchrony: it is the honest shape, + //! because the platform has no asynchronous file I/O to offer and the two + //! crates that claim otherwise are thread pools in a trench coat. + + use super::{Error, File, Read, Seek, SeekFrom, Write}; + use std::path::Path; + + /// A [`File`] backed by `std::fs`. + #[derive(Debug)] + pub struct HostFile { + inner: std::fs::File, + } + + impl HostFile { + /// Take an already-open file. + #[must_use] + pub fn new(inner: std::fs::File) -> Self { + Self { inner } + } + + /// The file underneath, for the platform-specific things this trait + /// deliberately does not cover. + #[must_use] + pub fn get_ref(&self) -> &std::fs::File { + &self.inner + } + } + + // `async fn` rather than a computed `Result` handed to an `async move` + // block: the second form runs the syscall when the method is called, so a + // future that is built and dropped unpolled would still have written. A + // future does nothing until it is polled, blocking body or not. + impl Read for HostFile { + async fn read(&mut self, buffer: &mut [u8]) -> Result { + std::io::Read::read(&mut self.inner, buffer).map_err(Error::from) + } + } + + impl Write for HostFile { + async fn write(&mut self, buffer: &[u8]) -> Result { + std::io::Write::write(&mut self.inner, buffer).map_err(Error::from) + } + + async fn flush(&mut self) -> Result<(), Error> { + std::io::Write::flush(&mut self.inner).map_err(Error::from) + } + } + + impl Seek for HostFile { + async fn seek(&mut self, from: SeekFrom) -> Result { + std::io::Seek::seek(&mut self.inner, from.into()).map_err(Error::from) + } + } + + impl File for HostFile { + async fn length(&mut self) -> Result { + self.inner.metadata().map(|m| m.len()).map_err(Error::from) + } + + async fn set_length(&mut self, length: u64) -> Result<(), Error> { + self.inner.set_len(length).map_err(Error::from) + } + + async fn sync_all(&mut self) -> Result<(), Error> { + self.inner.sync_all().map_err(Error::from) + } + + async fn sync_data(&mut self) -> Result<(), Error> { + self.inner.sync_data().map_err(Error::from) + } + } + + /// Wraps a `futures-io` type so it satisfies [`Read`], [`Write`] and + /// [`Seek`]. + /// + /// For consumers that already hold something from that ecosystem. It cannot + /// give you a [`File`]: the four methods there have no `futures-io` + /// equivalent, which is the whole reason [`File`] exists. + #[derive(Debug)] + pub struct Compat(pub T); + + impl Read for Compat + where + T: futures_io::AsyncRead + Unpin + Send, + { + async fn read(&mut self, buffer: &mut [u8]) -> Result { + futures_util::AsyncReadExt::read(&mut self.0, buffer) + .await + .map_err(Error::from) + } + } + + impl Write for Compat + where + T: futures_io::AsyncWrite + Unpin + Send, + { + async fn write(&mut self, buffer: &[u8]) -> Result { + futures_util::AsyncWriteExt::write(&mut self.0, buffer) + .await + .map_err(Error::from) + } + + async fn flush(&mut self) -> Result<(), Error> { + futures_util::AsyncWriteExt::flush(&mut self.0) + .await + .map_err(Error::from) + } + } + + impl Seek for Compat + where + T: futures_io::AsyncSeek + Unpin + Send, + { + async fn seek(&mut self, from: SeekFrom) -> Result { + futures_util::AsyncSeekExt::seek(&mut self.0, from.into()) + .await + .map_err(Error::from) + } + } + + /// Open an existing file for reading and writing. + pub async fn open(path: impl AsRef) -> Result { + Ok(HostFile::new( + std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(path)?, + )) + } + + /// Create a file for reading and writing, truncating one already there. + pub async fn create(path: impl AsRef) -> Result { + Ok(HostFile::new( + std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(path)?, + )) + } + + /// Open for reading and writing, creating it if absent and keeping what is + /// there if not. + pub async fn open_or_create(path: impl AsRef) -> Result { + Ok(HostFile::new( + std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path)?, + )) + } + + /// Open for appending, creating it if absent. + pub async fn append(path: impl AsRef) -> Result { + Ok(HostFile::new( + std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path)?, + )) + } + + /// Read a whole file. + pub async fn read(path: impl AsRef) -> Result, Error> { + Ok(std::fs::read(path)?) + } + + /// Replace a file's contents. + pub async fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<(), Error> { + Ok(std::fs::write(path, contents)?) + } + + /// How long the file at `path` is, without opening it. + pub async fn metadata(path: impl AsRef) -> Result { + Ok(std::fs::metadata(path)?.len()) + } + + /// Remove a file. + pub async fn remove_file(path: impl AsRef) -> Result<(), Error> { + Ok(std::fs::remove_file(path)?) + } + + /// Remove a directory and everything under it. + pub async fn remove_dir_all(path: impl AsRef) -> Result<(), Error> { + Ok(std::fs::remove_dir_all(path)?) + } + + /// Rename a file, replacing the destination if it exists. + pub async fn rename(from: impl AsRef, to: impl AsRef) -> Result<(), Error> { + Ok(std::fs::rename(from, to)?) + } + + /// Create a directory and every missing parent. + pub async fn create_dir_all(path: impl AsRef) -> Result<(), Error> { + Ok(std::fs::create_dir_all(path)?) + } +} + +/// A [`File`] over a fixed byte array, implemented the way a consumer that does +/// not want the `std` one would implement it. +/// +/// **This is a guard, not a utility.** `futures-io` still compiles with its +/// `std` feature off while exporting no traits at all, so "the crate builds" is +/// not evidence that anything downstream can be written. This module touches no +/// `std` and implements every method of every trait above, including the +/// provided ones, so a change that quietly made them unimplementable without +/// `std` would stop compiling here. +#[cfg(test)] +mod portable { + use super::{Error, ErrorKind, File, Read, Seek, SeekFrom, Write}; + use core::future::Future; + + struct Ram { + bytes: [u8; 256], + length: u64, + at: u64, + } + + impl Read for Ram { + fn read(&mut self, buffer: &mut [u8]) -> impl Future> + Send { + let start = self.at as usize; + let end = (start + buffer.len()).min(self.length as usize); + let taken = end.saturating_sub(start); + buffer[..taken].copy_from_slice(&self.bytes[start..end]); + self.at += taken as u64; + async move { Ok(taken) } + } + } + + impl Write for Ram { + fn write(&mut self, buffer: &[u8]) -> impl Future> + Send { + let start = self.at as usize; + let result = if start >= self.bytes.len() { + Err(Error::new(ErrorKind::WriteZero)) + } else { + let end = (start + buffer.len()).min(self.bytes.len()); + let taken = end - start; + self.bytes[start..end].copy_from_slice(&buffer[..taken]); + self.at += taken as u64; + self.length = self.length.max(self.at); + Ok(taken) + }; + async move { result } + } + + async fn flush(&mut self) -> Result<(), Error> { + Ok(()) + } + } + + impl Seek for Ram { + async fn seek(&mut self, from: SeekFrom) -> Result { + self.at = match from { + SeekFrom::Start(n) => n, + SeekFrom::End(n) => self.length.saturating_add_signed(n), + SeekFrom::Current(n) => self.at.saturating_add_signed(n), + }; + Ok(self.at) + } + } + + impl File for Ram { + async fn length(&mut self) -> Result { + Ok(self.length) + } + + async fn set_length(&mut self, length: u64) -> Result<(), Error> { + self.length = length; + Ok(()) + } + + async fn sync_all(&mut self) -> Result<(), Error> { + Ok(()) + } + + async fn sync_data(&mut self) -> Result<(), Error> { + Ok(()) + } + } + + #[test] + fn a_consumer_with_no_filesystem_can_implement_the_whole_trait() { + let out = crate::block_on(async { + let mut device = Ram { + bytes: [0; 256], + length: 0, + at: 0, + }; + device.write_all(b"disk").await?; + device.seek(SeekFrom::Start(0)).await?; + let mut back = [0u8; 4]; + device.read_exact(&mut back).await?; + assert_eq!(device.stream_position().await?, 4); + assert_eq!(device.length().await?, 4); + device.sync_all().await?; + Ok::<_, Error>(back) + }) + .unwrap(); + assert_eq!(&out, b"disk"); + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::{ + create, metadata, open, remove_file, ErrorKind, File, Read, Seek, SeekFrom, Write, + }; + use crate::block_on; + + fn scratch(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("nagoya-io-{name}-{}", std::process::id())) + } + + #[test] + fn a_file_round_trips_through_the_traits() { + let path = scratch("round-trip"); + block_on(async { + let mut file = create(&path).await.unwrap(); + file.write_all(b"the bytes").await.unwrap(); + file.seek(SeekFrom::Start(0)).await.unwrap(); + let mut back = [0u8; 9]; + file.read_exact(&mut back).await.unwrap(); + assert_eq!(&back, b"the bytes"); + }); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn the_four_methods_no_io_trait_carries() { + let path = scratch("methods"); + block_on(async { + let mut file = create(&path).await.unwrap(); + file.write_all(b"0123456789").await.unwrap(); + file.sync_all().await.unwrap(); + assert_eq!(file.length().await.unwrap(), 10); + + file.set_length(4).await.unwrap(); + assert_eq!(file.length().await.unwrap(), 4); + assert_eq!(metadata(&path).await.unwrap(), 4); + + file.sync_data().await.unwrap(); + }); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn reading_past_the_end_is_not_silently_short() { + let path = scratch("short"); + block_on(async { + let mut file = create(&path).await.unwrap(); + file.write_all(b"four").await.unwrap(); + file.seek(SeekFrom::Start(0)).await.unwrap(); + + // `read` reports the end as zero bytes and no error. + let mut plenty = [0u8; 64]; + assert_eq!(file.read(&mut plenty).await.unwrap(), 4); + assert_eq!(file.read(&mut plenty).await.unwrap(), 0); + + // `read_exact` asked for a fixed count, so running out is an error. + file.seek(SeekFrom::Start(0)).await.unwrap(); + let mut exact = [0u8; 64]; + let error = file.read_exact(&mut exact).await.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::UnexpectedEof); + }); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn seeking_reports_where_it_landed() { + let path = scratch("seek"); + block_on(async { + let mut file = create(&path).await.unwrap(); + file.write_all(&[0u8; 100]).await.unwrap(); + + assert_eq!(file.seek(SeekFrom::Start(10)).await.unwrap(), 10); + assert_eq!(file.seek(SeekFrom::Current(5)).await.unwrap(), 15); + assert_eq!(file.seek(SeekFrom::Current(-5)).await.unwrap(), 10); + assert_eq!(file.seek(SeekFrom::End(-1)).await.unwrap(), 99); + assert_eq!(file.stream_position().await.unwrap(), 99); + }); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn opening_something_that_is_not_there_says_so() { + let error = block_on(open(scratch("absent"))).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::NotFound); + } + + #[test] + fn removing_a_file_removes_it() { + let path = scratch("removed"); + block_on(async { + create(&path).await.unwrap(); + assert!(path.exists()); + remove_file(&path).await.unwrap(); + }); + assert!(!path.exists()); + } +} diff --git a/src/lib.rs b/src/lib.rs index fe96abd..ae671e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,12 +46,25 @@ use core::task::{Context, Poll}; use st3::fanout::Pool; mod block_on; +pub mod io; mod par; -mod yield_now; +// Owns threads, so it needs `std`. See its module comment for why a crate built +// not to own threads carries one that does. +#[cfg(feature = "std")] +pub mod runtime; +// The four async primitives a consumer was otherwise taking from `tokio::sync`. +// They are runtime-agnostic there too, which is exactly why the dependency was +// easy to acquire and hard to notice: see the module comment. +pub mod sync; mod task; +mod time; +mod yield_now; pub use block_on::block_on; pub use par::{par_for, Cancel, ParFor}; +pub use time::{ + now_ns, poll as poll_timers, set_clock, sleep, sleep_until, timeout, Elapsed, Sleep, Timeout, +}; pub use yield_now::{yield_now, YieldNow}; /// A handle to a spawned task's output. @@ -72,7 +85,9 @@ pub struct JoinHandle { impl JoinHandle { fn task(&mut self) -> &mut async_task::Task { - self.task.as_mut().expect("the task is taken only by `Drop`") + self.task + .as_mut() + .expect("the task is taken only by `Drop`") } /// Stop the task at its next suspension point and throw away its output. @@ -95,6 +110,24 @@ impl JoinHandle { } } +/// Deliberately not derived: a derive would demand `T: Debug` from every +/// consumer that puts a handle in a `#[derive(Debug)]` struct, and a handle's +/// output type is exactly the thing they cannot see yet. +impl core::fmt::Debug for JoinHandle { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("JoinHandle") + .field( + "finished", + &self + .task + .as_ref() + .is_some_and(async_task::Task::is_finished), + ) + .finish() + } +} + impl Drop for JoinHandle { fn drop(&mut self) { if let Some(task) = self.task.take() { @@ -126,9 +159,7 @@ impl Executor { /// which is what keeps this free of an operating system. #[must_use] pub fn new(pool: Arc) -> Self { - Self { - pool, - } + Self { pool } } /// Another handle to the same pool, for a task that spawns. diff --git a/src/par.rs b/src/par.rs index 8a86519..90d11b2 100644 --- a/src/par.rs +++ b/src/par.rs @@ -175,8 +175,8 @@ where let mut range = range; // Split iteratively rather than recursively, giving the right half to // the pool and keeping the left. Recursion here would put the whole - // depth on a worker's stack, and a bare-metal target may not have one - // to spare. + // split depth on one worker's stack, which is a stack overflow waiting + // for a large enough range. while range.end - range.start > leaf { let middle = range.start + (range.end - range.start) / 2; let right = middle..range.end; @@ -215,7 +215,6 @@ where fn park(&self, waker: &Waker) { *self.waiter.lock() = Some(waker.clone()); } - } /// Everything a loop needs to start: the shared half, the pool to put pieces diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000..35d2194 --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,166 @@ +//! A pool that owns its threads, for callers that have nowhere else to put them. +//! +//! # Why this exists in a crate whose point is not owning threads +//! +//! [`Executor`](crate::Executor) deliberately takes a pool whose threads the +//! caller provides. That is the property that lets this crate run without an +//! operating system, and it is not negotiable. +//! +//! But it pushes a real problem onto some callers. A storage engine has +//! background work that is *intrinsic to it* rather than to its user: a vacuum +//! sweep and a persistence writer are the engine's business, they must keep +//! running, and no caller should have to know they exist in order to hand over +//! a thread for them. Faced with that, the engine reached for `tokio::spawn`, +//! which works because tokio has an implicit global runtime to find. That one +//! call put the whole of `tokio` into a `no_std` dependency graph. +//! +//! So: this is the explicit version of the thing that was being got implicitly. +//! It owns threads, it needs `std`, and it says so in its name and its feature +//! gate, rather than arriving as a side effect of a `spawn`. +//! +//! **Off without `std`, and that is the honest outcome.** A build with no +//! threads has no background worker, and an engine that needs one has to say +//! what it does instead rather than pretend the problem is absent. + +use alloc::sync::Arc; +use core::future::Future; + +use st3::fanout::{Pool, StdHost}; + +use crate::{Executor, JoinHandle}; + +/// A work-stealing pool that started its own threads. +/// +/// Dropping this does **not** stop the threads: they are detached, because the +/// tasks on them are the ones nobody else is watching, and tearing them down +/// under a running sweep is worse than letting them run. A caller that wants +/// them stopped stops the work, not the runtime. +pub struct Runtime { + executor: Executor, +} + +impl Runtime { + /// A runtime with `workers` threads. + /// + /// # Panics + /// + /// If a thread cannot be started. There is no useful way to continue: the + /// caller asked for background execution and the platform refused it, and + /// returning a runtime that silently runs nothing would be worse. + #[must_use] + pub fn new(workers: usize) -> Self { + let workers = workers.max(1); + let host = Arc::new(StdHost::new(workers)); + let pool = Pool::new(workers, 1024, host); + for id in 0..workers { + let pool = pool.clone(); + let runner = pool.runner(id); + std::thread::Builder::new() + .name(alloc::format!("nagoya-{id}")) + .spawn(move || { + // Tells the scheduler that a wake happening on this thread + // belongs to worker `id`, so it can skip the injector. See + // `task::mark_current`. + let _current = crate::task::mark_current(&pool, id); + let _ = pool.run(runner); + }) + .expect("a runtime thread"); + } + Self { + executor: Executor::new(pool), + } + } + + /// Run a future on this runtime's threads. + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.executor.spawn(future) + } + + /// The executor underneath, for a caller that wants to hand it on. + #[must_use] + pub fn executor(&self) -> &Executor { + &self.executor + } +} + +/// Threads for the shared pool. +/// +/// The machine's parallelism, which is also what `tokio::spawn` gave the +/// callers this replaces. Keeping the number the same is the point: changing +/// the thread count and the runtime in one step makes any measurement of the +/// swap unreadable. +fn shared_threads() -> usize { + std::thread::available_parallelism().map_or(2, core::num::NonZeroUsize::get) +} + +/// One pool per process, started on first use. +/// +/// # Why this exists, given that an ambient runtime is what went wrong +/// +/// This is the same shape as `tokio`'s implicit global, and that global is +/// precisely how a whole runtime got into a `no_std` dependency graph without +/// anyone writing it down. So it is worth being explicit about why it is back. +/// +/// The alternative is each crate growing its own `OnceLock`. Two such +/// crates in one process start two pools and the process pays for both, with +/// neither able to steal the other's idle threads. A storage engine and +/// whatever else the binary links are not coordinating on this, and cannot. +/// One pool is the correct answer to that. +/// +/// What makes it different from the thing it resembles: it is behind `std`, so +/// a `--no-default-features` build cannot reach it and cannot silently acquire +/// threads. That gate is exactly the one `tokio::spawn` did not have. +/// +/// A caller that wants its own threads, its own count, or a pool it can stop +/// still builds a [`Runtime`] directly. This is the convenience, not the API. +pub fn background() -> &'static Runtime { + static SHARED: std::sync::OnceLock = std::sync::OnceLock::new(); + SHARED.get_or_init(|| Runtime::new(shared_threads())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + use core::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn the_shared_pool_is_one_pool() { + assert!(core::ptr::eq(background(), background())); + let counter = Arc::new(AtomicUsize::new(0)); + let handles: alloc::vec::Vec<_> = (0..32) + .map(|_| { + let counter = counter.clone(); + background().spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + }) + }) + .collect(); + for handle in handles { + block_on(handle); + } + assert_eq!(counter.load(Ordering::Relaxed), 32); + } + + #[test] + fn a_runtime_runs_what_it_is_given() { + let runtime = Runtime::new(2); + let counter = Arc::new(AtomicUsize::new(0)); + let handles: alloc::vec::Vec<_> = (0..64) + .map(|_| { + let counter = counter.clone(); + runtime.spawn(async move { + counter.fetch_add(1, Ordering::Relaxed); + }) + }) + .collect(); + for handle in handles { + block_on(handle); + } + assert_eq!(counter.load(Ordering::Relaxed), 64); + } +} diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..92f5ece --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,881 @@ +//! Async synchronisation, for a crate that does not want a runtime. +//! +//! # Why this is here and not taken from tokio +//! +//! `tokio::sync` is waker-based and runtime-agnostic: its `RwLock`, `Semaphore` +//! and `Notify` genuinely work on any executor, which is why a storage engine +//! could sit on nagoya's file layer and still lock with tokio's primitives, and +//! pass its tests doing so. +//! +//! It is still the wrong dependency. Taking those types means taking `tokio`, +//! and `tokio` is `std` and unconditional: `cargo tree --no-default-features +//! -e normal -i tokio` on WorkTable showed it linked even with every feature +//! off, because the uses were never gated. A crate whose point is to have no +//! runtime cannot reach into one for a lock. +//! +//! So these are the four types that were actually used, and nothing else. Not a +//! general-purpose library: `RwLock`, `Semaphore`, `Notify`, and the owned +//! guards, because those are what a table needed. +//! +//! # What these are not +//! +//! **Not fair.** A waiter is woken and then races for the lock with anyone +//! arriving at that moment, so a writer can be overtaken. `tokio::sync::RwLock` +//! is write-preferring and does not do this. Nothing here depends on fairness +//! today, and saying so is cheaper than implementing it; if a caller starts to +//! depend on it, that is the moment to write the queue properly rather than to +//! discover it in production. +//! +//! **Not `no_std`-free of allocation.** The waiter lists are `VecDeque`, +//! so this needs `alloc`, which the rest of the crate needs anyway. + +use alloc::collections::VecDeque; +use alloc::sync::Arc; +use core::cell::UnsafeCell; +use core::future::Future; +use core::ops::{Deref, DerefMut}; +use core::pin::Pin; +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use core::task::{Context, Poll, Waker}; + +use spin::Mutex; + +/// The waiter list every primitive here shares. +/// +/// A spin lock rather than a blocking one because it is held for the length of +/// a push or a pop and nothing else, and because a blocking mutex would need +/// the very thing this module exists to avoid. +#[derive(Default)] +struct Waiters(Mutex>); + +impl Waiters { + const fn new() -> Self { + Self(Mutex::new(VecDeque::new())) + } + + fn push(&self, waker: &Waker) { + let mut queue = self.0.lock(); + // Replacing an equivalent waker rather than pushing a second one: a + // future polled twice before it completes would otherwise leave a stale + // entry that wakes a task which has already moved on. + if queue.iter().any(|w| w.will_wake(waker)) { + return; + } + queue.push_back(waker.clone()); + } + + fn wake_one(&self) { + let waker = self.0.lock().pop_front(); + if let Some(waker) = waker { + waker.wake(); + } + } + + fn wake_all(&self) { + // Drained under the lock, woken outside it: `wake` runs arbitrary + // scheduler code, and holding a spin lock across that invites the + // woken task to spin on the lock its waker still holds. + let drained: VecDeque = core::mem::take(&mut *self.0.lock()); + for waker in drained { + waker.wake(); + } + } +} + +/// A one-shot wake-up that remembers a notification with no waiter. +/// +/// The permit is the whole point: a producer that signals before the consumer +/// has parked must not lose the signal, or the consumer sleeps forever holding +/// work that was already announced. +pub struct Notify { + permit: AtomicBool, + /// Bumped by [`Notify::notify_waiters`], which leaves no permit. + /// + /// A `Notified` snapshots this when it is created, so a broadcast landing + /// between creation and the first poll is still observed. That is the race + /// `tokio`'s `Notified::enable` exists for, and taking it at construction + /// costs one relaxed load instead of an intrusive list. + generation: AtomicUsize, + waiters: Waiters, +} + +impl core::fmt::Debug for Notify { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Notify") + .field("permit", &self.permit.load(Ordering::Relaxed)) + .finish_non_exhaustive() + } +} + +impl Default for Notify { + fn default() -> Self { + Self::new() + } +} + +impl Notify { + /// A `Notify` holding no permit and no waiters. + #[must_use] + pub const fn new() -> Self { + Self { + permit: AtomicBool::new(false), + generation: AtomicUsize::new(0), + waiters: Waiters::new(), + } + } + + /// Wake one waiter, or leave a permit for the next one to arrive. + pub fn notify_one(&self) { + self.permit.store(true, Ordering::Release); + self.waiters.wake_one(); + } + + /// Wake every current waiter. Leaves no permit. + pub fn notify_waiters(&self) { + self.generation.fetch_add(1, Ordering::AcqRel); + self.waiters.wake_all(); + } + + /// Wait for a notification, taking a stored permit if one is there. + pub fn notified(&self) -> Notified<'_> { + Notified { + notify: self, + generation: self.generation.load(Ordering::Acquire), + } + } +} + +/// The future returned by [`Notify::notified`]. +pub struct Notified<'a> { + notify: &'a Notify, + /// The broadcast generation when this was created. + generation: usize, +} + +impl Notified<'_> { + /// Whether a notification is already waiting for this future. + /// + /// **This is not `tokio`'s `enable` and does not need to be.** There, the + /// future must be linked into the waiter list before the caller reads any + /// state, or a notification in between is lost. Here the two ways of + /// notifying are both already durable across that window: `notify_one` + /// leaves a permit that outlives it, and `notify_waiters` bumps a + /// generation this future snapshotted when it was created. So the race the + /// call guards against cannot happen, and the method exists to keep the + /// registration point visible at the call site, which is worth having. + pub fn enable(self: Pin<&mut Self>) -> bool { + self.notify.permit.load(Ordering::Acquire) + || self.notify.generation.load(Ordering::Acquire) != self.generation + } +} + +impl Future for Notified<'_> { + type Output = (); + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> { + if self.is_notified() { + return Poll::Ready(()); + } + // Registered before the second look, so a notification landing between + // the two still finds a waker to call. + self.notify.waiters.push(context.waker()); + if self.is_notified() { + return Poll::Ready(()); + } + Poll::Pending + } +} + +impl Notified<'_> { + /// Take a permit, or observe that a broadcast has happened since creation. + fn is_notified(&self) -> bool { + if self + .notify + .permit + .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + return true; + } + self.notify.generation.load(Ordering::Acquire) != self.generation + } +} + +/// A rendezvous for a fixed number of tasks. +/// +/// Every waiter blocks until `n` of them have arrived, then all are released +/// together. Reusable: the next `n` arrivals rendezvous again. +/// +/// The benchmark case is what this is for. A harness that starts `n` client +/// tasks and times them has to release them at one instant, or the first +/// task's warm-up is measured against the last task's steady state and the +/// number means nothing. A barrier is how that instant is defined. +pub struct Barrier { + n: usize, + state: Mutex, + notify: Notify, +} + +struct BarrierState { + arrived: usize, + /// Bumped on each release, so a waiter can tell "my group has gone" from + /// "someone else's group has". Without it a task that is slow to be + /// scheduled after the wake rejoins the *next* rendezvous and hangs. + generation: usize, +} + +impl Barrier { + /// A barrier that releases once `n` tasks have arrived. + /// + /// `n` of 0 or 1 never blocks, which is what a single-threaded run of a + /// harness wants rather than a special case at every call site. + #[must_use] + pub fn new(n: usize) -> Self { + Self { + n, + state: Mutex::new(BarrierState { + arrived: 0, + generation: 0, + }), + notify: Notify::new(), + } + } + + /// Wait for the rest of the group. + /// + /// Returns `true` for exactly one waiter per rendezvous, the one whose + /// arrival completed it. Callers use that to elect a task to do the + /// once-per-round work without a second primitive. + pub async fn wait(&self) -> bool { + let generation = { + let mut state = self.state.lock(); + state.arrived += 1; + if state.arrived >= self.n { + state.arrived = 0; + state.generation = state.generation.wrapping_add(1); + drop(state); + self.notify.notify_waiters(); + return true; + } + state.generation + }; + + loop { + // Register before re-reading the generation. `notify_waiters` + // retains no permit, so a release landing between the read and the + // await would be lost and this task would wait for a rendezvous + // that already happened. + let notified = self.notify.notified(); + let mut notified = core::pin::pin!(notified); + notified.as_mut().enable(); + if self.state.lock().generation != generation { + return false; + } + notified.await; + } + } +} + +impl core::fmt::Debug for Barrier { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("Barrier") + .field("n", &self.n) + .finish_non_exhaustive() + } +} + +/// A counting semaphore. +pub struct Semaphore { + permits: AtomicUsize, + waiters: Waiters, +} + +impl Semaphore { + /// A semaphore starting with `permits` available. + #[must_use] + pub const fn new(permits: usize) -> Self { + Self { + permits: AtomicUsize::new(permits), + waiters: Waiters::new(), + } + } + + /// Permits available right now. + #[must_use] + pub fn available_permits(&self) -> usize { + self.permits.load(Ordering::Acquire) + } + + /// Take a permit without waiting, if one is free. + pub fn try_acquire(&self) -> Option> { + let mut current = self.permits.load(Ordering::Acquire); + loop { + if current == 0 { + return None; + } + match self.permits.compare_exchange_weak( + current, + current - 1, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => return Some(SemaphorePermit { semaphore: self }), + Err(seen) => current = seen, + } + } + } + + /// Wait for a permit. + pub fn acquire(&self) -> Acquire<'_> { + Acquire { semaphore: self } + } + + /// Hand the semaphore `n` more permits than it was created with. + /// + /// The counterpart to [`SemaphorePermit::forget`]: together they let a + /// caller move a permit's *ownership* somewhere the borrow could not + /// follow, which is how a semaphore gets used as a one-shot gate rather + /// than as a pool of N interchangeable slots. + pub fn add_permits(&self, n: usize) { + if n == 0 { + return; + } + self.permits.fetch_add(n, Ordering::AcqRel); + // One waker per permit. Waking only one and letting it cascade is + // wrong here: a woken task takes exactly one permit, so the rest would + // sleep through permits that are already available. + for _ in 0..n { + self.waiters.wake_one(); + } + } + + fn release(&self) { + self.permits.fetch_add(1, Ordering::AcqRel); + self.waiters.wake_one(); + } +} + +impl core::fmt::Debug for Semaphore { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("Semaphore") + .field("available_permits", &self.available_permits()) + .finish_non_exhaustive() + } +} + +/// A held semaphore permit, returned on drop. +pub struct SemaphorePermit<'a> { + semaphore: &'a Semaphore, +} + +impl SemaphorePermit<'_> { + /// Drop the permit without returning it, permanently shrinking the + /// semaphore by one. + /// + /// Pairs with [`Semaphore::add_permits`]. Forgetting every permit turns + /// the semaphore into a signal, where the count is a number of events that + /// happened rather than a number of slots that are free. + pub fn forget(self) { + core::mem::forget(self); + } +} + +impl core::fmt::Debug for SemaphorePermit<'_> { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("SemaphorePermit") + .finish_non_exhaustive() + } +} + +impl Drop for SemaphorePermit<'_> { + fn drop(&mut self) { + self.semaphore.release(); + } +} + +/// The future returned by [`Semaphore::acquire`]. +pub struct Acquire<'a> { + semaphore: &'a Semaphore, +} + +impl<'a> Future for Acquire<'a> { + type Output = SemaphorePermit<'a>; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if let Some(permit) = self.semaphore.try_acquire() { + return Poll::Ready(permit); + } + self.semaphore.waiters.push(context.waker()); + if let Some(permit) = self.semaphore.try_acquire() { + return Poll::Ready(permit); + } + Poll::Pending + } +} + +/// State value meaning "one writer holds this"; any other value is the number +/// of readers. +const WRITER: usize = usize::MAX; + +/// An async reader-writer lock. +pub struct RwLock { + state: AtomicUsize, + waiters: Waiters, + value: UnsafeCell, +} + +// SAFETY: access to `value` is gated by `state`, which admits either one writer +// or any number of readers and never both. `T: Send` is required to move the +// value between threads; `T: Sync` because readers hand out `&T` concurrently. +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} + +impl RwLock { + /// An unlocked `RwLock` holding `value`. + #[must_use] + pub const fn new(value: T) -> Self { + Self { + state: AtomicUsize::new(0), + waiters: Waiters::new(), + value: UnsafeCell::new(value), + } + } + + /// The value, consuming the lock. + pub fn into_inner(self) -> T { + self.value.into_inner() + } +} + +impl RwLock { + /// Exclusive access without a lock, because `&mut self` is already proof + /// that nobody else holds one. + pub fn get_mut(&mut self) -> &mut T { + self.value.get_mut() + } + + fn try_read_raw(&self) -> bool { + let mut current = self.state.load(Ordering::Acquire); + loop { + if current == WRITER { + return false; + } + match self.state.compare_exchange_weak( + current, + current + 1, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => return true, + Err(seen) => current = seen, + } + } + } + + fn try_write_raw(&self) -> bool { + self.state + .compare_exchange(0, WRITER, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + } + + fn release_read(&self) { + // The last reader out wakes a waiting writer. Waking on every release + // would be correct and wasteful; waking on none would stall the writer. + if self.state.fetch_sub(1, Ordering::AcqRel) == 1 { + self.waiters.wake_all(); + } + } + + fn release_write(&self) { + self.state.store(0, Ordering::Release); + // Every waiter, because the next holder may be a batch of readers and + // waking one of those leaves the rest asleep behind an unlocked lock. + self.waiters.wake_all(); + } + + /// Take a read lock without waiting, if no writer holds it. + pub fn try_read(&self) -> Option> { + self.try_read_raw().then(|| RwLockReadGuard { lock: self }) + } + + /// Take a write lock without waiting, if the lock is free. + pub fn try_write(&self) -> Option> { + self.try_write_raw() + .then(|| RwLockWriteGuard { lock: self }) + } + + /// Wait for a read lock. + pub fn read(&self) -> Read<'_, T> { + Read { lock: self } + } + + /// Wait for a write lock. + pub fn write(&self) -> Write<'_, T> { + Write { lock: self } + } +} + +impl RwLock { + /// Wait for a read lock that borrows the `Arc` rather than the lock. + /// + /// The guard a caller can hold across an await without naming a lifetime, + /// which is what `tokio::sync`'s owned guards were used for here. + pub fn read_owned(self: Arc) -> ReadOwned { + ReadOwned { lock: Some(self) } + } + + /// The write half of [`Self::read_owned`]. + pub fn write_owned(self: Arc) -> WriteOwned { + WriteOwned { lock: Some(self) } + } + + /// An owned read lock, or `None` if a writer holds it. + pub fn try_read_owned(self: Arc) -> Option> { + self.try_read_raw() + .then(|| OwnedRwLockReadGuard { lock: self }) + } + + /// An owned write lock, or `None` if the lock is held at all. + pub fn try_write_owned(self: Arc) -> Option> { + self.try_write_raw() + .then(|| OwnedRwLockWriteGuard { lock: self }) + } +} + +impl Default for RwLock { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl core::fmt::Debug for RwLock { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // The value is deliberately not shown: reading it would need the lock, + // and a `Debug` that can block is a debugging hazard rather than a help. + let state = self.state.load(Ordering::Relaxed); + let held = if state == WRITER { + "write" + } else if state == 0 { + "free" + } else { + "read" + }; + f.debug_struct("RwLock") + .field("held", &held) + .finish_non_exhaustive() + } +} + +/// A held read lock. +pub struct RwLockReadGuard<'a, T: ?Sized> { + lock: &'a RwLock, +} + +impl Deref for RwLockReadGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: a read guard exists only while `state` counts this reader, and + // a writer cannot hold the lock at the same time. + unsafe { &*self.lock.value.get() } + } +} + +impl Drop for RwLockReadGuard<'_, T> { + fn drop(&mut self) { + self.lock.release_read(); + } +} + +/// A held write lock. +pub struct RwLockWriteGuard<'a, T: ?Sized> { + lock: &'a RwLock, +} + +impl Deref for RwLockWriteGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: a write guard is the only guard that can exist. + unsafe { &*self.lock.value.get() } + } +} + +impl DerefMut for RwLockWriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: as above, and exclusively. + unsafe { &mut *self.lock.value.get() } + } +} + +impl Drop for RwLockWriteGuard<'_, T> { + fn drop(&mut self) { + self.lock.release_write(); + } +} + +/// A read lock that owns its `Arc`. +pub struct OwnedRwLockReadGuard { + lock: Arc>, +} + +impl Deref for OwnedRwLockReadGuard { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: as `RwLockReadGuard`. + unsafe { &*self.lock.value.get() } + } +} + +impl Drop for OwnedRwLockReadGuard { + fn drop(&mut self) { + self.lock.release_read(); + } +} + +/// A write lock that owns its `Arc`. +pub struct OwnedRwLockWriteGuard { + lock: Arc>, +} + +impl Deref for OwnedRwLockWriteGuard { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: as `RwLockWriteGuard`. + unsafe { &*self.lock.value.get() } + } +} + +impl DerefMut for OwnedRwLockWriteGuard { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: as `RwLockWriteGuard`. + unsafe { &mut *self.lock.value.get() } + } +} + +impl Drop for OwnedRwLockWriteGuard { + fn drop(&mut self) { + self.lock.release_write(); + } +} + +/// The future returned by [`RwLock::read`]. +pub struct Read<'a, T: ?Sized> { + lock: &'a RwLock, +} + +impl<'a, T: ?Sized> Future for Read<'a, T> { + type Output = RwLockReadGuard<'a, T>; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.lock.try_read_raw() { + return Poll::Ready(RwLockReadGuard { lock: self.lock }); + } + self.lock.waiters.push(context.waker()); + if self.lock.try_read_raw() { + return Poll::Ready(RwLockReadGuard { lock: self.lock }); + } + Poll::Pending + } +} + +/// The future returned by [`RwLock::write`]. +pub struct Write<'a, T: ?Sized> { + lock: &'a RwLock, +} + +impl<'a, T: ?Sized> Future for Write<'a, T> { + type Output = RwLockWriteGuard<'a, T>; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if self.lock.try_write_raw() { + return Poll::Ready(RwLockWriteGuard { lock: self.lock }); + } + self.lock.waiters.push(context.waker()); + if self.lock.try_write_raw() { + return Poll::Ready(RwLockWriteGuard { lock: self.lock }); + } + Poll::Pending + } +} + +/// The future returned by [`RwLock::read_owned`]. +pub struct ReadOwned { + lock: Option>>, +} + +impl Future for ReadOwned { + type Output = OwnedRwLockReadGuard; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let lock = this.lock.as_ref().expect("polled after completion"); + if lock.try_read_raw() { + let lock = this.lock.take().expect("just checked"); + return Poll::Ready(OwnedRwLockReadGuard { lock }); + } + lock.waiters.push(context.waker()); + if lock.try_read_raw() { + let lock = this.lock.take().expect("just checked"); + return Poll::Ready(OwnedRwLockReadGuard { lock }); + } + Poll::Pending + } +} + +/// The future returned by [`RwLock::write_owned`]. +pub struct WriteOwned { + lock: Option>>, +} + +impl Future for WriteOwned { + type Output = OwnedRwLockWriteGuard; + + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let lock = this.lock.as_ref().expect("polled after completion"); + if lock.try_write_raw() { + let lock = this.lock.take().expect("just checked"); + return Poll::Ready(OwnedRwLockWriteGuard { lock }); + } + lock.waiters.push(context.waker()); + if lock.try_write_raw() { + let lock = this.lock.take().expect("just checked"); + return Poll::Ready(OwnedRwLockWriteGuard { lock }); + } + Poll::Pending + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_on; + use alloc::vec::Vec; + + #[test] + fn a_notification_before_the_wait_is_not_lost() { + let notify = Notify::new(); + notify.notify_one(); + // Would hang if the permit were not remembered. + block_on(notify.notified()); + } + + #[test] + fn readers_share_and_a_writer_excludes() { + let lock = RwLock::new(5u32); + let a = lock.try_read().expect("first reader"); + let b = lock.try_read().expect("second reader"); + assert_eq!(*a, 5); + assert_eq!(*b, 5); + assert!(lock.try_write().is_none(), "a writer must not join readers"); + drop(a); + assert!(lock.try_write().is_none(), "one reader still holds it"); + drop(b); + let mut w = lock.try_write().expect("the last reader left"); + *w = 6; + drop(w); + assert_eq!(*block_on(lock.read()), 6); + } + + #[test] + fn a_write_guard_excludes_readers() { + let lock = RwLock::new(0u32); + let w = lock.try_write().expect("free"); + assert!(lock.try_read().is_none()); + drop(w); + assert!(lock.try_read().is_some()); + } + + #[test] + fn a_semaphore_hands_out_exactly_its_permits() { + let semaphore = Semaphore::new(2); + let a = semaphore.try_acquire().expect("first"); + let b = semaphore.try_acquire().expect("second"); + assert!(semaphore.try_acquire().is_none()); + drop(a); + let c = semaphore.try_acquire().expect("one was returned"); + drop(b); + drop(c); + assert_eq!(semaphore.available_permits(), 2); + } + + #[test] + fn a_barrier_releases_only_once_everyone_has_arrived() { + use crate::runtime::Runtime; + + let runtime = Runtime::new(4); + let barrier = Arc::new(Barrier::new(4)); + let passed = Arc::new(AtomicUsize::new(0)); + // Counted before and after, so a barrier that let anyone through early + // is visible rather than merely suspected. + let leaders = Arc::new(AtomicUsize::new(0)); + + let handles: alloc::vec::Vec<_> = (0..4) + .map(|_| { + let barrier = barrier.clone(); + let passed = passed.clone(); + let leaders = leaders.clone(); + runtime.spawn(async move { + if barrier.wait().await { + leaders.fetch_add(1, Ordering::Relaxed); + } + passed.fetch_add(1, Ordering::Relaxed); + }) + }) + .collect(); + for handle in handles { + block_on(handle); + } + + assert_eq!(passed.load(Ordering::Relaxed), 4); + assert_eq!( + leaders.load(Ordering::Relaxed), + 1, + "exactly one waiter leads a rendezvous" + ); + } + + #[test] + fn a_barrier_of_one_never_blocks() { + let barrier = Barrier::new(1); + assert!(block_on(barrier.wait())); + // Reusable: a second rendezvous behaves like the first. + assert!(block_on(barrier.wait())); + } + + #[test] + fn a_forgotten_permit_does_not_come_back() { + let semaphore = Semaphore::new(1); + block_on(semaphore.acquire()).forget(); + assert_eq!(semaphore.available_permits(), 0); + assert!(semaphore.try_acquire().is_none()); + } + + #[test] + fn added_permits_are_a_signal_a_forgetting_waiter_can_count() { + // The gate shape: `add_permits` says an event happened, the waiter + // consumes exactly one and forgets it, so the count tracks events + // rather than returning to a fixed pool. + let semaphore = Semaphore::new(0); + semaphore.add_permits(3); + assert_eq!(semaphore.available_permits(), 3); + for _ in 0..3 { + block_on(semaphore.acquire()).forget(); + } + assert_eq!(semaphore.available_permits(), 0); + assert!(semaphore.try_acquire().is_none()); + } + + #[test] + fn owned_guards_outlive_the_borrow() { + let lock = Arc::new(RwLock::new(Vec::::new())); + { + let mut w = block_on(lock.clone().write_owned()); + w.push(1); + } + let r = block_on(lock.clone().read_owned()); + assert_eq!(&*r, &[1]); + } +} diff --git a/src/task.rs b/src/task.rs index 477be3c..9da7a75 100644 --- a/src/task.rs +++ b/src/task.rs @@ -19,6 +19,52 @@ use core::ptr::NonNull; use async_task::{Runnable, Task}; use st3::fanout::{Act, Job, Pool}; +// Which pool worker this thread is, while it is running one. +// +// A raw pointer rather than an id, so a process with two pools cannot hand one +// pool's job to the other's worker. It is compared and never dereferenced. +#[cfg(feature = "std")] +std::thread_local! { + static CURRENT: core::cell::Cell> = + const { core::cell::Cell::new(None) }; +} + +/// Mark this thread as `worker` of `pool` until the guard drops. +/// +/// Called by [`crate::runtime::Runtime`] around its `Pool::run`. A caller +/// driving a pool by hand can call it too; without it nothing breaks, wakes +/// simply take the injector as they always did. +#[cfg(feature = "std")] +pub(crate) fn mark_current(pool: &Arc, worker: usize) -> CurrentGuard { + CURRENT.with(|current| current.set(Some((Arc::as_ptr(pool), worker)))); + CurrentGuard +} + +/// Clears the current-worker marker on drop, including on unwind. +#[cfg(feature = "std")] +pub(crate) struct CurrentGuard; + +#[cfg(feature = "std")] +impl Drop for CurrentGuard { + fn drop(&mut self) { + CURRENT.with(|current| current.set(None)); + } +} + +/// This thread's worker id, if it is running `pool`. +#[cfg(feature = "std")] +fn current_worker(pool: &Arc) -> Option { + CURRENT.with(|current| match current.get() { + Some((owner, worker)) if core::ptr::eq(owner, Arc::as_ptr(pool)) => Some(worker), + _ => None, + }) +} + +#[cfg(not(feature = "std"))] +fn current_worker(_pool: &Arc) -> Option { + None +} + /// Put `future` on `pool` and hand back the handle to its result. pub(crate) fn spawn(future: F, pool: Arc) -> Task where @@ -38,7 +84,22 @@ where // `F` and `F::Output` are `Send`; and `Runnable::run` catches no panic, // which is the same as every other job this pool takes. let job = unsafe { Job::from_raw(pointer, poll_once) }; - pool.submit_job(job); + // A wake that happens *on* a worker goes straight to that worker. + // + // Every wake used to take the injector every worker contends on, plus a + // wake for somebody already awake, and then came back on whichever + // worker won the race rather than the one holding the task's cache + // lines. `yield_now` wakes itself, so a task yielding in a retry loop + // paid that round trip per iteration, which is the shape a contended + // storage engine actually has. + // + // Off a worker thread this is the old path, which is what a wake from + // a timer thread or an application thread wants: it has no locality to + // preserve and the injector is where anyone can find the job. + match current_worker(&pool) { + Some(worker) => pool.submit_local(worker, job), + None => pool.submit_job(job), + } }; let (runnable, task) = async_task::spawn(future, schedule); diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..d876fb1 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,494 @@ +//! Sleeping and timing out, without asking the pool for either. +//! +//! # Why this is not a `Host` method +//! +//! The obvious design is to let a worker park with a deadline, so an idle pool +//! wakes exactly when the next timer is due. That needs [`Host`] to grow a +//! timed park, and it cannot: `StdHost` parks on `atomic_wait::wait`, which has +//! no timeout, so the trait method would have nothing to call. Changing the +//! parking primitive to get one would trade a futex for a condition variable on +//! the path ยง7 of the WorkTable work just finished measuring, which is a real +//! cost paid by every pool whether it uses timers or not. +//! +//! So timers live beside the pool instead of inside it. A registry holds the +//! pending deadlines; something drives it and wakes what is due; waking goes +//! through the ordinary [`Waker`], which submits the task back through the +//! ordinary path. The pool learns nothing about time, and a program that never +//! sleeps pays nothing. +//! +//! # What drives it +//! +//! With `std`, a thread, started the first time anything sleeps. Never before: +//! a runtime that is only ever polled should not own a thread it does not use. +//! +//! Without `std` there is no thread and no clock, so the consumer supplies +//! both: [`set_clock`] once, then [`poll`] from a tick interrupt or its own +//! loop. That is the whole no_std contract, and it is why the registry and the +//! driver are separate things in this file. +//! +//! [`Host`]: st3::fanout::Host + +use alloc::collections::BinaryHeap; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::cmp::Ordering as CmpOrdering; +use core::future::Future; +use core::pin::Pin; +use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use core::task::{Context, Poll, Waker}; +use core::time::Duration; + +use spin::Mutex; + +/// One pending timer, shared between the future and the registry. +/// +/// The future owns one end and the heap the other, so a cancelled timer can be +/// disarmed without finding and removing its heap entry: the entry survives to +/// its deadline and fires nothing. +#[derive(Debug)] +struct Slot { + fired: AtomicBool, + waker: Mutex>, +} + +/// A deadline and what to wake at it. +struct Entry { + deadline: u64, + /// Breaks ties so equal deadlines fire in registration order rather than + /// an arbitrary one, which keeps a paced loop from starving itself. + seq: u64, + slot: Arc, +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + (self.deadline, self.seq) == (other.deadline, other.seq) + } +} +impl Eq for Entry {} +impl PartialOrd for Entry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Entry { + /// Reversed, because `BinaryHeap` is a max-heap and the earliest deadline + /// is the one wanted first. + fn cmp(&self, other: &Self) -> CmpOrdering { + other + .deadline + .cmp(&self.deadline) + .then_with(|| other.seq.cmp(&self.seq)) + } +} + +static PENDING: Mutex>> = Mutex::new(None); +static SEQ: AtomicU64 = AtomicU64::new(0); +static CLOCK: AtomicUsize = AtomicUsize::new(0); + +/// Tell this module how to read a monotonic clock, in nanoseconds. +/// +/// Required before any timer is used without `std`, where there is no clock to +/// default to. With `std` this is already set and calling it replaces the +/// default, which is worth doing only to share an origin with something else. +/// +/// Only the first call takes effect, so a late caller cannot move time under a +/// timer that is already pending. +pub fn set_clock(clock: fn() -> u64) { + let _ = CLOCK.compare_exchange(0, clock as usize, Ordering::Release, Ordering::Relaxed); +} + +/// Now, in nanoseconds, on whatever clock this module was given. +/// +/// # Panics +/// +/// Without `std`, if [`set_clock`] has not been called. There is no monotonic +/// clock to fall back on and a timer that silently never fires is worse than a +/// panic that names the missing call. +#[must_use] +pub fn now_ns() -> u64 { + // The install is behind the load, not in front of it. Reading the clock is + // on the path of every `sleep` and every `timeout`, including the ones that + // never arm, and installing unconditionally put a second `OnceLock` on that + // path to answer a question already settled by the first call in the + // process. + #[cfg(feature = "std")] + let raw = { + let mut raw = CLOCK.load(Ordering::Acquire); + if raw == 0 { + driver::install_default_clock(); + raw = CLOCK.load(Ordering::Acquire); + } + raw + }; + // Nothing to install: without `std` the consumer's `set_clock` is the only + // way a clock ever arrives, so a zero here is the missing call, not a + // default waiting to be taken. + #[cfg(not(feature = "std"))] + let raw = CLOCK.load(Ordering::Acquire); + assert!(raw != 0, "nagoya::time::set_clock has not been called"); + // SAFETY: the only value ever stored is a `fn() -> u64` cast to `usize` by + // `set_clock`, and the compare-exchange there means it is written once. + let clock: fn() -> u64 = unsafe { core::mem::transmute:: u64>(raw) }; + clock() +} + +/// Fire every timer due at `now`, and report when the next one is due. +/// +/// The driver for this is a thread with `std`. Without one, call it from a tick +/// interrupt or an idle loop; nothing else pumps the registry, and a timer that +/// is never polled never fires. +/// +/// Returns `None` when nothing is pending, so a caller with nothing else to do +/// can sleep until something registers rather than spinning on an empty heap. +pub fn poll(now: u64) -> Option { + // Wakers are collected and woken after the lock is released. A waker may + // register another timer, and this lock is not reentrant. + let mut due: Vec = Vec::new(); + let next = { + let mut guard = PENDING.lock(); + let heap = guard.as_mut()?; + while let Some(entry) = heap.peek() { + if entry.deadline > now { + break; + } + let entry = heap.pop().expect("peeked"); + entry.slot.fired.store(true, Ordering::Release); + // Bind before the entry drops. The guard borrows it, and the + // scrutinee of an `if let` outlives the statement that produced it. + let waker = entry.slot.waker.lock().take(); + if let Some(waker) = waker { + due.push(waker); + } + } + heap.peek().map(|entry| entry.deadline) + }; + for waker in due { + waker.wake(); + } + next +} + +/// Register `slot` to fire at `deadline`, and say whether it is now the +/// earliest pending timer. +fn arm(deadline: u64, slot: &Arc) -> bool { + let mut guard = PENDING.lock(); + let heap = guard.get_or_insert_with(BinaryHeap::new); + let earliest = heap.peek().is_none_or(|entry| deadline < entry.deadline); + heap.push(Entry { + deadline, + seq: SEQ.fetch_add(1, Ordering::Relaxed), + slot: Arc::clone(slot), + }); + earliest +} + +/// A future that is ready once its deadline has passed. +/// +/// Dropping one disarms it. The heap entry survives until its deadline, holding +/// nothing and waking nothing, so a cancelled `timeout` costs one dead entry +/// for as long as it had left to run rather than forever. +#[derive(Debug)] +pub struct Sleep { + deadline: u64, + /// `None` until this is first polled and found to still be in the future. + /// + /// **The allocation is deferred because most timers never need it.** + /// `timeout` polls its inner future first and returns without touching the + /// sleep whenever that future is ready, which in production is nearly every + /// call. Allocating a slot in `sleep()` charged every one of those for a + /// timer that was never armed: measured at 65 ns against tokio's 27 ns for + /// the same arm-and-cancel loop. + slot: Option>, +} + +impl Sleep { + /// When this fires, on the clock [`now_ns`] reads. + #[must_use] + pub fn deadline(&self) -> u64 { + self.deadline + } +} + +/// A future that is ready after `duration`. +/// +/// The deadline is fixed here, at the call, not at the first poll, so a sleep +/// that sits unpolled in a `select!` arm does not quietly extend itself. +#[must_use] +pub fn sleep(duration: Duration) -> Sleep { + sleep_until(now_ns().saturating_add(u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX))) +} + +/// A future that is ready once the clock reaches `deadline`. +#[must_use] +pub fn sleep_until(deadline: u64) -> Sleep { + Sleep { + deadline, + slot: None, + } +} + +impl Future for Sleep { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + let this = self.get_mut(); + match &this.slot { + Some(slot) => { + if slot.fired.load(Ordering::Acquire) { + return Poll::Ready(()); + } + // Re-registering matters: a task moved between wakes carries a + // different waker, and the one held here would wake nothing. + *slot.waker.lock() = Some(cx.waker().clone()); + // Checked again after registering, because the driver may have + // fired between the load above and the store. Without this the + // waker is consumed by a firing that already happened and the + // sleep never returns. + if slot.fired.load(Ordering::Acquire) { + return Poll::Ready(()); + } + Poll::Pending + } + // **The deadline is checked before anything is allocated or + // locked.** A sleep whose time has already passed is by far the + // common case for a batch of timers awaited in order, and arming it + // costs a full round trip through the driver to learn what the + // clock could have said here: measured at 5495 us of overshoot on a + // 10 ms deadline against tokio's 3844. + None => { + if now_ns() >= this.deadline { + return Poll::Ready(()); + } + let slot = Arc::new(Slot { + fired: AtomicBool::new(false), + waker: Mutex::new(Some(cx.waker().clone())), + }); + let earliest = arm(this.deadline, &slot); + this.slot = Some(slot); + if earliest { + #[cfg(feature = "std")] + driver::nudge(); + } + Poll::Pending + } + } + } +} + +impl Drop for Sleep { + fn drop(&mut self) { + // Disarm rather than unregister. Finding this entry in the heap costs a + // linear scan under the lock; leaving it costs one wake of nothing. + if let Some(slot) = &self.slot { + *slot.waker.lock() = None; + } + } +} + +/// What [`timeout`] returns when the future did not finish in time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Elapsed; + +impl core::fmt::Display for Elapsed { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("the future did not complete within its timeout") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Elapsed {} + +/// Run `future`, giving up after `duration`. +/// +/// The inner future is polled first on every wake, so one that is ready at the +/// same moment its deadline passes completes rather than timing out. A timeout +/// is a failure to make progress, and a future that has made it should not be +/// punished for the order two wakes happened to arrive in. +pub fn timeout(duration: Duration, future: F) -> Timeout { + Timeout { + future, + sleep: sleep(duration), + } +} + +/// The future [`timeout`] returns. +#[derive(Debug)] +pub struct Timeout { + future: F, + sleep: Sleep, +} + +impl Future for Timeout { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // SAFETY: neither field is moved out, and the projections below are the + // only access to them, so both stay pinned for as long as this is. + let this = unsafe { self.get_unchecked_mut() }; + let future = unsafe { Pin::new_unchecked(&mut this.future) }; + if let Poll::Ready(out) = future.poll(cx) { + return Poll::Ready(Ok(out)); + } + let sleep = unsafe { Pin::new_unchecked(&mut this.sleep) }; + match sleep.poll(cx) { + Poll::Ready(()) => Poll::Ready(Err(Elapsed)), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(feature = "std")] +mod driver { + use super::{now_ns, poll, set_clock}; + use std::sync::{Condvar, Mutex, OnceLock}; + use std::time::{Duration, Instant}; + + static ORIGIN: OnceLock = OnceLock::new(); + static NUDGE: OnceLock<(Mutex, Condvar)> = OnceLock::new(); + static THREAD: OnceLock<()> = OnceLock::new(); + + fn std_clock() -> u64 { + let origin = ORIGIN.get_or_init(Instant::now); + u64::try_from(origin.elapsed().as_nanos()).unwrap_or(u64::MAX) + } + + /// Point the registry at `Instant` unless a consumer already chose a clock. + pub(super) fn install_default_clock() { + let _ = ORIGIN.get_or_init(Instant::now); + set_clock(std_clock); + } + + fn nudge_pair() -> &'static (Mutex, Condvar) { + NUDGE.get_or_init(|| (Mutex::new(false), Condvar::new())) + } + + /// Wake the driver because a nearer deadline than it is waiting for now + /// exists. Starts the driver if this is the first timer in the process. + pub(super) fn nudge() { + start(); + let (lock, signal) = nudge_pair(); + *lock.lock().expect("the nudge lock") = true; + signal.notify_one(); + } + + fn start() { + THREAD.get_or_init(|| { + install_default_clock(); + std::thread::Builder::new() + .name("nagoya-timer".into()) + .spawn(run) + .expect("a timer thread"); + }); + } + + fn run() { + let (lock, signal) = nudge_pair(); + loop { + let next = poll(now_ns()); + let mut nudged = lock.lock().expect("the nudge lock"); + if *nudged { + // A timer was armed while the heap above was being drained, so + // `next` may already be stale. Go round rather than wait on it. + *nudged = false; + continue; + } + match next { + Some(deadline) => { + let now = now_ns(); + if deadline <= now { + continue; + } + let (guard, _) = signal + .wait_timeout(nudged, Duration::from_nanos(deadline - now)) + .expect("the nudge lock"); + nudged = guard; + } + // Nothing pending. Waiting rather than looping is what keeps an + // idle timer thread off the CPU entirely. + None => nudged = signal.wait(nudged).expect("the nudge lock"), + } + *nudged = false; + } + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::{now_ns, sleep, timeout, Elapsed}; + use crate::block_on; + use core::time::Duration; + use std::time::Instant; + + #[test] + fn a_sleep_waits_at_least_its_duration() { + let started = Instant::now(); + block_on(sleep(Duration::from_millis(50))); + // Only the lower bound is asserted. The upper one is the scheduler's + // and the machine's, and a test that fails when the box is busy is a + // test that gets ignored. + assert!(started.elapsed() >= Duration::from_millis(50)); + } + + #[test] + fn sleeps_fire_in_deadline_order_not_registration_order() { + let order = block_on(async { + let mut out = alloc::vec::Vec::new(); + let far = sleep(Duration::from_millis(60)); + let near = sleep(Duration::from_millis(10)); + near.await; + out.push("near"); + far.await; + out.push("far"); + out + }); + assert_eq!(order, ["near", "far"]); + } + + #[test] + fn a_timeout_lets_a_finished_future_through() { + let out = block_on(timeout(Duration::from_secs(30), async { 7 })); + assert_eq!(out, Ok(7)); + } + + #[test] + fn a_timeout_gives_up_on_one_that_does_not_finish() { + let out = block_on(timeout( + Duration::from_millis(20), + sleep(Duration::from_secs(30)), + )); + assert_eq!(out, Err(Elapsed)); + } + + #[test] + fn a_future_ready_at_its_deadline_wins() { + // The inner future is polled first on every wake, so this completes + // rather than racing its own timer. Same duration on both sides. + let out = block_on(timeout( + Duration::from_millis(20), + sleep(Duration::from_millis(20)), + )); + assert_eq!(out, Ok(())); + } + + #[test] + fn dropping_a_sleep_disarms_it() { + // A cancelled timer must not wake anything. Arming and dropping many + // of them, then sleeping past all their deadlines, would deadlock or + // panic in `poll` if a dead slot were still woken. + block_on(async { + for _ in 0..1_000 { + let mut pending = sleep(Duration::from_millis(5)); + let _ = timeout(Duration::from_nanos(1), &mut pending).await; + } + sleep(Duration::from_millis(20)).await; + }); + } + + #[test] + fn the_clock_only_goes_forwards() { + let first = now_ns(); + let second = now_ns(); + assert!(second >= first); + } +} diff --git a/tests/executor.rs b/tests/executor.rs index 5dc9ebb..a994b68 100644 --- a/tests/executor.rs +++ b/tests/executor.rs @@ -241,7 +241,10 @@ fn dropping_a_loop_cancels_it() { let cancel = loop_.cancel(); assert!(!cancel.is_cancelled()); drop(loop_); - assert!(cancel.is_cancelled(), "dropping the future did not cancel it"); + assert!( + cancel.is_cancelled(), + "dropping the future did not cancel it" + ); } /// A yielding task finishes, and lets others run while it does. @@ -257,7 +260,9 @@ fn a_task_can_yield_its_worker() { } first.fetch_add(1, Ordering::AcqRel) }); - let short = running.executor.spawn(async move { second.fetch_add(1, Ordering::AcqRel) }); + let short = running + .executor + .spawn(async move { second.fetch_add(1, Ordering::AcqRel) }); let (long, short) = (block_on(long), block_on(short)); assert_eq!(long, Some(1), "the yielding task did not let the other in");