├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src └── lib.rs └── tests ├── async-std.rs ├── sync-wait.rs └── tokio.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: 3 | - push 4 | # Build if requested manually from the Actions tab 5 | - workflow_dispatch 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: --deny warnings 10 | 11 | jobs: 12 | formatting: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | components: rustfmt 21 | default: true 22 | - name: Check formatting 23 | run: cargo fmt -- --check 24 | 25 | linting: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | components: clippy 34 | default: true 35 | - name: Lint (clippy) 36 | uses: actions-rs/clippy-check@v1 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | audit: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - uses: actions-rs/toolchain@v1 46 | with: 47 | profile: minimal 48 | toolchain: stable 49 | default: true 50 | - name: Install cargo-audit 51 | uses: actions-rs/install@v0.1.2 52 | with: 53 | crate: cargo-audit 54 | version: latest 55 | use-tool-cache: true 56 | - name: Audit 57 | run: cargo audit --deny warnings 58 | 59 | 60 | build-and-test: 61 | strategy: 62 | matrix: 63 | os: [ubuntu-latest, macos-latest, windows-latest] 64 | rust: [stable, beta] 65 | include: 66 | - os: ubuntu-latest 67 | rust: nightly 68 | runs-on: ${{ matrix.os }} 69 | steps: 70 | - uses: actions/checkout@v2 71 | 72 | - uses: actions-rs/toolchain@v1 73 | with: 74 | profile: minimal 75 | toolchain: ${{ matrix.rust }} 76 | default: true 77 | 78 | - name: Build 79 | uses: actions-rs/cargo@v1 80 | with: 81 | command: build 82 | 83 | - name: Test 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: test 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | - stable 5 | - beta 6 | - 1.39.0 7 | os: 8 | - linux 9 | - osx 10 | - windows 11 | 12 | script: 13 | - cargo build --verbose 14 | - cargo test --verbose 15 | - if [[ "${TRAVIS_RUST_VERSION}" = "stable" && $(uname -s) == "Linux" ]]; then 16 | rustup component add rustfmt-preview; 17 | rustfmt --version; 18 | cargo fmt -- --check; 19 | else 20 | echo "Not checking formatting on this build"; 21 | fi 22 | 23 | notifications: 24 | email: 25 | on_success: never 26 | on_failure: never 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ### Categories each change fall into 8 | 9 | * **Added**: for new features. 10 | * **Changed**: for changes in existing functionality. 11 | * **Deprecated**: for soon-to-be removed features. 12 | * **Removed**: for now removed features. 13 | * **Fixed**: for any bug fixes. 14 | * **Security**: in case of vulnerabilities. 15 | 16 | 17 | ## [Unreleased] 18 | 19 | 20 | ## [0.1.3] - 2025-05-12 21 | ### Added 22 | - Add `Listener::wait_timeout`. Allows waiting for a trigger for a max amount of time. 23 | - Add license files to the package/repository. 24 | 25 | ### Fixed 26 | - Fix `let_underscore_lock` error, not assigning a lock guard to a variable. 27 | 28 | 29 | ## [0.1.2] - 2021-07-19 30 | ### Fixed 31 | - Don't store multiple `Waker`s for single `Listener`s that are being polled 32 | multiple times. Instead only store the last `Waker`. Fixes issue #1. 33 | 34 | 35 | ## [0.1.1] - 2020-05-01 36 | ### Added 37 | - Add `is_triggered` method to both `Trigger` and `Listener`. 38 | 39 | 40 | ## [0.1.0] - 2020-04-20 41 | First version 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "triggered" 3 | version = "0.1.3" 4 | authors = ["Linus Färnstrand "] 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | description = "Triggers for one time events between tasks and threads" 8 | repository = "https://github.com/faern/triggered" 9 | keywords = ["oneshot", "mpmc", "async", "sync", "event"] 10 | categories = ["asynchronous", "concurrency"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | 15 | [dev-dependencies] 16 | tokio = { version = "1.15.0", features = ["rt", "rt-multi-thread", "macros"] } 17 | async-std = { version = "1.5.0", features = ["attributes"] } 18 | ctrlc = "3.1.4" 19 | futures = "0.3.4" 20 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2025 Linus Färnstrand 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # triggered 2 | 3 | Triggers for one time events between tasks and threads. 4 | 5 | The mechanism consists of two types, the `Trigger` and the `Listener`. They come together 6 | as a pair. Much like the sender/receiver pair of a channel. The trigger half has a 7 | `Trigger::trigger` method that will make all tasks/threads waiting on 8 | a listener continue executing. 9 | The listener both has a sync `Listener::wait` method, and it also implements 10 | `Future` for async support. 11 | 12 | Both the `Trigger` and `Listener` can be cloned. So any number of trigger instances can 13 | trigger any number of waiting listeners. When any one trigger instance belonging to the pair is 14 | triggered, all the waiting listeners will be unblocked. Waiting on a listener whose 15 | trigger already went off will return instantly. So each trigger/listener pair can only be fired 16 | once. 17 | 18 | This crate does not use any `unsafe` code. 19 | 20 | ## Examples 21 | 22 | A trivial example showing the basic usage: 23 | 24 | ```rust 25 | #[tokio::main] 26 | async fn main() { 27 | let (trigger, listener) = triggered::trigger(); 28 | 29 | let task = tokio::spawn(async { 30 | // Blocks until `trigger.trigger()` below 31 | listener.await; 32 | 33 | println!("Triggered async task"); 34 | }); 35 | 36 | // This will make any thread blocked in `Listener::wait()` or async task awaiting the 37 | // listener continue execution again. 38 | trigger.trigger(); 39 | 40 | let _ = task.await; 41 | } 42 | ``` 43 | 44 | An example showing a trigger/listener pair being used to gracefully shut down some async 45 | server instances on a Ctrl-C event, where only an immutable `Fn` closure is accepted: 46 | 47 | ```rust 48 | 49 | #[tokio::main] 50 | async fn main() -> Result<(), Error> { 51 | let (shutdown_trigger, shutdown_signal1) = triggered::trigger(); 52 | 53 | // A sync `Fn` closure will trigger the trigger when the user hits Ctrl-C 54 | ctrlc::set_handler(move || { 55 | shutdown_trigger.trigger(); 56 | }).expect("Error setting Ctrl-C handler"); 57 | 58 | // If the server library has support for something like a shutdown signal: 59 | let shutdown_signal2 = shutdown_signal1.clone(); 60 | let server1_task = tokio::spawn(async move { 61 | SomeServer::new().serve_with_shutdown_signal(shutdown_signal1).await; 62 | }); 63 | 64 | // Or just select between the long running future and the signal to abort it 65 | tokio::select! { 66 | server_result = SomeServer::new().serve() => { 67 | eprintln!("Server error: {:?}", server_result); 68 | } 69 | _ = shutdown_signal2 => {} 70 | } 71 | 72 | let _ = server1_task.await; 73 | Ok(()) 74 | } 75 | ``` 76 | 77 | ## Rust Compatibility 78 | 79 | Will work with at least the two latest stable Rust releases. This gives users at least six 80 | weeks to upgrade their Rust toolchain after a new stable is released. 81 | 82 | The current MSRV can be seen in `travis.yml`. Any change to the MSRV will be considered a 83 | breaking change and listed in the [changelog](CHANGELOG.md). 84 | 85 | ## Comparison with similar primitives 86 | 87 | ### Channels 88 | 89 | The event triggering primitives in this library is somewhat similar to channels. The main 90 | difference and why I developed this library is that 91 | 92 | The listener is somewhat similar to a `futures::channel::oneshot::Receiver<()>`. But it: 93 | * Is not fallible - Implements `Future` instead of 94 | `Future>` 95 | * Implements `Clone` - Any number of listeners can wait for the same event 96 | * Has a sync [`Listener::wait`] - Both synchronous threads, and asynchronous tasks can wait 97 | at the same time. 98 | 99 | The trigger, when compared to a `futures::channel::oneshot::Sender<()>` has the differences 100 | that it: 101 | * Is not fallible - The trigger does not care if there are any listeners left 102 | * Does not consume itself on send, instead takes `&self` - So can be used 103 | in situations where it is not owned or not mutable. For example in `Drop` implementations 104 | or callback closures that are limited to `Fn` or `FnMut`. 105 | 106 | ### `futures::future::Abortable` 107 | 108 | One use case of these triggers is to abort futures when some event happens. See examples above. 109 | The differences include: 110 | * A single handle can abort any number of futures 111 | * Some futures are not properly cleaned up when just dropped the way `Abortable` does it. 112 | These libraries sometimes allows creating their futures with a shutdown signal that triggers 113 | a clean abort. Something like `serve_with_shutdown(signal: impl Future)`. 114 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Triggers for one time events between tasks and threads. 2 | //! 3 | //! The mechanism consists of two types, the [`Trigger`] and the [`Listener`]. They come together 4 | //! as a pair. Much like the sender/receiver pair of a channel. The trigger half has a 5 | //! [`Trigger::trigger`] method that will make all tasks/threads waiting on 6 | //! a listener continue executing. 7 | //! The listener both has a sync [`Listener::wait`] method, and it also implements 8 | //! `Future` for async support. 9 | //! 10 | //! Both the [`Trigger`] and [`Listener`] can be cloned. So any number of trigger instances can 11 | //! trigger any number of waiting listeners. When any one trigger instance belonging to the pair is 12 | //! triggered, all the waiting listeners will be unblocked. Waiting on a listener whose 13 | //! trigger already went off will return instantly. So each trigger/listener pair can only be fired 14 | //! once. 15 | //! 16 | //! This crate does not use any `unsafe` code. 17 | //! 18 | //! # Examples 19 | //! 20 | //! A trivial example showing the basic usage: 21 | //! 22 | //! ``` 23 | //! #[tokio::main] 24 | //! async fn main() { 25 | //! let (trigger, listener) = triggered::trigger(); 26 | //! 27 | //! let task = tokio::spawn(async { 28 | //! // Blocks until `trigger.trigger()` below 29 | //! listener.await; 30 | //! 31 | //! println!("Triggered async task"); 32 | //! }); 33 | //! 34 | //! // This will make any thread blocked in `Listener::wait()` or async task awaiting the 35 | //! // listener continue execution again. 36 | //! trigger.trigger(); 37 | //! 38 | //! let _ = task.await; 39 | //! } 40 | //! ``` 41 | //! 42 | //! An example showing a trigger/listener pair being used to gracefully shut down some async 43 | //! server instances on a Ctrl-C event, where only an immutable `Fn` closure is accepted: 44 | //! 45 | //! ``` 46 | //! # use std::future::Future; 47 | //! # type Error = Box; 48 | //! # struct SomeServer; 49 | //! # impl SomeServer { 50 | //! # fn new() -> Self { SomeServer } 51 | //! # async fn serve_with_shutdown_signal(self, s: impl Future) -> Result<(), Error> {Ok(())} 52 | //! # async fn serve(self) -> Result<(), Error> {Ok(())} 53 | //! # } 54 | //! 55 | //! #[tokio::main] 56 | //! async fn main() -> Result<(), Error> { 57 | //! let (shutdown_trigger, shutdown_signal1) = triggered::trigger(); 58 | //! 59 | //! // A sync `Fn` closure will trigger the trigger when the user hits Ctrl-C 60 | //! ctrlc::set_handler(move || { 61 | //! shutdown_trigger.trigger(); 62 | //! }).expect("Error setting Ctrl-C handler"); 63 | //! 64 | //! // If the server library has support for something like a shutdown signal: 65 | //! let shutdown_signal2 = shutdown_signal1.clone(); 66 | //! let server1_task = tokio::spawn(async move { 67 | //! SomeServer::new().serve_with_shutdown_signal(shutdown_signal1).await; 68 | //! }); 69 | //! 70 | //! // Or just select between the long running future and the signal to abort it 71 | //! tokio::select! { 72 | //! server_result = SomeServer::new().serve() => { 73 | //! eprintln!("Server error: {:?}", server_result); 74 | //! } 75 | //! _ = shutdown_signal2 => {} 76 | //! } 77 | //! 78 | //! let _ = server1_task.await; 79 | //! Ok(()) 80 | //! } 81 | //! ``` 82 | //! 83 | //! # Rust Compatibility 84 | //! 85 | //! Will work with at least the two latest stable Rust releases. This gives users at least six 86 | //! weeks to upgrade their Rust toolchain after a new stable is released. 87 | //! 88 | //! The current MSRV can be seen in `travis.yml`. Any change to the MSRV will be considered a 89 | //! breaking change and listed in the changelog. 90 | //! 91 | //! # Comparison with similar primitives 92 | //! 93 | //! ## Channels 94 | //! 95 | //! The event triggering primitives in this library is somewhat similar to channels. The main 96 | //! difference and why I developed this library is that 97 | //! 98 | //! The listener is somewhat similar to a `futures::channel::oneshot::Receiver<()>`. But it: 99 | //! * Is not fallible - Implements `Future` instead of 100 | //! `Future>` 101 | //! * Implements `Clone` - Any number of listeners can wait for the same event 102 | //! * Has a sync [`Listener::wait`] - Both synchronous threads, and asynchronous tasks can wait 103 | //! at the same time. 104 | //! 105 | //! The trigger, when compared to a `futures::channel::oneshot::Sender<()>` has the differences 106 | //! that it: 107 | //! * Is not fallible - The trigger does not care if there are any listeners left 108 | //! * Does not consume itself on send, instead takes `&self` - So can be used 109 | //! in situations where it is not owned or not mutable. For example in `Drop` implementations 110 | //! or callback closures that are limited to `Fn` or `FnMut`. 111 | //! 112 | //! ## `futures::future::Abortable` 113 | //! 114 | //! One use case of these triggers is to abort futures when some event happens. See examples above. 115 | //! The differences include: 116 | //! * A single handle can abort any number of futures 117 | //! * Some futures are not properly cleaned up when just dropped the way `Abortable` does it. 118 | //! These libraries sometimes allows creating their futures with a shutdown signal that triggers 119 | //! a clean abort. Something like `serve_with_shutdown(signal: impl Future)`. 120 | 121 | #![deny(unsafe_code)] 122 | #![deny(rust_2018_idioms)] 123 | 124 | use std::collections::HashMap; 125 | use std::mem; 126 | use std::pin::Pin; 127 | use std::sync::{ 128 | atomic::{AtomicBool, AtomicUsize, Ordering}, 129 | Arc, Condvar, Mutex, 130 | }; 131 | use std::task::{Context, Poll, Waker}; 132 | use std::time::Duration; 133 | 134 | /// Returns a [`Trigger`] and [`Listener`] pair bound to each other. 135 | /// 136 | /// The [`Listener`] is used to wait for the trigger to fire. It can be waited on both sync 137 | /// and async. 138 | pub fn trigger() -> (Trigger, Listener) { 139 | let inner = Arc::new(Inner { 140 | complete: AtomicBool::new(false), 141 | tasks: Mutex::new(HashMap::new()), 142 | condvar: Condvar::new(), 143 | next_listener_id: AtomicUsize::new(1), 144 | }); 145 | let trigger = Trigger { 146 | inner: inner.clone(), 147 | }; 148 | let listener = Listener { inner, id: 0 }; 149 | (trigger, listener) 150 | } 151 | 152 | /// A struct used to trigger [`Listener`]s it is paired with. 153 | /// 154 | /// Can be cloned to create multiple instances that all trigger the same listeners. 155 | #[derive(Clone, Debug)] 156 | pub struct Trigger { 157 | inner: Arc, 158 | } 159 | 160 | /// A struct used to wait for a trigger event from a [`Trigger`]. 161 | /// 162 | /// Can be waited on synchronously via [`Listener::wait`] or asynchronously thanks to the struct 163 | /// implementing `Future`. 164 | /// 165 | /// The listener can be cloned and any amount of threads and tasks can wait for the same trigger 166 | /// at the same time. 167 | #[derive(Debug)] 168 | pub struct Listener { 169 | inner: Arc, 170 | id: usize, 171 | } 172 | 173 | impl Clone for Listener { 174 | fn clone(&self) -> Self { 175 | Listener { 176 | inner: self.inner.clone(), 177 | id: self.inner.next_listener_id.fetch_add(1, Ordering::SeqCst), 178 | } 179 | } 180 | } 181 | 182 | impl Drop for Listener { 183 | fn drop(&mut self) { 184 | self.inner 185 | .tasks 186 | .lock() 187 | .expect("Some Trigger/Listener has panicked") 188 | .remove(&self.id); 189 | } 190 | } 191 | 192 | #[derive(Debug)] 193 | struct Inner { 194 | complete: AtomicBool, 195 | tasks: Mutex>, 196 | condvar: Condvar, 197 | next_listener_id: AtomicUsize, 198 | } 199 | 200 | impl Unpin for Trigger {} 201 | impl Unpin for Listener {} 202 | 203 | impl Trigger { 204 | /// Trigger all [`Listener`]s paired with this trigger. 205 | /// 206 | /// Makes all listeners currently blocked in [`Listener::wait`] return, 207 | /// and all that is being `await`ed finish. 208 | /// 209 | /// Calling this method only does anything the first time. Any subsequent trigger call to 210 | /// the same instance or a clone thereof does nothing, it has already been triggered. 211 | /// Any listener waiting on the trigger after it has been triggered will just return 212 | /// instantly. 213 | /// 214 | /// This method is safe to call from both async and sync code. It's not an async function, 215 | /// but it always finishes very fast. 216 | pub fn trigger(&self) { 217 | if self.inner.complete.swap(true, Ordering::SeqCst) { 218 | return; 219 | } 220 | // This code will only be executed once per trigger instance. No matter the amount of 221 | // `Trigger` clones or calls to `trigger()`, thanks to the atomic swap above. 222 | let mut tasks_guard = self 223 | .inner 224 | .tasks 225 | .lock() 226 | .expect("Some Trigger/Listener has panicked"); 227 | let tasks = mem::take(&mut *tasks_guard); 228 | mem::drop(tasks_guard); 229 | for (_listener_id, task) in tasks { 230 | task.wake(); 231 | } 232 | self.inner.condvar.notify_all(); 233 | } 234 | 235 | /// Returns true if this trigger has been triggered. 236 | pub fn is_triggered(&self) -> bool { 237 | self.inner.complete.load(Ordering::SeqCst) 238 | } 239 | } 240 | 241 | impl std::future::Future for Listener { 242 | type Output = (); 243 | 244 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 245 | if self.inner.complete.load(Ordering::SeqCst) { 246 | return Poll::Ready(()); 247 | } 248 | 249 | let mut task_guard = self 250 | .inner 251 | .tasks 252 | .lock() 253 | .expect("Some Trigger/Listener has panicked"); 254 | 255 | // If the trigger completed while we waited for the lock, skip adding our waker to the list 256 | // of tasks. 257 | if self.inner.complete.load(Ordering::SeqCst) { 258 | Poll::Ready(()) 259 | } else { 260 | task_guard.insert(self.id, cx.waker().clone()); 261 | Poll::Pending 262 | } 263 | } 264 | } 265 | 266 | impl Listener { 267 | /// Wait for this trigger synchronously. 268 | /// 269 | /// Blocks the current thread until the corresponding [`Trigger`] is triggered. 270 | /// If the trigger has already been triggered at least once, this returns immediately. 271 | pub fn wait(&self) { 272 | if self.inner.complete.load(Ordering::SeqCst) { 273 | return; 274 | } 275 | 276 | let task_guard = self 277 | .inner 278 | .tasks 279 | .lock() 280 | .expect("Some Trigger/Listener has panicked"); 281 | 282 | let _guard = self 283 | .inner 284 | .condvar 285 | .wait_while(task_guard, |_| !self.inner.complete.load(Ordering::SeqCst)) 286 | .expect("Some Trigger/Listener has panicked"); 287 | } 288 | 289 | /// Wait for this trigger synchronously, timing out after a specified duration. 290 | /// 291 | /// The semantics of this function are equivalent to [`Listener::wait`] except that the 292 | /// thread will be blocked for roughly no longer than `duration`. 293 | /// 294 | /// Returns `true` if this method returned because the trigger was triggered. Returns 295 | /// `false` if it returned due to the timeout. 296 | /// 297 | /// In an async program the same can be achieved by wrapping the `Listener` in one of the 298 | /// many `Timeout` implementations that exists. 299 | pub fn wait_timeout(&self, duration: Duration) -> bool { 300 | if self.inner.complete.load(Ordering::SeqCst) { 301 | return true; 302 | } 303 | 304 | let task_guard = self 305 | .inner 306 | .tasks 307 | .lock() 308 | .expect("Some Trigger/Listener has panicked"); 309 | 310 | let _ = self 311 | .inner 312 | .condvar 313 | .wait_timeout_while(task_guard, duration, |_| { 314 | !self.inner.complete.load(Ordering::SeqCst) 315 | }) 316 | .expect("Some Trigger/Listener has panicked"); 317 | 318 | self.inner.complete.load(Ordering::SeqCst) 319 | } 320 | 321 | /// Returns true if this trigger has been triggered. 322 | pub fn is_triggered(&self) -> bool { 323 | self.inner.complete.load(Ordering::SeqCst) 324 | } 325 | } 326 | 327 | #[allow(unsafe_code)] 328 | #[cfg(test)] 329 | mod tests { 330 | use super::*; 331 | use std::future::Future; 332 | use std::sync::atomic::AtomicU8; 333 | use std::task::{RawWaker, RawWakerVTable}; 334 | 335 | #[test] 336 | fn polling_listener_keeps_only_last_waker() { 337 | let (_trigger, mut listener) = trigger(); 338 | 339 | let (waker1, waker_handle1) = create_waker(); 340 | { 341 | let mut context = Context::from_waker(&waker1); 342 | let listener = Pin::new(&mut listener); 343 | assert_eq!(listener.poll(&mut context), Poll::Pending); 344 | } 345 | assert!(waker_handle1.data.load(Ordering::SeqCst) & CLONED != 0); 346 | assert!(waker_handle1.data.load(Ordering::SeqCst) & DROPPED == 0); 347 | 348 | let (waker2, waker_handle2) = create_waker(); 349 | { 350 | let mut context = Context::from_waker(&waker2); 351 | let listener = Pin::new(&mut listener); 352 | assert_eq!(listener.poll(&mut context), Poll::Pending); 353 | } 354 | assert!(waker_handle2.data.load(Ordering::SeqCst) & CLONED != 0); 355 | assert!(waker_handle2.data.load(Ordering::SeqCst) & DROPPED == 0); 356 | assert!(waker_handle1.data.load(Ordering::SeqCst) & DROPPED != 0); 357 | } 358 | 359 | const CLONED: u8 = 0b0001; 360 | const WOKE: u8 = 0b0010; 361 | const DROPPED: u8 = 0b0100; 362 | 363 | fn create_waker() -> (Waker, Arc) { 364 | let waker_handle = Arc::new(WakerHandle { 365 | data: AtomicU8::new(0), 366 | }); 367 | let data = Arc::into_raw(waker_handle.clone()) as *const _; 368 | let raw_waker = RawWaker::new(data, &VTABLE); 369 | (unsafe { Waker::from_raw(raw_waker) }, waker_handle) 370 | } 371 | 372 | struct WakerHandle { 373 | data: AtomicU8, 374 | } 375 | 376 | impl Drop for WakerHandle { 377 | fn drop(&mut self) { 378 | println!("WakerHandle dropped"); 379 | } 380 | } 381 | 382 | const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); 383 | 384 | unsafe fn clone(data: *const ()) -> RawWaker { 385 | let waker_handle = &*(data as *const WakerHandle); 386 | waker_handle.data.fetch_or(CLONED, Ordering::SeqCst); 387 | Arc::increment_strong_count(waker_handle); 388 | RawWaker::new(data, &VTABLE) 389 | } 390 | 391 | unsafe fn wake(data: *const ()) { 392 | let waker_handle = &*(data as *const WakerHandle); 393 | waker_handle.data.fetch_or(WOKE, Ordering::SeqCst); 394 | } 395 | 396 | unsafe fn wake_by_ref(_data: *const ()) { 397 | todo!(); 398 | } 399 | 400 | unsafe fn drop(data: *const ()) { 401 | let waker_handle = &*(data as *const WakerHandle); 402 | waker_handle.data.fetch_or(DROPPED, Ordering::SeqCst); 403 | Arc::decrement_strong_count(waker_handle); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /tests/async-std.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | #[async_std::test] 5 | async fn single_trigger_first() { 6 | let (trigger, listener) = triggered::trigger(); 7 | trigger.trigger(); 8 | listener.await; 9 | } 10 | 11 | #[async_std::test] 12 | async fn single_listen_first() { 13 | let (trigger, listener) = triggered::trigger(); 14 | let thread_handle = thread::spawn(move || { 15 | // This is a thread race. But hopefully the trigger will happen after the main thread 16 | // started waiting. 17 | thread::sleep(Duration::from_secs(1)); 18 | trigger.trigger(); 19 | }); 20 | listener.await; 21 | thread_handle.join().expect("Trigger thread panic"); 22 | } 23 | 24 | #[async_std::test] 25 | async fn many_listeners_trigger_first() { 26 | let (trigger, listener) = triggered::trigger(); 27 | let listener2 = listener.clone(); 28 | 29 | trigger.trigger(); 30 | 31 | let listener3 = listener2.clone(); 32 | listener.await; 33 | listener2.await; 34 | listener3.await; 35 | } 36 | 37 | #[async_std::test] 38 | async fn many_listeners_listen_first() { 39 | let (trigger, listener) = triggered::trigger(); 40 | 41 | // Spawn a bunch of tasks that all await their own clone of the trigger's listener 42 | let mut listeners = Vec::new(); 43 | for _ in 0..103 { 44 | let listener = listener.clone(); 45 | listeners.push(async_std::task::spawn(async { 46 | listener.await; 47 | })); 48 | } 49 | 50 | // Pause a while to let most/all listener tasks run their first poll. Then trigger. 51 | thread::sleep(Duration::from_secs(1)); 52 | trigger.trigger(); 53 | 54 | // Make sure all tasks finish 55 | for listener in listeners { 56 | listener.await; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/sync-wait.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::{Duration, Instant}; 3 | 4 | #[test] 5 | fn single_trigger_first() { 6 | let (trigger, listener) = triggered::trigger(); 7 | trigger.trigger(); 8 | listener.wait(); 9 | } 10 | 11 | #[test] 12 | fn single_listen_first() { 13 | let (trigger, listener) = triggered::trigger(); 14 | let thread_handle = thread::spawn(move || { 15 | // This is a thread race. But hopefully the trigger will happen after the main thread 16 | // started waiting. 17 | thread::sleep(Duration::from_secs(1)); 18 | trigger.trigger(); 19 | }); 20 | listener.wait(); 21 | thread_handle.join().expect("Trigger thread panic"); 22 | } 23 | 24 | #[test] 25 | fn many_listeners_trigger_first() { 26 | let (trigger, listener) = triggered::trigger(); 27 | let listener2 = listener.clone(); 28 | 29 | trigger.trigger(); 30 | 31 | let listener3 = listener2.clone(); 32 | listener.wait(); 33 | listener2.wait(); 34 | listener3.wait(); 35 | } 36 | 37 | #[test] 38 | fn many_listeners_listen_first() { 39 | let (trigger, listener) = triggered::trigger(); 40 | 41 | // Spawn a bunch of tasks that all await their own clone of the trigger's listener 42 | let mut listeners = Vec::new(); 43 | for _ in 0..103 { 44 | let listener = listener.clone(); 45 | listeners.push(thread::spawn(move || { 46 | listener.wait(); 47 | Instant::now() 48 | })); 49 | } 50 | 51 | // Pause a while to let most/all listeners end up in wait(), then trigger. 52 | thread::sleep(Duration::from_secs(1)); 53 | let trigger_instant = Instant::now(); 54 | trigger.trigger(); 55 | 56 | // Make sure all tasks finish 57 | for listener in listeners { 58 | let wakeup_instant = listener.join().expect("Listener task panicked"); 59 | assert!(trigger_instant < wakeup_instant); 60 | } 61 | } 62 | 63 | #[test] 64 | fn wait_timeout() { 65 | let (_trigger, listener) = triggered::trigger(); 66 | 67 | let start = Instant::now(); 68 | let triggered = listener.wait_timeout(Duration::from_millis(100)); 69 | let elapsed = start.elapsed(); 70 | assert!(!triggered); 71 | assert!( 72 | elapsed > Duration::from_millis(99), 73 | "Timeout not within acceptable range: {:?}", 74 | elapsed 75 | ); 76 | assert!( 77 | elapsed < Duration::from_millis(300), 78 | "Timeout not within acceptable range: {:?}", 79 | elapsed 80 | ); 81 | assert!(!listener.is_triggered()) 82 | } 83 | 84 | #[test] 85 | fn wait_timeout_trigger_first() { 86 | let (trigger, listener) = triggered::trigger(); 87 | 88 | trigger.trigger(); 89 | 90 | let start = Instant::now(); 91 | let triggered = listener.wait_timeout(Duration::from_millis(1000)); 92 | assert!(triggered); 93 | let elapsed = start.elapsed(); 94 | assert!(elapsed < Duration::from_millis(10)); 95 | } 96 | 97 | #[test] 98 | fn wait_timeout_triggered_while_waiting() { 99 | let (trigger, listener) = triggered::trigger(); 100 | 101 | // Spawn a bunch of tasks that all await their own clone of the trigger's listener 102 | let mut listeners = Vec::new(); 103 | for _ in 0..103 { 104 | let listener = listener.clone(); 105 | listeners.push(thread::spawn(move || { 106 | let triggered = listener.wait_timeout(Duration::from_millis(1000)); 107 | assert!(triggered); 108 | Instant::now() 109 | })); 110 | } 111 | 112 | // Pause a while to let most/all listeners end up in wait(), then trigger. 113 | thread::sleep(Duration::from_millis(100)); 114 | 115 | let trigger_instant = Instant::now(); 116 | trigger.trigger(); 117 | 118 | // Make sure all tasks finish 119 | for listener in listeners { 120 | let wakeup_instant = listener.join().expect("Listener task panicked"); 121 | assert!(trigger_instant < wakeup_instant); 122 | } 123 | } 124 | 125 | #[test] 126 | fn is_triggered() { 127 | let (trigger, listener) = triggered::trigger(); 128 | assert!(!trigger.is_triggered()); 129 | assert!(!listener.is_triggered()); 130 | trigger.trigger(); 131 | assert!(trigger.is_triggered()); 132 | assert!(listener.is_triggered()); 133 | 134 | // Check so a second trigger does not do anything unexpected. 135 | trigger.trigger(); 136 | assert!(trigger.is_triggered()); 137 | assert!(listener.is_triggered()); 138 | } 139 | -------------------------------------------------------------------------------- /tests/tokio.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::{Duration, Instant}; 3 | 4 | #[tokio::test] 5 | async fn single_trigger_first() { 6 | let (trigger, listener) = triggered::trigger(); 7 | trigger.trigger(); 8 | listener.await; 9 | } 10 | 11 | #[tokio::test] 12 | async fn single_listen_first() { 13 | let (trigger, listener) = triggered::trigger(); 14 | let thread_handle = thread::spawn(move || { 15 | // This is a thread race. But hopefully the trigger will happen after the main thread 16 | // started waiting. 17 | thread::sleep(Duration::from_secs(1)); 18 | trigger.trigger(); 19 | }); 20 | listener.await; 21 | thread_handle.join().expect("Trigger thread panic"); 22 | } 23 | 24 | #[tokio::test] 25 | async fn many_listeners_trigger_first() { 26 | let (trigger, listener) = triggered::trigger(); 27 | let listener2 = listener.clone(); 28 | 29 | trigger.trigger(); 30 | 31 | let listener3 = listener2.clone(); 32 | listener.await; 33 | listener2.await; 34 | listener3.await; 35 | } 36 | 37 | #[tokio::test] 38 | async fn many_listeners_listen_first() { 39 | let (trigger, listener) = triggered::trigger(); 40 | 41 | // Spawn a bunch of tasks that all await their own clone of the trigger's listener 42 | let mut listeners = Vec::new(); 43 | for _ in 0..103 { 44 | let listener = listener.clone(); 45 | listeners.push(tokio::spawn(async { 46 | listener.await; 47 | Instant::now() 48 | })); 49 | } 50 | 51 | // Pause a while to let most/all listener tasks run their first poll. Then trigger. 52 | thread::sleep(Duration::from_secs(1)); 53 | let trigger_instant = Instant::now(); 54 | trigger.trigger(); 55 | 56 | // Make sure all tasks finish 57 | for listener in listeners { 58 | let wakeup_instant = listener.await.expect("Listener task panicked"); 59 | assert!(trigger_instant < wakeup_instant); 60 | } 61 | } 62 | 63 | #[tokio::test] 64 | async fn many_sync_and_async_listeners_listen_first() { 65 | let (trigger, listener) = triggered::trigger(); 66 | 67 | // Spawn a bunch of tasks that all await their own clone of the trigger's listener 68 | let mut sync_listeners = Vec::new(); 69 | let mut async_listeners = Vec::new(); 70 | for _ in 0..103 { 71 | let listener1 = listener.clone(); 72 | sync_listeners.push(thread::spawn(move || { 73 | listener1.wait(); 74 | Instant::now() 75 | })); 76 | 77 | let listener2 = listener.clone(); 78 | async_listeners.push(tokio::spawn(async { 79 | listener2.await; 80 | Instant::now() 81 | })); 82 | } 83 | 84 | // Pause a while to let most/all listener tasks start waiting. Then trigger. 85 | thread::sleep(Duration::from_secs(1)); 86 | let trigger_instant = Instant::now(); 87 | trigger.trigger(); 88 | 89 | // Make sure all tasks finish 90 | for listener in sync_listeners { 91 | let wakeup_instant = listener.join().expect("Listener task panicked"); 92 | assert!(trigger_instant < wakeup_instant); 93 | } 94 | for listener in async_listeners { 95 | let wakeup_instant = listener.await.expect("Listener task panicked"); 96 | assert!(trigger_instant < wakeup_instant); 97 | } 98 | } 99 | --------------------------------------------------------------------------------