├── extra ├── fuzzing │ ├── fuzz │ │ ├── .gitignore │ │ ├── fuzz_targets │ │ │ ├── queue.rs │ │ │ └── timers.rs │ │ └── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ ├── timers.rs │ │ └── queue.rs │ ├── Cargo.toml │ ├── run.pl │ └── pack-corpus.pl ├── stress-waker │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── bin │ │ ├── complete.rs │ │ └── edge.rs ├── README.md ├── restore_extra_rs.sh └── stress_queue.sh ├── src ├── test │ ├── queue_corpus.bin │ ├── timers_corpus.bin │ ├── extra.rs │ ├── mod.rs │ ├── regen-macro_coverage.pl │ ├── memsizes.rs │ ├── core.rs │ ├── task.rs │ ├── share.rs │ ├── macro_coverage.rs │ ├── timers_corpus.rs │ ├── queue_corpus.rs │ ├── test.rs │ ├── waker.rs │ ├── log.rs │ └── thread.rs ├── rc │ ├── fwdrc_std.rs │ ├── fwdrc_min.rs │ ├── count.rs │ ├── minrc.rs │ ├── actorrc_std.rs │ └── actorrc_packed.rs ├── cell │ ├── qcell.rs │ ├── tcell.rs │ └── tlcell.rs ├── deferrer │ ├── inline_safe.rs │ ├── thread_local_safe.rs │ ├── inline.rs │ ├── thread_local.rs │ ├── api.rs │ └── global.rs ├── queue │ └── boxed.rs ├── sync │ ├── mod.rs │ ├── channel.rs │ └── thread.rs ├── task.rs ├── fwd.rs ├── ret.rs ├── share.rs └── timers │ └── tests.rs ├── .gitignore ├── run-doc ├── run-clippy-all ├── run-test-all ├── run-clippy-all-msrv ├── run-test-all-msrv ├── examples ├── README.md ├── actor_own_anon.rs └── tutorial.rs ├── run-clippy-all-max ├── run-semver-checks ├── LICENSE-MIT ├── run-feature-combinations ├── run-measure-sizes ├── Cargo.toml ├── run-wasm-test-all ├── run-valgrind-all ├── README.md ├── run-kcov-test-all ├── run-miri-test-all ├── fixup-doc-links.pl ├── CHANGELOG.md └── LICENSE-APACHE /extra/fuzzing/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /src/test/queue_corpus.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uazu/stakker/HEAD/src/test/queue_corpus.bin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | EXPORT 4 | *.org 5 | kcov* 6 | **/*~ 7 | **/tmp* 8 | **/_* 9 | -------------------------------------------------------------------------------- /src/test/timers_corpus.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uazu/stakker/HEAD/src/test/timers_corpus.bin -------------------------------------------------------------------------------- /extra/fuzzing/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod queue; 2 | mod timers; 3 | 4 | pub use queue::fuzz_queue; 5 | pub use timers::fuzz_timers; 6 | -------------------------------------------------------------------------------- /run-doc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=== Docs.rs output" 4 | RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 5 | -------------------------------------------------------------------------------- /src/test/extra.rs: -------------------------------------------------------------------------------- 1 | //! Normally this is empty, but when extra tests are built, this is 2 | //! modified. However those changes should not be checked in. 3 | -------------------------------------------------------------------------------- /run-clippy-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./run-feature-combinations | while read FEATURES 4 | do 5 | echo === $FEATURES 6 | cargo clippy $FEATURES 7 | done 8 | -------------------------------------------------------------------------------- /run-test-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./run-feature-combinations | while read FEATURES 4 | do 5 | echo === $FEATURES 6 | cargo test $FEATURES || exit 1 7 | done 8 | -------------------------------------------------------------------------------- /extra/fuzzing/fuzz/fuzz_targets/queue.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | fuzzing::fuzz_queue(data); 6 | }); 7 | -------------------------------------------------------------------------------- /run-clippy-all-msrv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./run-feature-combinations | while read FEATURES 4 | do 5 | echo === $FEATURES 6 | cargo +1.63 clippy $FEATURES 7 | done 8 | -------------------------------------------------------------------------------- /extra/fuzzing/fuzz/fuzz_targets/timers.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | fuzzing::fuzz_timers(data); 6 | }); 7 | -------------------------------------------------------------------------------- /run-test-all-msrv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./run-feature-combinations | while read FEATURES 4 | do 5 | echo === $FEATURES 6 | cargo +1.63 test $FEATURES || exit 1 7 | done 8 | -------------------------------------------------------------------------------- /extra/fuzzing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzzing" 3 | version = "0.1.0" 4 | authors = ["Jim Peters "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | stakker = { path = "../.." } 9 | -------------------------------------------------------------------------------- /extra/stress-waker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stress-waker" 3 | version = "0.1.0" 4 | authors = ["Jim Peters "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | stakker = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | There may also be examples in other stakker-related crates. For 2 | example the **stakker_mio** crate has an example of an echo server: 3 | 4 | http://github.com/uazu/stakker_mio/tree/master/examples/ 5 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod test; 3 | 4 | mod actor; 5 | mod core; 6 | mod extra; 7 | mod log; 8 | mod macros; 9 | mod memsizes; 10 | mod queue_corpus; 11 | mod share; 12 | mod task; 13 | mod thread; 14 | mod timers_corpus; 15 | mod waker; 16 | 17 | pub(crate) mod macro_coverage; 18 | -------------------------------------------------------------------------------- /extra/README.md: -------------------------------------------------------------------------------- 1 | # Here are some extra tests 2 | 3 | These long-running or large generated tests don't make sense to run as 4 | part of unit tests, so are kept here instead. Those that need to run 5 | within the crate patch themselves in at src/tests/extra. To unpatch 6 | that run ./restore-extra-rs.sh. 7 | -------------------------------------------------------------------------------- /run-clippy-all-max: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Maximise the number of combinations of features tested, rather than 4 | # use the shorter combined list of features which minimises testing 5 | # time. 6 | 7 | ./run-feature-combinations --max | while read FEATURES 8 | do 9 | echo === $FEATURES 10 | cargo clippy $FEATURES 11 | done 12 | -------------------------------------------------------------------------------- /extra/restore_extra_rs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOP=. 4 | [ ! -d $TOP/src/test ] && { 5 | TOP=.. 6 | [ ! -d $TOP/src/test ] && { echo "Running in wrong directory"; exit 1; } 7 | } 8 | 9 | rm -r $TOP/src/test/extra 10 | 11 | cat >$TOP/src/test/extra.rs <macro_coverage.rs"; 4 | print OUT <<"EOF"; 5 | //! These functions are used in macro expansions when #[cfg(test)] is 6 | //! enabled to test coverage of macro invocations in the unit tests. 7 | 8 | EOF 9 | 10 | die unless open IN, "<../macros.rs"; 11 | while () { 12 | next unless /COVERAGE\!\((\S+)\)/; 13 | print OUT "pub fn $1() {}\n"; 14 | } 15 | 16 | die unless close OUT; 17 | -------------------------------------------------------------------------------- /run-semver-checks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install cargo-semver-checks first 4 | 5 | if [ -z "$1" ] 6 | then 7 | COMMAND="cargo semver-checks" 8 | else 9 | COMMAND="cargo semver-checks --baseline-rev $1" 10 | fi 11 | 12 | ./run-feature-combinations | while read FEATURES 13 | do 14 | echo === $FEATURES 15 | 16 | FEAT="$(echo $FEATURES | perl -pe 's/--no-default-features/--only-explicit-features/; s/,/ --features /g;')" 17 | 18 | $COMMAND $FEAT || exit 1 19 | done 20 | -------------------------------------------------------------------------------- /src/rc/fwdrc_std.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | pub(crate) struct FwdRc(Rc); 4 | 5 | impl FwdRc { 6 | #[inline] 7 | pub fn new(val: impl Fn(M) + 'static) -> Self { 8 | Self(Rc::new(val)) 9 | } 10 | 11 | #[inline] 12 | pub fn inner(&self) -> &dyn Fn(M) { 13 | &*self.0 14 | } 15 | } 16 | 17 | impl Clone for FwdRc { 18 | #[inline] 19 | fn clone(&self) -> Self { 20 | Self(self.0.clone()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /extra/fuzzing/run.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | my $target = shift; 4 | die "Usage: ./run.pl \n" unless $target =~ /^(queue|timers)$/; 5 | 6 | die unless open LOG, ">tmp-log-$target"; 7 | LOG->autoflush(1); 8 | 9 | die unless open IN, "cargo +nightly fuzz run $target -- -len_control=10000 -use_value_profile=1 2>&1 |"; 10 | while () { 11 | chomp; 12 | s/MS:.*//; 13 | print "$_\n"; 14 | if (/pulse/) { 15 | print LOG "$_\n"; 16 | } 17 | } 18 | close IN; 19 | -------------------------------------------------------------------------------- /extra/stress-waker/README.md: -------------------------------------------------------------------------------- 1 | These two tests stress the `Waker` internal implementation across 2 | threads. See the comments in each file. One checks that all wakes 3 | come through correctly when collection occurs in parallel, and the 4 | other checks that a final wake that clashes with a collection also 5 | comes through okay. 6 | 7 | These will probably need adjusting to exercise things properly on 8 | another CPU since timing is so sensitive. So if you want to try 9 | these, be ready to tweak things until you're comfortable that things 10 | have been tested sufficiently on your CPU. 11 | -------------------------------------------------------------------------------- /src/cell/qcell.rs: -------------------------------------------------------------------------------- 1 | // QCell-based cells 2 | use qcell::{QCell, QCellOwner, QCellOwnerID}; 3 | 4 | pub(crate) type ActorCell = QCell; 5 | pub(crate) type ActorCellMaker = QCellOwnerID; 6 | pub(crate) type ActorCellOwner = QCellOwner; 7 | 8 | pub(crate) fn new_actor_cell_owner() -> (ActorCellOwner, ActorCellMaker) { 9 | let owner = QCellOwner::new(); 10 | let maker = owner.id(); 11 | (owner, maker) 12 | } 13 | 14 | pub(crate) type ShareCell = QCell; 15 | pub(crate) type ShareCellOwner = QCellOwner; 16 | 17 | pub(crate) fn new_share_cell_owner() -> ShareCellOwner { 18 | QCellOwner::new() 19 | } 20 | -------------------------------------------------------------------------------- /extra/fuzzing/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "fuzz_queue-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.3" 14 | 15 | [dependencies.fuzzing] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "queue" 24 | path = "fuzz_targets/queue.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "timers" 30 | path = "fuzz_targets/timers.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /extra/fuzzing/pack-corpus.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use bytes; 4 | 5 | # This packs the corpus into a single binary file. Each item is 6 | # preceded by its length as a 16-bit big-endian number. 7 | 8 | my $target = shift; 9 | die "Usage: ./pack-corpus.pl \n" unless $target =~ /^(queue|timers)$/; 10 | 11 | die unless open OUT, ">${target}_corpus.bin"; 12 | 13 | $/ = undef; 14 | for my $fnam () { 15 | die unless open IN, "<$fnam"; 16 | my $data = ; 17 | die unless close IN; 18 | print OUT chr(length($data)>>8), chr(length($data)&255), $data; 19 | } 20 | 21 | die unless close OUT; 22 | -------------------------------------------------------------------------------- /src/cell/tcell.rs: -------------------------------------------------------------------------------- 1 | // TCell-based cells 2 | use qcell::{TCell, TCellOwner}; 3 | 4 | pub(crate) struct ActorMarker; 5 | pub(crate) type ActorCell = TCell; 6 | pub(crate) struct ActorCellMaker; 7 | pub(crate) type ActorCellOwner = TCellOwner; 8 | 9 | impl ActorCellMaker { 10 | #[inline] 11 | pub fn cell(&self, value: T) -> ActorCell { 12 | TCell::new(value) 13 | } 14 | } 15 | 16 | pub(crate) fn new_actor_cell_owner() -> (ActorCellOwner, ActorCellMaker) { 17 | (TCellOwner::new(), ActorCellMaker) 18 | } 19 | 20 | pub(crate) struct ShareMarker; 21 | pub(crate) type ShareCell = TCell; 22 | pub(crate) type ShareCellOwner = TCellOwner; 23 | 24 | pub(crate) fn new_share_cell_owner() -> ShareCellOwner { 25 | TCellOwner::new() 26 | } 27 | -------------------------------------------------------------------------------- /src/cell/tlcell.rs: -------------------------------------------------------------------------------- 1 | // TLCell-based cells 2 | use qcell::{TLCell, TLCellOwner}; 3 | 4 | pub(crate) struct ActorMarker; 5 | pub(crate) type ActorCell = TLCell; 6 | pub(crate) struct ActorCellMaker; 7 | pub(crate) type ActorCellOwner = TLCellOwner; 8 | 9 | impl ActorCellMaker { 10 | #[inline] 11 | pub fn cell(&self, value: T) -> ActorCell { 12 | TLCell::new(value) 13 | } 14 | } 15 | 16 | pub(crate) fn new_actor_cell_owner() -> (ActorCellOwner, ActorCellMaker) { 17 | (TLCellOwner::new(), ActorCellMaker) 18 | } 19 | 20 | pub(crate) struct ShareMarker; 21 | pub(crate) type ShareCell = TLCell; 22 | pub(crate) type ShareCellOwner = TLCellOwner; 23 | 24 | pub(crate) fn new_share_cell_owner() -> ShareCellOwner { 25 | TLCellOwner::new() 26 | } 27 | -------------------------------------------------------------------------------- /src/rc/fwdrc_min.rs: -------------------------------------------------------------------------------- 1 | use crate::rc::minrc::MinRc; 2 | 3 | pub(crate) struct FwdRc(MinRc); 4 | 5 | impl FwdRc { 6 | #[inline] 7 | pub fn new(val: impl Fn(M) + 'static) -> Self { 8 | // Original code: Self(MinRc::new(val)) 9 | 10 | // TODO: This is a CoerceUnsized workaround 11 | Self(MinRc::::new_with(|| { 12 | Box::new(super::minrc::MinRcBox { 13 | count: std::cell::Cell::new(1), 14 | inner: val, // <= coerce appears to occur here 15 | }) 16 | })) 17 | } 18 | 19 | #[inline] 20 | pub fn inner(&self) -> &dyn Fn(M) { 21 | self.0.inner() 22 | } 23 | } 24 | 25 | impl Clone for FwdRc { 26 | #[inline] 27 | fn clone(&self) -> Self { 28 | Self(self.0.clone()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /extra/stress-waker/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Condvar, Mutex}; 2 | 3 | /// Simple channel for sending and waiting for notification events. 4 | /// Returns (send, recv) closures. 5 | #[allow(clippy::mutex_atomic)] 6 | pub fn notify_channel() -> (impl Fn() + Send + Sync, impl FnMut() + Send + Sync) { 7 | let pair1 = Arc::new((Mutex::new(0_usize), Condvar::new())); 8 | let pair2 = pair1.clone(); 9 | let mut count = 0; 10 | ( 11 | move || { 12 | let mut lock = pair1.0.lock().unwrap(); 13 | *lock = lock.wrapping_add(1); 14 | pair1.1.notify_one(); 15 | }, 16 | move || { 17 | let mut lock = pair2.0.lock().unwrap(); 18 | while *lock == count { 19 | lock = pair2.1.wait(lock).unwrap(); 20 | } 21 | count = *lock; 22 | }, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/deferrer/inline_safe.rs: -------------------------------------------------------------------------------- 1 | use crate::queue::FnOnceQueue; 2 | use crate::{task::Task, Stakker}; 3 | use std::cell::{Cell, RefCell}; 4 | use std::mem; 5 | use std::rc::Rc; 6 | 7 | // Tried using Cell instead of RefCell, swapping out queue and 8 | // swapping it back in, but it works out way more expensive, from 9 | // looking at the assembly. 10 | 11 | #[derive(Clone)] 12 | pub struct DeferrerAux(Rc); 13 | 14 | struct Inner { 15 | queue: RefCell>, 16 | task: Cell>, 17 | } 18 | 19 | impl DeferrerAux { 20 | pub(crate) fn new() -> Self { 21 | Self(Rc::new(Inner { 22 | queue: RefCell::new(FnOnceQueue::new()), 23 | task: Cell::new(None), 24 | })) 25 | } 26 | 27 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 28 | mem::swap(&mut *self.0.queue.borrow_mut(), queue) 29 | } 30 | 31 | #[inline] 32 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 33 | self.0.queue.borrow_mut().push(f); 34 | } 35 | 36 | #[inline] 37 | pub fn task_replace(&self, task: Option) -> Option { 38 | self.0.task.replace(task) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2020 Jim Peters 2 | Copyright (c) 2020 `stakker` crate contributors 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software 11 | is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice 15 | shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /examples/actor_own_anon.rs: -------------------------------------------------------------------------------- 1 | //! The ActorOwnAnon example from: 2 | //! https://docs.rs/stakker/*/stakker/struct.ActorOwnAnon.html 3 | 4 | use stakker::*; 5 | use std::time::Instant; 6 | 7 | struct Cat; 8 | impl Cat { 9 | fn init(_: CX![]) -> Option { 10 | Some(Self) 11 | } 12 | fn sound(&mut self, _: CX![]) { 13 | println!("Miaow"); 14 | } 15 | } 16 | 17 | struct Dog; 18 | impl Dog { 19 | fn init(_: CX![]) -> Option { 20 | Some(Self) 21 | } 22 | fn sound(&mut self, _: CX![]) { 23 | println!("Woof"); 24 | } 25 | } 26 | 27 | // This function doesn't know whether it's getting a cat or a dog, 28 | // but it can still call it and drop it when it has finished 29 | pub fn call_and_drop(sound: Fwd<()>, _own: ActorOwnAnon) { 30 | fwd!([sound]); 31 | } 32 | 33 | fn main() { 34 | let mut stakker = Stakker::new(Instant::now()); 35 | let s = &mut stakker; 36 | 37 | let cat = actor!(s, Cat::init(), ret_nop!()); 38 | call_and_drop(fwd_to!([cat], sound() as ()), cat.anon()); 39 | 40 | let dog = actor!(s, Dog::init(), ret_nop!()); 41 | call_and_drop(fwd_to!([dog], sound() as ()), dog.anon()); 42 | 43 | s.run(Instant::now(), false); 44 | } 45 | -------------------------------------------------------------------------------- /run-feature-combinations: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | my @list; 4 | push @list, ''; 5 | 6 | sub add { 7 | my @tmp = (); 8 | for my $a (@_) { 9 | for my $f (@list) { 10 | push @tmp, "$f$a"; 11 | } 12 | } 13 | @list = @tmp; 14 | } 15 | 16 | if (@ARGV > 0 && $ARGV[0] eq '--max') { 17 | add('', 'multi-thread,'); 18 | add('', 'multi-stakker,'); 19 | add('', 'logger,'); 20 | add('', 'no-unsafe-queue,'); 21 | add('', 'no-unsafe,'); 22 | add('', 'inline-deferrer,'); 23 | add('', 'inter-thread,'); 24 | } else { 25 | # multi-stakker implies multi-thread; logger is independent 26 | add('', 'multi-thread,', 'multi-stakker,logger,'); 27 | 28 | # no-unsafe implies no-unsafe-queue 29 | add('', 'no-unsafe-queue,', 'no-unsafe,'); 30 | 31 | # These features are independent and can be switched together 32 | add('', 'inline-deferrer,inter-thread,'); 33 | } 34 | 35 | # Sort by length which biases towards testing single features before 36 | # combined features 37 | for (sort { length($a) <=> length($b) } @list) { 38 | s/,$//; 39 | if ($_ eq '') { 40 | print("--no-default-features\n"); 41 | } else { 42 | print("--no-default-features --features $_\n"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/deferrer/thread_local_safe.rs: -------------------------------------------------------------------------------- 1 | use crate::queue::FnOnceQueue; 2 | use crate::{task::Task, Stakker}; 3 | use std::cell::{Cell, RefCell}; 4 | use std::marker::PhantomData; 5 | use std::mem; 6 | 7 | thread_local!( 8 | static QUEUE: RefCell> = RefCell::new(FnOnceQueue::new()); 9 | static TASK: Cell> = const { Cell::new(None) }; 10 | ); 11 | 12 | // Use *const to make it !Send and !Sync 13 | #[derive(Clone)] 14 | pub struct DeferrerAux(PhantomData<*const u8>); 15 | 16 | impl DeferrerAux { 17 | pub(crate) fn new() -> Self { 18 | Self(PhantomData) 19 | } 20 | 21 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 22 | // Does nothing if it's being destroyed 23 | let _ = QUEUE.try_with(move |qref| mem::swap(&mut *qref.borrow_mut(), queue)); 24 | } 25 | 26 | #[inline] 27 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 28 | // Does nothing if it's being destroyed 29 | let _ = QUEUE.try_with(|qref| qref.borrow_mut().push(f)); 30 | } 31 | 32 | #[inline] 33 | pub fn task_replace(&self, task: Option) -> Option { 34 | // Return None if it's being destroyed 35 | TASK.try_with(move |tref| tref.replace(task)) 36 | .unwrap_or(None) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /run-measure-sizes: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "(This intentionally leaks some memory in order to measure it)" 4 | echo "" 5 | 6 | for xx in \ 7 | "" \ 8 | "--features no-unsafe" \ 9 | "--all-features" 10 | do 11 | echo "=== Testing: $xx" 12 | 13 | # Run cargo test twice. First builds and tests. Second skips the 14 | # build (already done) and just runs the test under valgrind. We only 15 | # want to valgrind the test, not building-related stuff. 16 | cargo test --lib test::memsizes::actor_size $xx 2>&1 17 | 18 | STAKKER_ENABLE_TEST_MEMSIZES=1 \ 19 | valgrind \ 20 | --tool=memcheck \ 21 | --trace-children=yes \ 22 | --leak-check=full \ 23 | --show-leak-kinds=definite,indirect \ 24 | --undef-value-errors=no \ 25 | cargo test --lib test::memsizes::actor_size $xx 2>&1 | 26 | perl -e ' 27 | while (<>) { 28 | if (/ indirectly lost /) { 29 | print $_; 30 | while (<>) { 31 | last unless /by |at /; 32 | if (/stakker/) { 33 | print $_; 34 | while (<>) { 35 | last unless /stakker/; 36 | last if /test/; 37 | print $_; 38 | } 39 | last; 40 | } 41 | } 42 | } 43 | }' 44 | 45 | done 46 | -------------------------------------------------------------------------------- /src/test/memsizes.rs: -------------------------------------------------------------------------------- 1 | //! Check sizes of objects 2 | //! 3 | //! This test intentionally mem::forgets some allocations in order to 4 | //! have the allocation size show up in valgrind. 5 | 6 | use crate::time::Instant; 7 | use crate::{actor_new, ret_nop, ret_panic, ret_to, ActorOwn, Stakker, StopCause, CX}; 8 | 9 | #[allow(dead_code)] 10 | struct Actor0([u8; 0]); 11 | #[allow(dead_code)] 12 | struct Actor1000([u8; 1000]); 13 | #[allow(dead_code)] 14 | struct Actor2000([u8; 2000]); 15 | 16 | impl Actor0 { 17 | #[inline(never)] 18 | fn test(&self, _cx: CX![], _cause: Option) {} 19 | } 20 | 21 | struct Actors { 22 | _a0: ActorOwn, 23 | _a1: ActorOwn, 24 | _a2: ActorOwn, 25 | } 26 | 27 | test_fn!( 28 | fn actor_size() { 29 | let mut stakker = Stakker::new(Instant::now()); 30 | let s = &mut stakker; 31 | let _a0 = actor_new!(s, Actor0, ret_nop!()); 32 | let _a1 = actor_new!(s, Actor1000, ret_to!([_a0], test() as (StopCause))); 33 | let _a2 = actor_new!(s, Actor2000, ret_panic!("Shouldn't have died")); 34 | 35 | if std::env::var("STAKKER_ENABLE_TEST_MEMSIZES").is_ok() { 36 | // Forget a boxed struct holding the references. This means that 37 | // they show up as indirect lost blocks in valgrind 38 | std::mem::forget(Box::new(Actors { _a0, _a1, _a2 })); 39 | } 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /src/queue/boxed.rs: -------------------------------------------------------------------------------- 1 | /// Queue of `FnOnce(&mut Stakker)` items waiting for execution. 2 | pub(crate) struct FnOnceQueue { 3 | #[allow(clippy::type_complexity)] 4 | vec: Vec>, 5 | } 6 | 7 | impl FnOnceQueue { 8 | pub fn new() -> Self { 9 | Self { vec: Vec::new() } 10 | } 11 | 12 | /// Check that internal implementation assumptions are valid 13 | pub fn sanity_check() {} 14 | 15 | /// Push a `FnOnce` callback onto the queue. 16 | #[inline] 17 | pub fn push(&mut self, value: impl FnOnce(&mut S) + 'static) { 18 | self.vec.push(Box::new(value)); 19 | } 20 | 21 | /// Push a boxed `FnOnce` onto the queue 22 | #[inline] 23 | pub fn push_box(&mut self, value: Box) { 24 | self.vec.push(value); 25 | } 26 | 27 | /// Execute all the `FnOnce` instances found on this queue, 28 | /// passing them the given context object ref. Leaves the queue 29 | /// empty, but with the same backing memory still allocated to 30 | /// aid in cache reuse. 31 | pub fn execute(&mut self, context: &mut S) { 32 | for f in self.vec.drain(..) { 33 | f(context); 34 | } 35 | } 36 | 37 | /// Test whether this queue is empty 38 | pub fn is_empty(&self) -> bool { 39 | self.vec.is_empty() 40 | } 41 | } 42 | 43 | impl Default for FnOnceQueue { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/core.rs: -------------------------------------------------------------------------------- 1 | use crate::time::{Instant, SystemTime}; 2 | use crate::*; 3 | use std::rc::Rc; 4 | use std::time::Duration; 5 | 6 | test_fn!( 7 | fn test_times() { 8 | let t0 = Instant::now(); 9 | let t1 = t0 + Duration::from_secs(1); 10 | let s0 = SystemTime::now(); 11 | let s1 = s0 + Duration::from_secs(2); 12 | 13 | let mut stakker = Stakker::new(t0); 14 | let s = &mut stakker; 15 | s.set_systime(Some(s0)); 16 | 17 | assert_eq!(t0, s.start_instant()); 18 | assert_eq!(t0, s.now()); 19 | assert_eq!(s0, s.systime()); 20 | 21 | s.set_systime(Some(s1)); 22 | s.run(t1, false); 23 | assert_eq!(t0, s.start_instant()); 24 | assert_eq!(t1, s.now()); 25 | assert_eq!(s1, s.systime()); 26 | } 27 | ); 28 | 29 | test_fn!( 30 | fn anymap() { 31 | let mut stakker = Stakker::new(Instant::now()); 32 | let s = &mut stakker; 33 | 34 | s.anymap_set(Rc::new(42u8)); 35 | s.anymap_set(Rc::new(-17i8)); 36 | assert_eq!(42, *s.anymap_get::>()); 37 | assert_eq!(-17, *s.anymap_get::>()); 38 | 39 | s.anymap_set(Rc::new(24u8)); 40 | assert_eq!(24, *s.anymap_get::>()); 41 | assert_eq!(-17, *s.anymap_get::>()); 42 | 43 | s.anymap_unset::>(); 44 | assert_eq!(None, s.anymap_try_get::>()); 45 | assert_eq!(-17, *s.anymap_get::>()); 46 | 47 | assert_eq!(None, s.anymap_try_get::>()); 48 | assert_eq!(None, s.anymap_try_get::>()); 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /src/test/task.rs: -------------------------------------------------------------------------------- 1 | use crate::task::*; 2 | use crate::time::Instant; 3 | use crate::*; 4 | use std::cell::RefCell; 5 | use std::pin::Pin; 6 | use std::rc::Rc; 7 | 8 | // This tests the Task mechanism, but without needing async/await 9 | test_fn!( 10 | fn task_test() { 11 | let now = Instant::now(); 12 | let mut stakker = Stakker::new(now); 13 | let s = &mut stakker; 14 | 15 | let progress0 = Rc::new(RefCell::new(Vec::new())); 16 | 17 | struct MyTask { 18 | deferrer: Deferrer, 19 | count: u32, 20 | progress: Rc>>, 21 | } 22 | impl TaskTrait for MyTask { 23 | fn resume(mut self: Pin<&mut Self>, core: &mut Core) { 24 | self.progress.borrow_mut().push(self.count); 25 | self.count += 1; 26 | if self.count < 4 { 27 | let mut task = Task::from_context(&self.deferrer).unwrap(); 28 | call!([core], |s| task.resume(s)); 29 | } 30 | } 31 | } 32 | 33 | let my_task = MyTask { 34 | deferrer: s.deferrer(), 35 | count: 0, 36 | progress: progress0.clone(), 37 | }; 38 | let mut task = Task::new(s, my_task); 39 | task.resume(s); 40 | 41 | // Task reference should now be held in item on queue 42 | drop(task); 43 | 44 | // Executing the queue causes the task to be repeatedly resumed 45 | // and queued until it is done 46 | s.run(now, false); 47 | 48 | assert_eq!(vec![0, 1, 2, 3], *progress0.borrow()); 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /src/rc/count.rs: -------------------------------------------------------------------------------- 1 | use crate::actor::State; 2 | 3 | // If the count reaches the maximum value, it locks there and never 4 | // decreases, possibly leaking the ActorBox memory if there are any 5 | // weak reference cycles. However the only way to get a count that 6 | // high is by the coder leaking actor references, which means there 7 | // are much bigger problems. Leaking memory is safe in any case. 8 | #[derive(Copy, Clone)] 9 | pub(crate) struct CountAndState(usize); 10 | 11 | const COUNT_SHIFT: u32 = 2; 12 | const COUNT_INC: usize = 1 << COUNT_SHIFT; 13 | const COUNT_MASK: usize = !(COUNT_INC - 1); 14 | 15 | impl CountAndState { 16 | pub fn new() -> Self { 17 | // Start at count of 0 18 | Self(State::Prep as usize) 19 | } 20 | 21 | #[inline] 22 | pub fn inc(self) -> Self { 23 | if self.0 >= COUNT_MASK { 24 | self 25 | } else { 26 | Self(self.0 + COUNT_INC) 27 | } 28 | } 29 | 30 | #[inline] 31 | pub fn dec(self) -> (Self, bool) { 32 | if self.0 < COUNT_INC || self.0 >= COUNT_MASK { 33 | (self, false) 34 | } else { 35 | let val = self.0 - COUNT_INC; 36 | (Self(val), val < COUNT_INC) 37 | } 38 | } 39 | 40 | #[inline] 41 | pub fn set_state(self, state: State) -> Self { 42 | Self((self.0 & COUNT_MASK) | (state as usize)) 43 | } 44 | 45 | #[inline] 46 | pub fn is_prep(self) -> bool { 47 | (self.0 & !COUNT_MASK) == (State::Prep as usize) 48 | } 49 | 50 | #[inline] 51 | pub fn is_zombie(self) -> bool { 52 | (self.0 & !COUNT_MASK) == (State::Zombie as usize) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/deferrer/inline.rs: -------------------------------------------------------------------------------- 1 | use crate::queue::FnOnceQueue; 2 | use crate::{task::Task, Stakker}; 3 | use std::cell::{Cell, UnsafeCell}; 4 | use std::mem; 5 | use std::rc::Rc; 6 | 7 | // Unsafe version which gets rid of RefCell overheads 8 | 9 | #[derive(Clone)] 10 | pub struct DeferrerAux(Rc); 11 | 12 | struct Inner { 13 | queue: UnsafeCell>, 14 | task: Cell>, 15 | } 16 | 17 | impl DeferrerAux { 18 | pub(crate) fn new() -> Self { 19 | Self(Rc::new(Inner { 20 | queue: UnsafeCell::new(FnOnceQueue::new()), 21 | task: Cell::new(None), 22 | })) 23 | } 24 | 25 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 26 | // Safety: Safe because the methods called within the unsafe 27 | // region do not do any operations which will attempt to get 28 | // another mutable reference to the queue 29 | 30 | // Clippy false positive: They don't overlap, guaranteed by 31 | // `&mut` param outside of `unsafe` 32 | #[allow(clippy::swap_ptr_to_ref)] 33 | unsafe { 34 | mem::swap(&mut *self.0.queue.get(), queue) 35 | } 36 | } 37 | 38 | #[inline] 39 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 40 | // Safety: Safe because the methods called within the unsafe 41 | // region do not do any operations which will attempt to get 42 | // another mutable reference to the queue 43 | unsafe { (*self.0.queue.get()).push(f) }; 44 | } 45 | 46 | #[inline] 47 | pub fn task_replace(&self, task: Option) -> Option { 48 | self.0.task.replace(task) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/share.rs: -------------------------------------------------------------------------------- 1 | use crate::time::Instant; 2 | use crate::*; 3 | 4 | test_fn!( 5 | fn share() { 6 | let now = Instant::now(); 7 | let mut stakker = Stakker::new(now); 8 | let s = &mut stakker; 9 | 10 | let s1 = Share::new(s, 12345_u32); 11 | assert_eq!(*s1.ro(s), 12345); 12 | 13 | *s1.rw(s) += 11111; 14 | assert_eq!(*s1.ro(s), 23456); 15 | 16 | let s2 = Share::new(s, 98765_u32); 17 | assert_eq!(*s2.ro(s), 98765); 18 | 19 | let (p1, p2) = s.share_rw2(&s1, &s2); 20 | std::mem::swap(p1, p2); 21 | assert_eq!(*s1.ro(s), 98765); 22 | assert_eq!(*s2.ro(s), 23456); 23 | 24 | // Use a different type to check share_rw3 doesn't mind 25 | let s3 = Share::new(s, 13579_u64); 26 | assert_eq!(*s3.ro(s), 13579); 27 | 28 | let (p1, p2, p3) = s.share_rw3(&s1, &s2, &s3); 29 | *p3 += (*p2 * 3 + *p1) as u64; 30 | assert_eq!(*s3.ro(s), 13579 + 23456 * 3 + 98765); 31 | } 32 | ); 33 | 34 | test_fn!( 35 | fn share_weak() { 36 | let now = Instant::now(); 37 | let mut stakker = Stakker::new(now); 38 | let s = &mut stakker; 39 | 40 | let s1 = Share::new(s, 12345_u32); 41 | assert_eq!(*s1.ro(s), 12345); 42 | assert_eq!(s1.strong_count(), 1); 43 | assert_eq!(s1.weak_count(), 0); 44 | 45 | let w1 = s1.downgrade(); 46 | assert_eq!(s1.strong_count(), 1); 47 | assert_eq!(s1.weak_count(), 1); 48 | assert_eq!(w1.strong_count(), 1); 49 | assert_eq!(w1.weak_count(), 1); 50 | assert_eq!(*w1.upgrade().unwrap().ro(s), 12345); 51 | 52 | drop(s1); 53 | assert_eq!(w1.strong_count(), 0); 54 | assert!(w1.upgrade().is_none()); 55 | } 56 | ); 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stakker" 3 | version = "0.2.13" 4 | authors = ["Jim Peters "] 5 | edition = "2021" 6 | rust-version = "1.63.0" 7 | 8 | description = "A lightweight low-level single-threaded actor runtime" 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | 12 | repository = "https://github.com/uazu/stakker" 13 | documentation = "https://docs.rs/stakker" 14 | homepage = "https://uazu.github.io/stakker" 15 | 16 | keywords = [ "actor", "runtime", "async", "pony", "erlang" ] 17 | categories = [ "asynchronous", "concurrency", "data-structures" ] 18 | 19 | [badges] 20 | maintenance = { status = "actively-developed" } 21 | 22 | [dependencies] 23 | static_assertions = "1.0" 24 | qcell = "0.5" 25 | slab = "0.4" 26 | 27 | [target.'cfg(target_family = "wasm")'.dependencies] 28 | web-time = "1.1.0" 29 | 30 | # For more details on features, see crate docs. Features are additive 31 | # in cargo, so summing features must result a less restrictive, more 32 | # flexible configuration (even if less efficient), or else things will 33 | # break when different crates using Stakker are combined in a build. 34 | 35 | [features] 36 | default = ["inter-thread"] 37 | 38 | # Disable all unsafe code and compile with #[forbid(unsafe_code)] 39 | no-unsafe = [] 40 | 41 | # Disable the unsafe fast FnOnce queue code 42 | no-unsafe-queue = [] 43 | 44 | # Allow Stakker to run in more than one thread at a time 45 | multi-thread = [] 46 | 47 | # Allow more than one Stakker to run in each thread 48 | multi-stakker = [] 49 | 50 | # Force use of the inline Deferrer 51 | inline-deferrer = [] 52 | 53 | # Enable inter-thread operations: Waker, PipedThread 54 | inter-thread = [] 55 | 56 | # Enable core logger support (Stakker::set_logger()) 57 | logger = [] 58 | 59 | # Ignored for backwards-compatibility. `anymap` is now always enabled 60 | anymap = [] 61 | -------------------------------------------------------------------------------- /run-wasm-test-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die() { echo "ABORT: $*"; exit 1; } 4 | 5 | [ -d tmp ] && die "tmp/ already exists" 6 | 7 | # Copy whole crate to tmp/ and modify it in place to test with 8 | # `wasm_bindgen_test` instead of `cargo test`. Install `wasm-pack` 9 | # using `cargo install wasm-pack` 10 | 11 | mkdir tmp && 12 | cp -ra src Cargo.toml Cargo.lock tmp/ && 13 | cd tmp || die "Failed copying crate" 14 | 15 | echo "[dev-dependencies]" >>Cargo.toml 16 | echo "wasm-bindgen-test = \"0.3.0\"" >>Cargo.toml 17 | 18 | find -name "*.rs" | 19 | while read xx 20 | do 21 | perl -i -pe 's/#\[test\]/#[wasm_bindgen_test]/g;' $xx 22 | grep -E 'wasm_bindgen_test|test_fn!' $xx >&/dev/null && { 23 | perl -g -i -pe ' 24 | # Insert `use` for `wasm_bindgen_test` 25 | s/(mod tests \{\s*)use/${1}use wasm_bindgen_test::*;\nuse/s || 26 | s/(\n|^)use/${1}use wasm_bindgen_test::*;\nuse/s; 27 | 28 | # Cannot test panics, so cut those tests out 29 | s/ #\[should_panic\](.*?{)(.*?)(})/$1$3/sg; 30 | ' $xx 31 | } 32 | done 33 | 34 | # Threads tests won't pass on WASM, so cut them out 35 | rm src/test/thread.rs 36 | touch src/test/thread.rs 37 | 38 | # Ignore some warnings caused by the testing modifications 39 | perl -g -i -pe ' 40 | s/^/#![allow(unused_imports)]\n/s; 41 | s/^/#![allow(dead_code)]\n/s; 42 | ' src/lib.rs 43 | 44 | ../run-feature-combinations | while read FEATURES 45 | do 46 | echo === $FEATURES 47 | case $FEATURES in 48 | *multi-*) 49 | wasm-pack test --node $FEATURES || exit 1 50 | ;; 51 | *) 52 | echo "SKIPPING: not supported on this platform" 53 | ;; 54 | esac 55 | done 56 | 57 | echo SUCCESS 58 | cd .. 59 | rm -r tmp 60 | exit 0 61 | -------------------------------------------------------------------------------- /src/deferrer/thread_local.rs: -------------------------------------------------------------------------------- 1 | use crate::queue::FnOnceQueue; 2 | use crate::{task::Task, Stakker}; 3 | use std::cell::{Cell, UnsafeCell}; 4 | use std::marker::PhantomData; 5 | use std::mem; 6 | 7 | thread_local!( 8 | #[allow(clippy::missing_const_for_thread_local)] 9 | static QUEUE: UnsafeCell> = UnsafeCell::new(FnOnceQueue::new()); 10 | static TASK: Cell> = const { Cell::new(None) }; 11 | ); 12 | 13 | // Use *const to make it !Send and !Sync 14 | #[derive(Clone)] 15 | pub struct DeferrerAux(PhantomData<*const u8>); 16 | 17 | impl DeferrerAux { 18 | pub(crate) fn new() -> Self { 19 | Self(PhantomData) 20 | } 21 | 22 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 23 | // Safety: Safe because the methods called within the unsafe 24 | // region do not do any operations which will attempt to get 25 | // another mutable reference to the queue 26 | 27 | // Clippy false positive: They don't overlap, guaranteed by 28 | // `&mut` param outside of `unsafe` 29 | #[allow(clippy::swap_ptr_to_ref)] 30 | unsafe { 31 | // Does nothing if it's being destroyed 32 | let _ = QUEUE.try_with(move |qref| mem::swap(&mut *qref.get(), queue)); 33 | } 34 | } 35 | 36 | #[inline] 37 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 38 | // Safety: Safe because the methods called within the unsafe 39 | // region do not do any operations which will attempt to get 40 | // another mutable reference to the queue 41 | unsafe { 42 | // Does nothing if it's being destroyed 43 | let _ = QUEUE.try_with(|qref| (*qref.get()).push(f)); 44 | } 45 | } 46 | 47 | #[inline] 48 | pub fn task_replace(&self, task: Option) -> Option { 49 | // Return None if it's being destroyed 50 | TASK.try_with(move |tref| tref.replace(task)) 51 | .unwrap_or(None) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /run-valgrind-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Filters out non-stakker-related valgrind backtrace lines 4 | filter() { 5 | perl -e ' 6 | while (<>) { 7 | if (/ at 0x/) { 8 | print $_ if /stakker/; 9 | while (<>) { 10 | last unless / by 0x/; 11 | if (/stakker/) { 12 | print $_; 13 | } 14 | } 15 | print $_; 16 | } else { 17 | print $_; 18 | } 19 | }' 20 | } 21 | 22 | cat >tmp-valgrind-suppressions.txt </dev/null 2>&1 || { 51 | # If it fails, repeat and show output, then fail 52 | cargo test --lib $FEATURES 2>&1 53 | echo FAILED 54 | exit 1 55 | } 56 | 57 | # --log-file=log/valgrind_%p \ 58 | # --gen-suppressions=all \ 59 | # --show-leak-kinds=definite,indirect \ 60 | valgrind \ 61 | --tool=memcheck \ 62 | --trace-children=yes \ 63 | --leak-check=full \ 64 | --suppressions=tmp-valgrind-suppressions.txt \ 65 | --show-leak-kinds=definite \ 66 | --undef-value-errors=no \ 67 | cargo test --lib $FEATURES 2>&1 | filter 68 | done 2>&1 | tee tmp-valgrind.log 69 | 70 | rm tmp-valgrind-suppressions.txt 71 | 72 | echo "" 73 | echo "SUMMARY: (see tmp-valgrind.log for details)" 74 | fgrep definitely tmp-valgrind.log 75 | -------------------------------------------------------------------------------- /src/test/macro_coverage.rs: -------------------------------------------------------------------------------- 1 | //! These functions are used in macro expansions when #[cfg(test)] is 2 | //! enabled to test coverage of macro invocations in the unit tests. 3 | 4 | pub fn actor_0() {} 5 | pub fn actor_1() {} 6 | pub fn actor_new() {} 7 | pub fn actor_2() {} 8 | pub fn actor_3() {} 9 | pub fn actor_in_slab_0() {} 10 | pub fn actor_in_slab_1() {} 11 | pub fn actor_in_slab_2() {} 12 | pub fn actor_in_slab_3() {} 13 | pub fn query_0() {} 14 | pub fn generic_call_0() {} 15 | pub fn generic_call_1() {} 16 | pub fn generic_call_2() {} 17 | pub fn generic_call_3() {} 18 | pub fn generic_call_4() {} 19 | pub fn call_0() {} 20 | pub fn call_1() {} 21 | pub fn lazy_0() {} 22 | pub fn lazy_1() {} 23 | pub fn idle_0() {} 24 | pub fn idle_1() {} 25 | pub fn after_0() {} 26 | pub fn after_1() {} 27 | pub fn at_0() {} 28 | pub fn at_1() {} 29 | pub fn timer_max_0() {} 30 | pub fn timer_max_1() {} 31 | pub fn timer_min_0() {} 32 | pub fn timer_min_1() {} 33 | pub fn fwd_0() {} 34 | pub fn fwd_1() {} 35 | pub fn ret_0() {} 36 | pub fn ret_1() {} 37 | pub fn generic_fwd_0() {} 38 | pub fn generic_fwd_1() {} 39 | pub fn generic_fwd_2() {} 40 | pub fn generic_fwd_3() {} 41 | pub fn generic_fwd_4() {} 42 | pub fn generic_fwd_5() {} 43 | pub fn generic_fwd_6() {} 44 | pub fn generic_fwd_7() {} 45 | pub fn generic_fwd_8() {} 46 | pub fn fwd_to_0() {} 47 | pub fn fwd_to_1() {} 48 | pub fn fwd_to_2() {} 49 | pub fn fwd_panic_0() {} 50 | pub fn fwd_do_0() {} 51 | pub fn fwd_nop_0() {} 52 | pub fn ret_to_0() {} 53 | pub fn ret_to_1() {} 54 | pub fn ret_to_2() {} 55 | pub fn ret_to_3() {} 56 | pub fn ret_some_to_0() {} 57 | pub fn ret_some_to_1() {} 58 | pub fn ret_some_to_2() {} 59 | pub fn ret_do_0() {} 60 | pub fn ret_some_do_0() {} 61 | pub fn ret_panic_0() {} 62 | pub fn ret_nop_0() {} 63 | pub fn ret_shutdown_0() {} 64 | pub fn ret_fail_0() {} 65 | pub fn ret_fail_1() {} 66 | pub fn ret_fail_2() {} 67 | pub fn ret_failthru_0() {} 68 | pub fn ret_failthru_1() {} 69 | pub fn ret_failthru_2() {} 70 | pub fn fail_0() {} 71 | pub fn fail_1() {} 72 | pub fn fail_2() {} 73 | pub fn stop_0() {} 74 | pub fn kill_0() {} 75 | pub fn kill_1() {} 76 | pub fn kill_2() {} 77 | -------------------------------------------------------------------------------- /extra/fuzzing/src/timers.rs: -------------------------------------------------------------------------------- 1 | use stakker::*; 2 | use std::cell::RefCell; 3 | use std::collections::HashSet; 4 | use std::rc::Rc; 5 | use std::time::{Duration, Instant}; 6 | 7 | // Add timers and remove items from queue, checking that all of them 8 | // come out correctly, and that timing is as expected 9 | 10 | pub fn fuzz_timers(data: &[u8]) { 11 | if data.is_empty() { 12 | return; 13 | } 14 | let mut now = Instant::now(); 15 | let mut stakker = Stakker::new(now); 16 | let s = &mut stakker; 17 | let mut seq = 1; 18 | let expecting = Rc::new(RefCell::new(HashSet::new())); 19 | 20 | // pull_inc value of 0 means never pull, 255 means pull after 21 | // every push. This means that the fuzzer can exercise moving 22 | // data out of the queue whilst adding as well. 23 | let pull_inc = data[0] as u32; 24 | let mut pull_val = 128; 25 | 26 | for b in &data[1..] { 27 | seq += 1; 28 | // Range is roughly exponential from 0 to 491520, which with 29 | // ~0.853s unit is 0 to ~116 hours. This means we test quite 30 | // a few cases of over 9 hours, which causes different code to 31 | // run, since the maximum delay supported within the queue is 32 | // ~9 hours. 33 | let dur = ((*b & 15) as u64) << ((*b >> 4) as u32); 34 | // Use non-exact value to exercise internal time format 35 | // conversion and manipulation 36 | let dur = Duration::from_micros(dur * 853439); 37 | let expiry = now + dur; 38 | expecting.borrow_mut().insert(seq); 39 | let exp = expecting.clone(); 40 | let seq2 = seq; 41 | at!(expiry, [s], |s| { 42 | let now = s.now(); 43 | // Resolution of internal time format is just less than 44 | // 17us, so expect the expiry to run within range of 45 | // expiry to expiry+17us 46 | assert!(now >= expiry); 47 | assert!(now < expiry + Duration::from_micros(17)); 48 | assert!(exp.borrow_mut().remove(&seq2)); 49 | }); 50 | 51 | pull_val += pull_inc; 52 | if pull_val >= 255 { 53 | pull_val -= 255; 54 | now = s.next_expiry().unwrap(); 55 | s.run(now, false); 56 | } 57 | } 58 | 59 | // Run the rest of the timers 60 | while let Some(next) = s.next_expiry() { 61 | now = next; 62 | s.run(now, false); 63 | } 64 | 65 | // Check that they have all completed 66 | assert!(expecting.borrow_mut().is_empty()); 67 | } 68 | -------------------------------------------------------------------------------- /src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cross-thread support 2 | //! 3 | //! # `Waker` primitive 4 | //! 5 | //! [`Waker`] is the primitive that allows all the other cross-thread 6 | //! types to integrate with **Stakker**. It allows a wakeup to be 7 | //! scheduled in the **Stakker** thread. That wakeup handler (a 8 | //! [`Fwd`]) can then check the channel or mutex or whatever and 9 | //! perform whatever actions are necessary. 10 | //! 11 | //! # Unbounded channels 12 | //! 13 | //! - For receiving messages into a **Stakker** thread via an 14 | //! unbounded queue, use [`Channel`]. 15 | //! 16 | //! - For sending messages to another thread via an unbounded channel, 17 | //! use whatever unbounded channel is supported by the runtime of that 18 | //! thread, for example `tokio::sync::mpsc::unbounded_channel()`. 19 | //! 20 | //! # Bounded channels 21 | //! 22 | //! There is no high-level support for bounded channels yet. Raise an 23 | //! issue if this is required. 24 | //! 25 | //! - For a bounded receiving channel, it would be straightforward to 26 | //! wrap the native bounded channel of the sending runtime with some 27 | //! code to use a [`Waker`] to wake **Stakker** to receive the 28 | //! messages. 29 | //! 30 | //! - For a bounded sending channel, it requires that the other 31 | //! runtime supports some means of calling a [`Waker`] when the 32 | //! channel has free space. Possibly this could also be done with a 33 | //! wrapper. 34 | //! 35 | //! - Alternatively bounded channels could be layered on top of 36 | //! unbounded channels by doing some form of token exchange (like 37 | //! token ring), i.e. a sender uses up one of its logical tokens to 38 | //! send, and the logical token has to be returned before it can be 39 | //! used to send again. This just means keeping a count of tokens and 40 | //! having certain messages logically pass a token one way or the 41 | //! other. A limited number of tokens would be introduced into the 42 | //! system on initialisation. 43 | //! 44 | //! # Worker threads 45 | //! 46 | //! [`PipedThread`] provides a simple and safe way to start a thread 47 | //! to offload CPU-intensive or blocking tasks. This thread may in 48 | //! turn use `rayon` or whatever to parallelize work if necessary. 49 | //! 50 | //! [`Channel`]: ../sync/struct.Channel.html 51 | //! [`Fwd`]: ../struct.Fwd.html 52 | //! [`PipedThread`]: ../sync/struct.PipedThread.html 53 | //! [`Waker`]: ../sync/struct.Waker.html 54 | 55 | mod channel; 56 | pub(crate) mod thread; 57 | pub(crate) mod waker; 58 | pub use channel::{Channel, ChannelGuard}; 59 | pub use thread::{PipedLink, PipedThread}; 60 | pub use waker::Waker; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A lightweight low-level single-threaded actor runtime 2 | 3 | [![license:MIT/Apache-2.0][1]](https://github.com/uazu/stakker) 4 | [![github:uazu/stakker][2]](https://github.com/uazu/stakker) 5 | [![crates.io:stakker][3]](https://crates.io/crates/stakker) 6 | [![docs.rs:stakker][4]](https://docs.rs/stakker) 7 | [![uazu.github.io:stakker][5]](https://uazu.github.io/stakker/) 8 | 9 | [1]: https://img.shields.io/badge/license-MIT%2FApache--2.0-blue 10 | [2]: https://img.shields.io/badge/github-uazu%2Fstakker-brightgreen 11 | [3]: https://img.shields.io/badge/crates.io-stakker-red 12 | [4]: https://img.shields.io/badge/docs.rs-stakker-purple 13 | [5]: https://img.shields.io/badge/uazu.github.io-stakker-yellow 14 | 15 | **Stakker** is designed to be layered on top of whatever event loop 16 | the user prefers to use. It aims to take maximum advantage of Rust's 17 | compile-time checks and optimisations. 18 | 19 | ### Documentation 20 | 21 | See the [crate documentation](http://docs.rs/stakker) and the [Stakker 22 | Guide and Design Notes](https://uazu.github.io/stakker/) 23 | 24 | # License 25 | 26 | This project is licensed under either the Apache License version 2 or 27 | the MIT license, at your option. (See 28 | [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT)). 29 | 30 | ### Contribution 31 | 32 | Unless you explicitly state otherwise, any contribution intentionally 33 | submitted for inclusion in this crate by you, as defined in the 34 | Apache-2.0 license, shall be dual licensed as above, without any 35 | additional terms or conditions. 36 | 37 | ### Maintenance approach 38 | 39 | You're very welcome to try to break this code! I intend to conform to 40 | Rust safety conventions, including on internal interfaces. Any 41 | unsound behaviour that can be shown to exist will be treated as a 42 | serious bug, and I will endeavour to find a solution as soon as 43 | reasonably possible. 44 | 45 | I reserve the right to (metaphorically) go off to a mountain-top cave 46 | to consider issues in depth, to make the right decision without being 47 | rushed. 48 | 49 | Most of the design decisions in this software have had a lot of 50 | consideration, with many different approaches tried and discarded 51 | before arriving at the current solution. The current implementations 52 | have been rewritten and refactored and minimised to get to the current 53 | state. So I'd ask that any requests for changes to how things are 54 | done be accompanied by some reasonably in-depth justification, such as 55 | example use-cases that require the change, or some other discussion of 56 | why that change would be a good one. I prefer to keep the code tight, 57 | so I might need to refactor PRs, or reimplement them a different way. 58 | -------------------------------------------------------------------------------- /src/deferrer/api.rs: -------------------------------------------------------------------------------- 1 | use super::DeferrerAux; 2 | use crate::queue::FnOnceQueue; 3 | use crate::{task::Task, Stakker}; 4 | 5 | /// Defer queue which can be accessed without a [`Core`] ref 6 | /// 7 | /// [`Deferrer`] provides a way to defer calls from contexts which 8 | /// don't have access to a [`Core`] reference. The most obvious of 9 | /// these is a `Drop` handler. So when a `Drop` handler needs to 10 | /// queue cleanup actions, keep a [`Deferrer`] instance in the 11 | /// structure and then those operations can be deferred without 12 | /// problem. (The size of a [`Deferrer`] instance is 0 bytes for the 13 | /// global or thread-local implementations, or a `usize` for inline.) 14 | /// 15 | /// Obtain a [`Deferrer`] instance using [`Core::deferrer`]. To use 16 | /// it, call the [`Deferrer::defer`] method with a closure which 17 | /// performs the operation required. Note that all [`Actor`] 18 | /// instances have a [`Deferrer`] built in which can be used from 19 | /// outside the actor as [`Actor::defer`]. 20 | /// 21 | /// Note that in final shutdown of a **Stakker** system, deferring an 22 | /// action after the main loop has stopped running the [`Stakker`] 23 | /// queues or after the [`Stakker`] instance has been dropped will be 24 | /// accepted but the call will never execute. So make sure that all 25 | /// actors are terminated before the last run of the [`Stakker`] 26 | /// queues if you need cleanup actions to complete. 27 | /// 28 | /// [`Actor::defer`]: struct.Actor.html#method.defer 29 | /// [`Actor`]: struct.Actor.html 30 | /// [`Core::deferrer`]: struct.Core.html#method.deferrer 31 | /// [`Core`]: struct.Core.html 32 | /// [`Deferrer::defer`]: struct.Deferrer.html#method.defer 33 | /// [`Deferrer`]: struct.Deferrer.html 34 | /// [`Stakker`]: struct.Stakker.html 35 | pub struct Deferrer(super::DeferrerAux); 36 | 37 | impl Deferrer { 38 | #[inline] 39 | pub(crate) fn new() -> Self { 40 | Self(DeferrerAux::new()) 41 | } 42 | 43 | // Swap out the queue 44 | #[inline] 45 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 46 | self.0.swap_queue(queue); 47 | } 48 | 49 | // Set the queue and discard the old value 50 | #[inline] 51 | pub(crate) fn set_queue(&self, mut queue: FnOnceQueue) { 52 | self.0.swap_queue(&mut queue); 53 | } 54 | 55 | /// Defer a call to be executed in the main loop at the next 56 | /// opportunity. 57 | #[inline] 58 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 59 | self.0.defer(f); 60 | } 61 | 62 | /// Replace the current task, returning the old value. 63 | #[inline] 64 | pub(crate) fn task_replace(&self, task: Option) -> Option { 65 | self.0.task_replace(task) 66 | } 67 | } 68 | 69 | impl Clone for Deferrer { 70 | fn clone(&self) -> Self { 71 | Self(self.0.clone()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /run-kcov-test-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die() { echo "ABORT: $*"; exit 1; } 4 | 5 | TARGET=${CARGO_TARGET_DIR:-./target} 6 | 7 | # Check macro.rs COVERAGE tags 8 | perl -e ' 9 | die unless open IN, ") { 12 | if (/COVERAGE\!\((.*?)\)/) { 13 | die "Duplicate COVERAGE tag: $1" if exists $tags{$1}; 14 | $tags{$1} = 1; 15 | } 16 | } 17 | die unless close IN; 18 | print((0 + keys %tags) . " macro.rs COVERAGE tags\n"); 19 | die unless open IN, ") { 21 | if (/pub\s+fn\s+(\S*?)\(\)/) { 22 | delete $tags{$1}; 23 | } 24 | } 25 | exit 0 if 0 == keys %tags; 26 | print "Coverage tag without entry in macro_coverage.rs: $_" for keys %tags; 27 | exit 1; 28 | ' || die COVERAGE tag problem 29 | 30 | 31 | # Run test with all feature combinations 32 | rm -r kcov >/dev/null 2>&1 33 | mkdir kcov || die "Can't creat kcov/" 34 | 35 | COUNT=1000 36 | ./run-feature-combinations | while read FEATURES 37 | do 38 | ID=${COUNT#1} 39 | let COUNT=COUNT+1 40 | echo === $ID $FEATURES 41 | 42 | # Depending on the Rust version, test executables may be 43 | # dumped in $TARGET/debug or $TARGET/debug/deps 44 | rm -r $(find $TARGET/debug -name "stakker-*") >/dev/null 2>&1 45 | 46 | # Can't use --no-run because it excludes some of the tests. 47 | # Need "link-dead-code" to get all the source (including 48 | # source without coverage) to show up. Need "opt-level=0" or 49 | # otherwise "link-dead-code" fails with linking problems. 50 | RUSTFLAGS="-C link-dead-code -C codegen-units=1 -C opt-level=0" \ 51 | cargo test $FEATURES >kcov/test-$ID 2>&1 || 52 | die "cargo test failed; see kcov/test-$ID" 53 | rm kcov/test-$ID 54 | 55 | set "" 56 | set $(find $TARGET/debug -name "stakker-*" -type f -executable) 57 | #was: ls $TARGET/debug/stakker-* | egrep 'stakker-[A-Fa-f0-9]+$' 58 | 59 | [ -z "$1" ] && die "Can't find test binary in $TARGET/debug" 60 | [ ! -z "$2" ] && die "Found more than one test binary in $TARGET/debug" 61 | EXE="$1" 62 | 63 | TESTS=$(echo $(find src -name "*.rs" | fgrep /test | fgrep -v macro_coverage) | perl -pe 's/ /,/g') 64 | 65 | mkdir kcov/out-$ID 66 | kcov --verify \ 67 | --include-pattern=stakker/src \ 68 | --exclude-pattern=$TESTS \ 69 | kcov/out-$ID $EXE || 70 | die "kcov failed" 71 | done 72 | 73 | echo "=== MERGING reports to kcov/out/ ..." 74 | mkdir kcov/out 75 | kcov --merge kcov/out kcov/out-* 76 | 77 | # Check that no files have been excluded. Skip src/lib.rs, because 78 | # there is no executable code in it. 79 | ( find src -name "*.rs" | 80 | fgrep -v test | 81 | perl -pe 's|.*/||'; 82 | echo macro_coverage.rs; # Always include this 83 | ) | sort >kcov/list-source 84 | ls kcov/out/kcov-merged/*.rs.*.html | 85 | perl -pe 's/\.rs.*/.rs/; s|.*/||;' | 86 | sort >kcov/list-covered 87 | cmp kcov/list-source kcov/list-covered >/dev/null 2>&1 || { 88 | echo "WARNING: Some files not included in report:" $(comm -23 kcov/list-source kcov/list-covered) 89 | } 90 | -------------------------------------------------------------------------------- /run-miri-test-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # To install MIRI, see: https://github.com/rust-lang/miri 4 | 5 | echo "Sometime between 2022-07-01 and 2022-10-01 MIRI stopped exposing its vtable" 6 | echo "simulation, so any tests using the queue/flat.rs implementation will fail." 7 | echo "Those tests are now skipped. This means that it's no longer possible to check" 8 | echo "for undefined behaviour in that queue implementation, unless you use an old " 9 | echo "nightly. (It does pass with 2022-07-01 nightlies, though.)" 10 | echo "" 11 | 12 | export MIRIFLAGS=-Zmiri-disable-isolation 13 | 14 | ./run-feature-combinations | while read FEATURES 15 | do 16 | echo === $FEATURES 17 | 18 | case "$FEATURES" in 19 | *no-unsafe*) ;; 20 | *) 21 | echo "*** SKIPPING test that uses queue/flat.rs because MIRI no longer exposes vtable simulation ***" 22 | continue;; 23 | esac 24 | case "$FEATURES" in 25 | *multi-*) 26 | # These tests can run in parallel 27 | cargo +nightly miri test $FEATURES || exit 1 28 | continue;; 29 | esac 30 | 31 | # Since 1.67 `RUST_TEST_THREADS=1` no longer runs all tests in a 32 | # single thread. See: 33 | # and 34 | # . The general 35 | # workaround for this problem is to run a worker thread and send 36 | # the tests to that thread to run. However running a worker 37 | # thread makes MIRI complain that we haven't waited for that 38 | # thread to finish, and it seems impossible to do that. So 39 | # instead run MIRI tests one at a time using --list and --exact. 40 | 41 | COMMAND="cargo +nightly miri test --lib $FEATURES" 42 | 43 | echo "*** Applying work-around for testing single-threaded tests ***" 44 | FLAG_FAILED=/tmp/stakker-run-test-all-aux-$$-flag 45 | FLAG_TESTED=/tmp/stakker-run-test-all-aux-$$-tested 46 | TMPOUT=/tmp/stakker-run-test-all-aux-$$-out 47 | rm -f $FLAG_FAILED >&/dev/null 48 | rm -f $FLAG_TESTED >&/dev/null 49 | $COMMAND -- --list 2>/dev/null | 50 | grep ': test' | 51 | perl -pe 's/: test$//;' | 52 | while read xx 53 | do 54 | if $COMMAND -- --exact "$xx" >$TMPOUT 2>&1 55 | then 56 | grep "test .* [.][.][.]" $TMPOUT || cat $TMPOUT 57 | else 58 | echo "=== $xx" 59 | cat $TMPOUT 60 | touch $FLAG_FAILED 61 | fi 62 | rm $TMPOUT 63 | touch $FLAG_TESTED 64 | done 65 | [ ! -f $FLAG_TESTED ] && { 66 | $COMMAND -- --list 67 | echo "NO TESTS WERE FOUND. Check run-miri-test-all script. Maybe output of --list has changed?" 68 | rm -f $FLAG_FAILED >&/dev/null 69 | exit 1 70 | } 71 | rm -f $FLAG_TESTED >&/dev/null 72 | [ -f $FLAG_FAILED ] && { 73 | rm $FLAG_FAILED 74 | exit 1 75 | } 76 | # Doc-tests are run one-per-process and are unaffected 77 | cargo +nightly miri test --doc $FEATURES || exit 1 78 | done 79 | -------------------------------------------------------------------------------- /src/rc/minrc.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::marker::PhantomData; 3 | use std::ptr::NonNull; 4 | 5 | /// Minimum Rc for our purposes. Only uses a couple of `unsafe`. Has 6 | /// a single reference count which takes care of dropping the 7 | /// contained instance when it goes to zero. This could be regarded 8 | /// as a strong count if it is the only count, or as a weak count if 9 | /// there is another count which deletes the contents earlier. 10 | pub(crate) struct MinRc { 11 | ptr: NonNull>, 12 | // Use *const to make it !Send and !Sync 13 | phantomdata: PhantomData<*const T>, 14 | } 15 | 16 | pub(super) struct MinRcBox { 17 | pub count: Cell, 18 | pub inner: T, 19 | } 20 | 21 | // TODO: Handle DSTs like Rc does, but CoerceUnsized is not stable. 22 | // So right now we have to use a work-around via `new_with`. 23 | // https://github.com/rust-lang/rust/issues/27732 24 | // 25 | //impl, U: ?Sized> CoerceUnsized> for MinRc {} 26 | 27 | impl MinRc { 28 | #[inline] 29 | pub fn new(val: T) -> Self { 30 | Self { 31 | ptr: NonNull::new(Box::into_raw(Box::new(MinRcBox { 32 | count: Cell::new(1), 33 | inner: val, 34 | }))) 35 | .unwrap(), 36 | phantomdata: PhantomData, 37 | } 38 | } 39 | } 40 | 41 | impl MinRc { 42 | // TODO: This is a CoerceUnsized work-around 43 | #[inline] 44 | pub(super) fn new_with(cb: impl FnOnce() -> Box>) -> Self { 45 | Self { 46 | ptr: NonNull::new(Box::into_raw(cb())).unwrap(), 47 | phantomdata: PhantomData, 48 | } 49 | } 50 | 51 | #[inline] 52 | fn rcbox(&self) -> &MinRcBox { 53 | // Safe because MinRcBox is not freed until last ref has gone 54 | unsafe { self.ptr.as_ref() } 55 | } 56 | 57 | #[inline] 58 | pub fn inner(&self) -> &T { 59 | &self.rcbox().inner 60 | } 61 | } 62 | 63 | impl Clone for MinRc { 64 | fn clone(&self) -> Self { 65 | let rcbox = self.rcbox(); 66 | rcbox.count.replace(rcbox.count.get().saturating_add(1)); 67 | Self { 68 | ptr: self.ptr, 69 | phantomdata: PhantomData, 70 | } 71 | } 72 | } 73 | 74 | impl Drop for MinRc { 75 | fn drop(&mut self) { 76 | // Locks on max value. So this leaks the memory, which is 77 | // safe. This can only happen if someone has already leaked 78 | // ~2**64 references to this instance (on 64-bit). 79 | let rcbox = self.rcbox(); 80 | let (count, went_to_zero) = match rcbox.count.get() { 81 | 1 => (0, true), 82 | 0 => (0, false), 83 | usize::MAX => (usize::MAX, false), 84 | v => (v - 1, false), 85 | }; 86 | rcbox.count.replace(count); 87 | 88 | if went_to_zero { 89 | // Safe because we drop this just once, when the final 90 | // reference is released 91 | unsafe { drop(Box::from_raw(self.ptr.as_mut())) } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/timers_corpus.rs: -------------------------------------------------------------------------------- 1 | use crate::time::Instant; 2 | use crate::*; 3 | use std::cell::RefCell; 4 | use std::collections::HashSet; 5 | use std::rc::Rc; 6 | use std::time::Duration; 7 | 8 | // This uses a snapshot of a small fuzzing corpus that gives full 9 | // coverage of fixed timer addition and execution in `cargo fuzz` 10 | 11 | // TODO: Re-enable MIRI test when timers have been switched to use 12 | // N-ary heap. This can't run under MIRI right now. It runs very 13 | // slowly using a huge amount of memory until eventually it crashes 14 | // trying to allocate even more memory. 15 | 16 | test_fn!( 17 | #[cfg_attr(miri, ignore)] 18 | fn run() { 19 | let data = std::include_bytes!("timers_corpus.bin"); 20 | let mut p = &data[..]; 21 | while !p.is_empty() { 22 | let len = ((p[0] as usize) << 8) + (p[1] as usize); 23 | fuzz_timers(&p[2..2 + len]); 24 | p = &p[2 + len..]; 25 | } 26 | } 27 | ); 28 | 29 | // Add timers and remove items from queue, checking that all of them 30 | // come out correctly, and that timing is as expected 31 | 32 | pub fn fuzz_timers(data: &[u8]) { 33 | if data.is_empty() { 34 | return; 35 | } 36 | let mut now = Instant::now(); 37 | let mut stakker = Stakker::new(now); 38 | let s = &mut stakker; 39 | let mut seq = 1; 40 | let expecting = Rc::new(RefCell::new(HashSet::new())); 41 | 42 | // pull_inc value of 0 means never pull, 255 means pull after 43 | // every push. This means that the fuzzer can exercise moving 44 | // data out of the queue whilst adding as well. 45 | let pull_inc = data[0] as u32; 46 | let mut pull_val = 128; 47 | 48 | for b in &data[1..] { 49 | seq += 1; 50 | // Range is roughly exponential from 0 to 491520, which with 51 | // ~0.853s unit is 0 to ~116 hours. This means we test quite 52 | // a few cases of over 9 hours, which causes different code to 53 | // run, since the maximum delay supported within the queue is 54 | // ~9 hours. 55 | let dur = ((*b & 15) as u64) << ((*b >> 4) as u32); 56 | // Use non-exact value to exercise internal time format 57 | // conversion and manipulation 58 | let dur = Duration::from_micros(dur * 853439); 59 | let expiry = now + dur; 60 | expecting.borrow_mut().insert(seq); 61 | let exp = expecting.clone(); 62 | let seq2 = seq; 63 | at!(expiry, [s], |s| { 64 | let now = s.now(); 65 | // Resolution of internal time format is just less than 66 | // 17us, so expect the expiry to run within range of 67 | // expiry to expiry+17us 68 | assert!(now >= expiry); 69 | assert!(now < expiry + Duration::from_micros(17)); 70 | assert!(exp.borrow_mut().remove(&seq2)); 71 | }); 72 | 73 | pull_val += pull_inc; 74 | if pull_val >= 255 { 75 | pull_val -= 255; 76 | now = s.next_expiry().unwrap(); 77 | s.run(now, false); 78 | } 79 | } 80 | 81 | // Run the rest of the timers 82 | while let Some(next) = s.next_expiry() { 83 | now = next; 84 | s.run(now, false); 85 | } 86 | 87 | // Check that they have all completed 88 | assert!(expecting.borrow_mut().is_empty()); 89 | } 90 | -------------------------------------------------------------------------------- /extra/stress-waker/src/bin/complete.rs: -------------------------------------------------------------------------------- 1 | //! There are two operations on the waker bitmap tree: setting a bit, 2 | //! and collecting the bits (zeroing them in the process). These two 3 | //! operations may race against one another. Collecting works down 4 | //! the tree atomically clearing bits, and setting a bit goes the 5 | //! other way, working **up** the tree atomically setting bits. It is 6 | //! expected that these two operations will occasionally cross over. 7 | //! When they do, the code is designed such that it should not lose a 8 | //! wake, and such that it should give nothing worse than a spurious 9 | //! wake-up. This test verifies that that occurs. 10 | 11 | use stakker::Stakker; 12 | use std::cell::RefCell; 13 | use std::collections::VecDeque; 14 | use std::rc::Rc; 15 | use std::thread; 16 | use std::time::Instant; 17 | use stress_waker::notify_channel; 18 | 19 | /// Number of wakers to use 20 | const COUNT: i32 = 1000; 21 | /// Number of times to repeat the test 22 | const REPEAT: i32 = 10000; 23 | 24 | fn main() { 25 | let now = Instant::now(); 26 | let mut stakker = Stakker::new(now); 27 | let s = &mut stakker; 28 | let (wake_tx, mut wake_rx) = notify_channel(); 29 | s.set_poll_waker(wake_tx); 30 | 31 | let (run_tx, mut run_rx) = notify_channel(); 32 | 33 | let mut wakers = Vec::new(); 34 | let woken = Rc::new(RefCell::new(VecDeque::new())); 35 | for id in 0..COUNT { 36 | let woken = woken.clone(); 37 | wakers.push(s.waker(move |_, deleted| { 38 | if !deleted { 39 | woken.borrow_mut().push_back(id); 40 | } 41 | })); 42 | } 43 | 44 | thread::spawn(move || loop { 45 | run_rx(); 46 | for w in &wakers { 47 | w.wake(); 48 | } 49 | }); 50 | 51 | for _ in 0..REPEAT { 52 | // Tell the thread to run all the wakers 53 | run_tx(); 54 | 55 | let mut count = 0; 56 | while count < COUNT { 57 | wake_rx(); 58 | let before = woken.borrow().len(); 59 | s.poll_wake(); 60 | s.run(now, false); 61 | count += (woken.borrow().len() - before) as i32; 62 | woken.borrow_mut().push_back(-2); 63 | } 64 | 65 | // Check and report results 66 | let mut woken = std::mem::replace(&mut *woken.borrow_mut(), VecDeque::new()); 67 | let mut set = vec![1u8; COUNT as usize]; 68 | for id in &woken { 69 | if *id >= 0 { 70 | set[*id as usize] = 0; 71 | } 72 | } 73 | for id in 0..COUNT { 74 | if set[id as usize] != 0 { 75 | println!("ERROR: Missing wake: {}", id); 76 | std::process::exit(1); 77 | } 78 | } 79 | 80 | while let Some(fr) = woken.pop_front() { 81 | if fr < 0 { 82 | print!("WAIT "); 83 | } else { 84 | let mut to = fr; 85 | while woken.front() == Some(&(to + 1)) { 86 | to = woken.pop_front().unwrap(); 87 | } 88 | if to == fr { 89 | print!("{} ", fr); 90 | } else { 91 | print!("{}-{} ", fr, to); 92 | } 93 | } 94 | } 95 | println!(); 96 | } 97 | println!("SUCCESS"); 98 | } 99 | -------------------------------------------------------------------------------- /extra/stress-waker/src/bin/edge.rs: -------------------------------------------------------------------------------- 1 | //! This attempts to test the edge case of the final wake crossing 2 | //! over with a collect. Since this is extremely time-sensitive, the 3 | //! parameters in this file will need to be adjusted for the tested 4 | //! CPU, especially RANGE. 5 | //! 6 | //! What we're aiming for in the output is to see a good number of 7 | //! lines ending with "WAKE 999", which means that the last wake 8 | //! happened at almost the same time as the previous collect. So if 9 | //! there was a bug, there was a good chance of it being exposed 10 | //! (e.g. by losing the last "wake" and getting stuck in an endless 11 | //! wait). 12 | //! 13 | //! The output can be piped to `sort | uniq -c | sort -rn | less` to 14 | //! see that the "WAIT 999" case is getting exercised enough. 15 | 16 | use stakker::Stakker; 17 | use std::cell::RefCell; 18 | use std::collections::VecDeque; 19 | use std::ops::Range; 20 | use std::rc::Rc; 21 | use std::sync::atomic::{AtomicUsize, Ordering}; 22 | use std::sync::Arc; 23 | use std::thread; 24 | use std::time::Instant; 25 | use stress_waker::notify_channel; 26 | 27 | /// Number of wakers to use 28 | const COUNT: i32 = 1000; 29 | /// Number of times to repeat the test 30 | const REPEAT: i32 = 1000000; 31 | /// Waker-count range over which to test. Will need adjusting for 32 | /// different CPUs, and for release or debug builds 33 | const RANGE: Range = 180..200; 34 | 35 | fn main() { 36 | let now = Instant::now(); 37 | let mut stakker = Stakker::new(now); 38 | let s = &mut stakker; 39 | 40 | let estimate = Arc::new(AtomicUsize::new(20)); 41 | let estimate_2 = estimate.clone(); 42 | 43 | let (wake_tx, mut wake_rx) = notify_channel(); 44 | s.set_poll_waker(wake_tx); 45 | 46 | let (run_tx, mut run_rx) = notify_channel(); 47 | 48 | let mut wakers = Vec::new(); 49 | let woken = Rc::new(RefCell::new(VecDeque::new())); 50 | for id in 0..COUNT { 51 | let woken = woken.clone(); 52 | wakers.push(s.waker(move |_, deleted| { 53 | if !deleted { 54 | woken.borrow_mut().push_back(id); 55 | } 56 | })); 57 | } 58 | 59 | thread::spawn(move || loop { 60 | run_rx(); 61 | let mut it = wakers.iter(); 62 | for _ in 0..estimate_2.load(Ordering::SeqCst) { 63 | if let Some(w) = it.next() { 64 | w.wake(); 65 | } 66 | } 67 | // Always wake #999 at the end 68 | wakers.last().unwrap().wake(); 69 | }); 70 | 71 | let mut est_it = RANGE.clone(); 72 | for _ in 0..REPEAT { 73 | let est = if let Some(est) = est_it.next() { 74 | est 75 | } else { 76 | est_it = RANGE.clone(); 77 | est_it.next().unwrap() 78 | }; 79 | estimate.store(est, Ordering::SeqCst); 80 | 81 | // Tell the thread to run the wakers 82 | run_tx(); 83 | 84 | while woken.borrow().back() != Some(&999) { 85 | woken.borrow_mut().push_back(-2); 86 | wake_rx(); 87 | s.poll_wake(); 88 | s.run(now, false); 89 | } 90 | 91 | // Check and report results 92 | let mut woken = std::mem::replace(&mut *woken.borrow_mut(), VecDeque::new()); 93 | while let Some(fr) = woken.pop_front() { 94 | if fr < 0 { 95 | print!("WAIT "); 96 | } else { 97 | let mut to = fr; 98 | while woken.front() == Some(&(to + 1)) { 99 | to = woken.pop_front().unwrap(); 100 | } 101 | if to == fr { 102 | print!("{} ", fr); 103 | } else { 104 | print!("{}-{} ", fr, to); 105 | } 106 | } 107 | } 108 | println!(); 109 | } 110 | println!("SUCCESS"); 111 | } 112 | -------------------------------------------------------------------------------- /extra/stress_queue.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The intention is to test all sizes and alignments crossing the 4 | # break-point where the code decides to allocate a new queue block. 5 | 6 | TOP=. 7 | [ ! -d $TOP/src/test ] && { 8 | TOP=.. 9 | [ ! -d $TOP/src/test ] && { echo "Running in wrong directory"; exit 1; } 10 | } 11 | 12 | mkdir $TOP/src/test/extra 13 | FILE=$TOP/src/test/extra/tmp_stress_queue.rs 14 | 15 | echo "mod tmp_stress_queue; // DON'T CHECK THIS CHANGE IN" >$TOP/src/test/extra.rs 16 | 17 | echo "Writing test to $FILE" 18 | ( 19 | cat <) { 31 | match (v.first(), v.last()) { 32 | (Some(false), Some(true)) => (), 33 | _ => panic!("Didn't cover break-point"), 34 | } 35 | } 36 | EOF 37 | 38 | CALL1="" 39 | CALL2="" 40 | for A in 1 2 4 8 16 32 64; do 41 | CALL1="$CALL1 check_vec(vec![" 42 | CALL2="$CALL2 check_vec(vec![" 43 | for N in {112..128} #{2016..2032} 44 | do 45 | let L=N*8 46 | AL="${A}_${L}" 47 | CALL1="$CALL1 check_empty_a$AL()," 48 | CALL2="$CALL2 check_single_a$AL()," 49 | cat <, v: A$AL) { 61 | queue.push(move |c| accept_a$AL(c, v)); 62 | } 63 | 64 | #[inline(never)] 65 | fn check_empty_a$AL() -> bool { 66 | // First push directly from 0 67 | let mut confirm = Confirm(0xF); 68 | let mut queue = FnOnceQueue::::new(); 69 | assert_eq!(queue.cap(), 0); 70 | let mut v = A$AL([0; $L]); 71 | v.0[0] = 123; 72 | v.0[$L-1] = 234; 73 | push_a$AL(&mut queue, v); 74 | let rv = queue.cap() == 2048; 75 | println!("A$AL on empty -> {}", queue.cap()); 76 | queue.execute(&mut confirm); 77 | assert_eq!(confirm.0, 0xF2); 78 | rv 79 | } 80 | 81 | #[inline(never)] 82 | fn check_single_a$AL() -> bool { 83 | // Then push with a small item already on the queue 84 | let mut confirm = Confirm(0xF); 85 | let mut queue = FnOnceQueue::::new(); 86 | queue.push(|c| c.push(0x1)); 87 | assert_eq!(queue.cap(), 1024); 88 | let mut v = A$AL([0; $L]); 89 | v.0[0] = 123; 90 | v.0[$L-1] = 234; 91 | push_a$AL(&mut queue, v); 92 | let rv = queue.cap() == 2048; 93 | println!("A$AL on non-empty -> {}", queue.cap()); 94 | queue.execute(&mut confirm); 95 | assert_eq!(confirm.0, 0xF12); 96 | rv 97 | } 98 | EOF 99 | done 100 | CALL1="$CALL1 ]);" 101 | CALL2="$CALL2 ]);" 102 | done 103 | 104 | cat <$FILE 113 | 114 | # This builds and tests 115 | cargo test --lib tmp_stress_queue -- --nocapture 116 | 117 | # Retest with valgrind (after build has been done) for any other issues 118 | SUPP=$PWD/tmp-valgrind-suppressions.txt 119 | cat >$SUPP <&1 144 | 145 | rm $SUPP 146 | -------------------------------------------------------------------------------- /src/deferrer/global.rs: -------------------------------------------------------------------------------- 1 | use crate::queue::FnOnceQueue; 2 | use crate::{task::Task, Stakker}; 3 | use std::marker::PhantomData; 4 | use std::mem; 5 | use std::mem::MaybeUninit; 6 | use std::sync::Once; 7 | use std::thread::ThreadId; 8 | 9 | // ONCE protects initialisation of LOCKED_TO_THREAD and QUEUE 10 | static ONCE: Once = Once::new(); 11 | 12 | // Thread onto which this QUEUE is locked. Once initialised it 13 | // remains constant. 14 | static mut LOCKED_TO_THREAD: Option = None; 15 | 16 | // QUEUE is only ever accessed by the thread registered in 17 | // LOCKED_TO_THREAD, which is guaranteed by the checks below. 18 | // 19 | // Due to problems making FnOnceQueue::new() const in queue/boxed.rs 20 | // (due to compiler complaining about `&mut` in the type created by 21 | // Vec::new()), we can't initialise this here. MaybeUninit is used 22 | // because all the code that accesses it can only run after 23 | // LOCKED_TO_THREAD and QUEUE are initialised, so at that point we can 24 | // assume that it is initialised without doing any checks, for 25 | // efficiency. 26 | static mut QUEUE: MaybeUninit> = MaybeUninit::uninit(); 27 | 28 | // TASK is only ever accessed by the thread registered in 29 | // LOCKED_TO_THREAD, which is guaranteed by the checks below. 30 | static mut TASK: Option = None; 31 | 32 | // Use *const to make it !Send and !Sync 33 | #[derive(Clone)] 34 | pub struct DeferrerAux(PhantomData<*const u8>); 35 | 36 | impl DeferrerAux { 37 | pub(crate) fn new() -> Self { 38 | // Safety: Once LOCKED_TO_THREAD is set to a ThreadId, QUEUE 39 | // and TASK will only be accessed from that thread. This is 40 | // guaranteed by this check and the fact that DeferrerAux is 41 | // !Send and !Sync. So there are no data races on QUEUE or 42 | // TASK. QUEUE is initialised only once, and this constructor 43 | // guarantees that it is initialised before use. 44 | let tid = std::thread::current().id(); 45 | #[allow(static_mut_refs)] 46 | unsafe { 47 | ONCE.call_once(|| { 48 | LOCKED_TO_THREAD = Some(tid); 49 | QUEUE.write(FnOnceQueue::new()); 50 | }); 51 | assert_eq!( 52 | LOCKED_TO_THREAD, 53 | Some(tid), 54 | "Attempted to create another Stakker instance on a different thread. {}", 55 | "Enable crate feature `multi-thread` to allow this." 56 | ); 57 | } 58 | Self(PhantomData) 59 | } 60 | 61 | pub(crate) fn swap_queue(&self, queue: &mut FnOnceQueue) { 62 | // Safety: The running thread has exclusive access to QUEUE; 63 | // see above. The operation doesn't call into any method 64 | // which might also attempt to access the same global. 65 | // This code cannot be called before QUEUE is initialised. 66 | unsafe { 67 | #[allow(static_mut_refs)] 68 | mem::swap(queue, QUEUE.assume_init_mut()); 69 | } 70 | } 71 | 72 | #[inline] 73 | pub fn defer(&self, f: impl FnOnce(&mut Stakker) + 'static) { 74 | // Safety: The running thread has exclusive access to QUEUE; 75 | // see above. The operation doesn't call into any method 76 | // which might also attempt to access the same global. 77 | // This code cannot be called before QUEUE is initialised. 78 | unsafe { 79 | #[allow(static_mut_refs)] 80 | QUEUE.assume_init_mut().push(f); 81 | }; 82 | } 83 | 84 | #[inline] 85 | pub fn task_replace(&self, task: Option) -> Option { 86 | // Safety: The running thread has exclusive access to TASK; 87 | // see above. The operation doesn't call into any method 88 | // which might also attempt to access the same global. 89 | // 90 | // This used to be `&mut TASK`, but now we have to jump 91 | // through some hoops. 92 | unsafe { mem::replace(&mut *std::ptr::addr_of_mut!(TASK), task) } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /extra/fuzzing/src/queue.rs: -------------------------------------------------------------------------------- 1 | use stakker::*; 2 | use std::time::Instant; 3 | 4 | // The idea is to push random sequences of calls of different lengths 5 | // onto the queue, interspersed with calls to run the queue. 6 | 7 | pub fn fuzz_queue(data: &[u8]) { 8 | let now = Instant::now(); 9 | let mut stakker = Stakker::new(now); 10 | let s = &mut stakker; 11 | 12 | let acc = actor!(s, Acc::init(), ret_nop![]); 13 | s.run(now, false); 14 | 15 | let mut expected = Vec::new(); 16 | let mut seq = 1; 17 | for b in data { 18 | seq += 1; 19 | if *b < 4 { 20 | // If pure-random, would flush queue 1 in 64 bytes, which 21 | // biases queue-length to about 64*(8+(8+16+24+32)/4) == 22 | // 1792. So this helps the fuzzer start to explore around 23 | // the 1024-2048 range. 24 | s.run(now, false); 25 | } else { 26 | let n = *b & 15; 27 | expected.push(n); 28 | match n { 29 | 0 => Acc::push_c0(&acc, seq), 30 | 1 => Acc::push_c1(&acc, seq), 31 | 2 => Acc::push_c2(&acc, seq), 32 | 3 => Acc::push_c3(&acc, seq), 33 | 4 => Acc::push_c4(&acc, seq), 34 | 5 => Acc::push_c5(&acc, seq), 35 | 6 => Acc::push_c6(&acc, seq), 36 | 7 => Acc::push_c7(&acc, seq), 37 | 8 => Acc::push_c8(&acc, seq), 38 | 9 => Acc::push_c9(&acc, seq), 39 | 10 => Acc::push_c10(&acc, seq), 40 | 11 => Acc::push_c11(&acc, seq), 41 | 12 => Acc::push_c12(&acc, seq), 42 | 13 => Acc::push_c13(&acc, seq), 43 | 14 => Acc::push_c14(&acc, seq), 44 | 15 => Acc::push_c15(&acc, seq), 45 | _ => (), 46 | } 47 | } 48 | } 49 | s.run(now, false); 50 | 51 | // Check that all the calls executed, and in the correct order 52 | let done = acc 53 | .query(s, |this, _| std::mem::replace(&mut this.done, Vec::new())) 54 | .unwrap(); 55 | assert_eq!(done, expected); 56 | } 57 | 58 | struct Acc { 59 | done: Vec, 60 | } 61 | 62 | macro_rules! def { 63 | (0, $name:ident, $push:ident) => { 64 | fn $name(&mut self, _: CX![]) { 65 | self.done.push(0); 66 | } 67 | fn $push(this: &Actor, _: u16) { 68 | call!([this], $name()); 69 | } 70 | }; 71 | ($len:expr, $name:ident, $push:ident) => { 72 | // Never inline either call, in order to force `n` to be 73 | // treated as a variable and actually store the array on the 74 | // queue (rather than risk inlining and specialising the whole 75 | // call) 76 | #[inline(never)] 77 | fn $name(&mut self, _: CX![], val: [u16; $len]) { 78 | self.done.push($len); 79 | if $len >= 2 { 80 | assert_eq!(val[0] + val[$len - 1], 44444); 81 | } 82 | } 83 | #[inline(never)] 84 | fn $push(this: &Actor, n: u16) { 85 | let mut v = [0; $len]; 86 | if $len >= 2 { 87 | v[0] = n; 88 | v[$len - 1] = 44444 - n; 89 | } else { 90 | v[0] = 22222; 91 | } 92 | call!([this], $name(v)); 93 | } 94 | }; 95 | } 96 | 97 | impl Acc { 98 | fn init(_: CX![]) -> Option { 99 | Some(Self { done: Vec::new() }) 100 | } 101 | 102 | def!(0, c0, push_c0); 103 | def!(1, c1, push_c1); 104 | def!(2, c2, push_c2); 105 | def!(3, c3, push_c3); 106 | def!(4, c4, push_c4); 107 | def!(5, c5, push_c5); 108 | def!(6, c6, push_c6); 109 | def!(7, c7, push_c7); 110 | def!(8, c8, push_c8); 111 | def!(9, c9, push_c9); 112 | def!(10, c10, push_c10); 113 | def!(11, c11, push_c11); 114 | def!(12, c12, push_c12); 115 | def!(13, c13, push_c13); 116 | def!(14, c14, push_c14); 117 | def!(15, c15, push_c15); 118 | } 119 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | //! Support code for asynchronous tasks 2 | 3 | use crate::cell::cell::ActorCell; 4 | use crate::{Core, Deferrer, Stakker}; 5 | use std::pin::Pin; 6 | use std::rc::Rc; 7 | 8 | /// An asynchronous task reference 9 | /// 10 | /// **Stakker** provides only a very thin wrapping to enable tasks. 11 | /// Full support is provided in a separate crate 12 | /// (**stakker_async_await**). A task is represented as an 13 | /// implementation of the [`TaskTrait`](trait.TaskTrait.html) trait 14 | /// which is called when the task needs to be resumed. It is expected 15 | /// that even if the task cannot advance, it will accept a spurious 16 | /// call gracefully. Spurious calls may be generated for example if 17 | /// the task is woken from another thread. 18 | /// 19 | /// It is guaranteed that the task is resumed directly from the main 20 | /// loop, not within any actor call nor any deeper down the stack. 21 | /// This may be important in order to avoid `RefCell` panics, or to 22 | /// allow the use of `UnsafeCell`. Internally this is statically 23 | /// guaranteed by using an `ActorCell` to wrap the closure. This code 24 | /// also handles the pinning required for running futures. 25 | /// 26 | /// Deeper down the stack whilst the task is running, it is possible 27 | /// to get a reference to the running task by using 28 | /// [`Task::from_context`](#method.from_context). This may be 29 | /// used to implement efficient same-thread wakers. 30 | #[derive(Clone)] 31 | pub struct Task { 32 | rc: Rc>>>, 33 | } 34 | 35 | impl Task { 36 | /// Create a new task from an implementation of the 37 | /// [`TaskTrait`](trait.TaskTrait.html), and return a reference to 38 | /// it. (`Task` is like an `Rc` reference to the task.) The 39 | /// `resume` method will be called each time the task needs to be 40 | /// resumed. 41 | pub fn new(core: &mut Core, inner: impl TaskTrait + 'static) -> Self { 42 | Self { 43 | rc: Rc::new(core.actor_maker.cell(Box::pin(inner))), 44 | } 45 | } 46 | 47 | /// Resume execution of the task. The task will advance its state 48 | /// as much as possible, and then return. It's intended that 49 | /// whatever poll-handler caused the task to yield will have saved 50 | /// a reference to this task to keep it alive until it can be 51 | /// woken again. 52 | pub fn resume(&mut self, s: &mut Stakker) { 53 | s.deferrer.task_replace(Some(self.clone())); 54 | s.actor_owner.rw(&self.rc).as_mut().resume(&mut s.core); 55 | s.deferrer.task_replace(None); 56 | } 57 | 58 | /// Obtain the [`Task`](struct.Task.html) reference of the 59 | /// currently-running task from the provided 60 | /// [`Deferrer`](../struct.Deferrer.html), if a task is currently 61 | /// running. 62 | pub fn from_context(deferrer: &Deferrer) -> Option { 63 | let v0 = deferrer.task_replace(None); 64 | let v1 = v0.clone(); 65 | deferrer.task_replace(v0); 66 | v1 67 | } 68 | } 69 | 70 | /// Trait that tasks must implement 71 | pub trait TaskTrait { 72 | /// Resume execution of the task. This must handle spurious calls 73 | /// gracefully, even if nothing has changed. 74 | fn resume(self: Pin<&mut Self>, core: &mut Core); 75 | } 76 | 77 | // Not useful yet, as there's no way to pass an owner-borrow through 78 | // `Future::poll` 79 | // 80 | // /// Statically-checked cell type for use within task-related code 81 | // /// 82 | // /// This requires access to `&mut Stakker` in order to borrow the cell 83 | // /// contents, so cannot be used within actor code or anywhere else 84 | // /// where a Stakker reference is not available. 85 | // pub struct TaskCell(ActorCell); 86 | // 87 | // impl TaskCell { 88 | // /// Create a new TaskCell 89 | // pub fn new(core: &mut Core, value: T) -> Self { 90 | // Self(core.actor_maker.cell(value)) 91 | // } 92 | // 93 | // // Get a mutable reference to the cell contents 94 | // pub fn rw<'a>(&'a self, s: &'a mut Stakker) -> &'a mut T { 95 | // s.actor_owner.rw(&self.0) 96 | // } 97 | // } 98 | -------------------------------------------------------------------------------- /src/rc/actorrc_std.rs: -------------------------------------------------------------------------------- 1 | // Safe variant of actor: costs ~16 bytes more than minimum possible 2 | use crate::actor::{Prep, State}; 3 | use crate::cell::cell::{ActorCell, ActorCellOwner}; 4 | use crate::queue::FnOnceQueue; 5 | use crate::rc::count::CountAndState; 6 | use crate::{Core, Deferrer, LogID, Ret, Stakker, StopCause}; 7 | use std::cell::Cell; 8 | use std::mem; 9 | use std::rc::Rc; 10 | 11 | // Memory overhead on 64-bit including Rc counts is around 56 + 12 | // size-of-A. Effectively we have to duplicate the State enum 13 | // both inside and outside of ActorCell to make it safe. 14 | struct ActorBox { 15 | // 16 bytes for Rc, plus ... 16 | strong: Cell, // 8 17 | notify: Cell>>, // 16 18 | deferrer: Deferrer, // 0 or 8 19 | #[cfg(feature = "logger")] 20 | id: LogID, 21 | inner: ActorCell>, // 8 + A 22 | } 23 | 24 | enum Inner { 25 | Prep(Prep), 26 | Ready(A), 27 | Zombie, 28 | } 29 | 30 | pub(crate) struct ActorRc(Rc>); 31 | 32 | impl ActorRc { 33 | pub fn new(core: &mut Core, notify: Option>, _parent_id: LogID) -> Self { 34 | Self(Rc::new(ActorBox { 35 | strong: Cell::new(CountAndState::new()), 36 | notify: Cell::new(notify), 37 | #[cfg(feature = "logger")] 38 | id: core.log_span_open(std::any::type_name::(), _parent_id, |_| {}), 39 | inner: core.actor_maker.cell(Inner::Prep(Prep { 40 | queue: FnOnceQueue::new(), 41 | })), 42 | deferrer: core.deferrer(), 43 | })) 44 | } 45 | 46 | #[inline] 47 | pub fn id(&self) -> LogID { 48 | #[cfg(feature = "logger")] 49 | { 50 | self.0.id 51 | } 52 | #[cfg(not(feature = "logger"))] 53 | 0 54 | } 55 | 56 | #[inline] 57 | fn strong(&self) -> &Cell { 58 | &self.0.strong 59 | } 60 | 61 | pub fn strong_inc(&self) { 62 | self.strong().replace(self.strong().get().inc()); 63 | } 64 | pub fn strong_dec(&self) -> bool { 65 | let (count, went_to_zero) = self.strong().get().dec(); 66 | self.strong().replace(count); 67 | went_to_zero 68 | } 69 | 70 | #[inline] 71 | pub fn is_zombie(&self) -> bool { 72 | self.strong().get().is_zombie() 73 | } 74 | #[inline] 75 | pub fn is_prep(&self) -> bool { 76 | self.strong().get().is_prep() 77 | } 78 | 79 | pub fn to_ready(&self, s: &mut Stakker, val: A) { 80 | let inner = s.actor_owner.rw(&self.0.inner); 81 | match mem::replace(inner, Inner::Ready(val)) { 82 | Inner::Prep(mut prep) => { 83 | self.strong() 84 | .replace(self.strong().get().set_state(State::Ready)); 85 | prep.queue.execute(s); 86 | } 87 | Inner::Ready(_) => panic!("Actor::to_ready() called twice"), 88 | Inner::Zombie => *inner = Inner::Zombie, 89 | } 90 | } 91 | pub fn to_zombie(&self, s: &mut Stakker) -> Option> { 92 | self.strong() 93 | .replace(self.strong().get().set_state(State::Zombie)); 94 | *s.actor_owner.rw(&self.0.inner) = Inner::Zombie; 95 | self.0.notify.replace(None) 96 | } 97 | pub fn borrow_ready<'a>(&'a self, o: &'a mut ActorCellOwner) -> Option<&'a mut A> { 98 | match o.rw(&self.0.inner) { 99 | Inner::Ready(val) => Some(val), 100 | _ => None, 101 | } 102 | } 103 | pub fn borrow_prep<'a>(&'a self, o: &'a mut ActorCellOwner) -> Option<&'a mut Prep> { 104 | match o.rw(&self.0.inner) { 105 | Inner::Prep(prep) => Some(prep), 106 | _ => None, 107 | } 108 | } 109 | 110 | #[inline] 111 | pub fn access_deferrer(&self) -> &Deferrer { 112 | &self.0.deferrer 113 | } 114 | } 115 | 116 | impl Clone for ActorRc { 117 | fn clone(&self) -> Self { 118 | Self(self.0.clone()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/queue_corpus.rs: -------------------------------------------------------------------------------- 1 | use crate::time::Instant; 2 | use crate::*; 3 | 4 | // This uses a snapshot of a small fuzzing corpus that gives full 5 | // coverage of queue operations in `cargo fuzz` 6 | 7 | test_fn!( 8 | fn run() { 9 | let data = std::include_bytes!("queue_corpus.bin"); 10 | let mut p = &data[..]; 11 | while !p.is_empty() { 12 | let len = ((p[0] as usize) << 8) + (p[1] as usize); 13 | fuzz_queue(&p[2..2 + len]); 14 | p = &p[2 + len..]; 15 | } 16 | } 17 | ); 18 | 19 | // The idea is to push random sequences of calls of different lengths 20 | // onto the queue, interspersed with calls to run the queue. 21 | 22 | fn fuzz_queue(data: &[u8]) { 23 | let now = Instant::now(); 24 | let mut stakker = Stakker::new(now); 25 | let s = &mut stakker; 26 | 27 | let acc = actor!(s, Acc::init(), ret_nop![]); 28 | s.run(now, false); 29 | 30 | let mut expected = Vec::new(); 31 | let mut seq = 1; 32 | for b in data { 33 | seq += 1; 34 | if *b < 4 { 35 | // If pure-random, would flush queue 1 in 64 bytes, which 36 | // biases queue-length to about 64*(8+(8+16+24+32)/4) == 37 | // 1792. So this helps the fuzzer start to explore around 38 | // the 1024-2048 range. 39 | s.run(now, false); 40 | } else { 41 | let n = *b & 15; 42 | expected.push(n); 43 | match n { 44 | 0 => Acc::push_c0(&acc, seq), 45 | 1 => Acc::push_c1(&acc, seq), 46 | 2 => Acc::push_c2(&acc, seq), 47 | 3 => Acc::push_c3(&acc, seq), 48 | 4 => Acc::push_c4(&acc, seq), 49 | 5 => Acc::push_c5(&acc, seq), 50 | 6 => Acc::push_c6(&acc, seq), 51 | 7 => Acc::push_c7(&acc, seq), 52 | 8 => Acc::push_c8(&acc, seq), 53 | 9 => Acc::push_c9(&acc, seq), 54 | 10 => Acc::push_c10(&acc, seq), 55 | 11 => Acc::push_c11(&acc, seq), 56 | 12 => Acc::push_c12(&acc, seq), 57 | 13 => Acc::push_c13(&acc, seq), 58 | 14 => Acc::push_c14(&acc, seq), 59 | 15 => Acc::push_c15(&acc, seq), 60 | _ => (), 61 | } 62 | } 63 | } 64 | s.run(now, false); 65 | 66 | // Check that all the calls executed, and in the correct order 67 | let done = acc 68 | .query(s, |this, _| std::mem::replace(&mut this.done, Vec::new())) 69 | .unwrap(); 70 | assert_eq!(done, expected); 71 | } 72 | 73 | struct Acc { 74 | done: Vec, 75 | } 76 | 77 | macro_rules! def { 78 | (0, $name:ident, $push:ident) => { 79 | fn $name(&mut self, _: CX![]) { 80 | self.done.push(0); 81 | } 82 | fn $push(this: &Actor, _: u16) { 83 | call!([this], $name()); 84 | } 85 | }; 86 | ($len:expr, $name:ident, $push:ident) => { 87 | // Never inline either call, in order to force `n` to be 88 | // treated as a variable and actually store the array on the 89 | // queue (rather than risk inlining and specialising the whole 90 | // call) 91 | #[inline(never)] 92 | fn $name(&mut self, _: CX![], val: [u16; $len]) { 93 | self.done.push($len); 94 | if $len >= 2 { 95 | assert_eq!(val[0] + val[$len - 1], 44444); 96 | } 97 | } 98 | #[inline(never)] 99 | fn $push(this: &Actor, n: u16) { 100 | let mut v = [0; $len]; 101 | if $len >= 2 { 102 | v[0] = n; 103 | v[$len - 1] = 44444 - n; 104 | } else { 105 | v[0] = 22222; 106 | } 107 | call!([this], $name(v)); 108 | } 109 | }; 110 | } 111 | 112 | impl Acc { 113 | fn init(_: CX![]) -> Option { 114 | Some(Self { done: Vec::new() }) 115 | } 116 | 117 | def!(0, c0, push_c0); 118 | def!(1, c1, push_c1); 119 | def!(2, c2, push_c2); 120 | def!(3, c3, push_c3); 121 | def!(4, c4, push_c4); 122 | def!(5, c5, push_c5); 123 | def!(6, c6, push_c6); 124 | def!(7, c7, push_c7); 125 | def!(8, c8, push_c8); 126 | def!(9, c9, push_c9); 127 | def!(10, c10, push_c10); 128 | def!(11, c11, push_c11); 129 | def!(12, c12, push_c12); 130 | def!(13, c13, push_c13); 131 | def!(14, c14, push_c14); 132 | def!(15, c15, push_c15); 133 | } 134 | -------------------------------------------------------------------------------- /src/sync/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{Core, Fwd, Waker}; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | /// Channel for sending messages to an actor 5 | /// 6 | /// A [`Channel`] may be used to send messages of type `M` to an actor 7 | /// from any thread. It is an unbounded queue. 8 | /// 9 | /// Messages are delivered directly to an actor method via a [`Fwd`] 10 | /// instance. Cleanup of the channel is handled via a 11 | /// [`ChannelGuard`] which should be kept in the same actor. When 12 | /// this is dropped, the channel is closed, and senders are informed 13 | /// via the [`Channel::send`] and [`Channel::is_closed`] methods. So 14 | /// this handles cleanup automatically when the actor fails or 15 | /// terminates for any reason. 16 | /// 17 | /// [`Channel::is_closed`]: ../sync/struct.Channel.html#method.is_closed 18 | /// [`Channel::send`]: ../sync/struct.Channel.html#method.send 19 | /// [`ChannelGuard`]: ../sync/struct.ChannelGuard.html 20 | /// [`Channel`]: ../sync/struct.Channel.html 21 | /// [`Fwd`]: ../struct.Fwd.html 22 | pub struct Channel { 23 | arc: Arc>>, 24 | } 25 | 26 | struct ChannelBuf { 27 | queue: Vec, 28 | waker: Option, // None if closed 29 | } 30 | 31 | impl Channel { 32 | /// Create a new channel that directs messages to an actor using 33 | /// the given `Fwd` instance. Returns the channel and a 34 | /// channel-guard. The [`Channel`] may be cloned as many times as 35 | /// necessary and sent to other threads. The [`ChannelGuard`] 36 | /// should be kept in the actor that receives the messages. 37 | /// 38 | /// [`ChannelGuard`]: ../sync/struct.ChannelGuard.html 39 | /// [`Channel`]: ../sync/struct.Channel.html 40 | pub fn new(core: &mut Core, fwd: Fwd) -> (Self, ChannelGuard) { 41 | let arc = Arc::new(Mutex::new(ChannelBuf { 42 | queue: Vec::new(), 43 | waker: None, 44 | })); 45 | let arc1 = arc.clone(); 46 | let waker = core.waker(move |_, _| { 47 | let mut guard = arc1.lock().expect("Stakker channel lock poisoned"); 48 | let vec = std::mem::take(&mut guard.queue); 49 | let is_open = guard.waker.is_some(); 50 | drop(guard); 51 | if is_open { 52 | for msg in vec { 53 | fwd.fwd(msg); 54 | } 55 | } 56 | }); 57 | arc.lock().unwrap().waker = Some(waker); 58 | let this = Self { arc }; 59 | let guard = ChannelGuard(Box::new(this.clone())); 60 | (this, guard) 61 | } 62 | 63 | /// Send a message to destination actor in the `Stakker` thread if 64 | /// the channel is open, and return `true`. If the channel has 65 | /// been closed, returns `false`. 66 | pub fn send(&self, msg: M) -> bool { 67 | let mut guard = self.arc.lock().expect("Stakker channel lock poisoned"); 68 | if let Some(ref waker) = guard.waker { 69 | if guard.queue.is_empty() { 70 | waker.wake(); 71 | } 72 | guard.queue.push(msg); 73 | true 74 | } else { 75 | false 76 | } 77 | } 78 | 79 | /// Tests whether the channel has been closed. 80 | pub fn is_closed(&self) -> bool { 81 | let guard = self.arc.lock().expect("Stakker channel lock poisoned"); 82 | guard.waker.is_none() 83 | } 84 | } 85 | 86 | impl Clone for Channel { 87 | /// Get another reference to the same channel 88 | fn clone(&self) -> Self { 89 | Self { 90 | arc: self.arc.clone(), 91 | } 92 | } 93 | } 94 | 95 | trait Closable { 96 | fn close(&self); 97 | } 98 | 99 | impl Closable for Channel { 100 | fn close(&self) { 101 | let mut guard = self.arc.lock().expect("Stakker channel lock poisoned"); 102 | guard.waker.take(); 103 | guard.queue = Vec::new(); 104 | } 105 | } 106 | 107 | /// Guard for a channel 108 | /// 109 | /// When this is dropped, the associated [`Channel`] is closed and any 110 | /// pending messages are dropped. This should be kept in the actor 111 | /// that receives messages so that any failure or other termination of 112 | /// the actor results in correct cleanup. 113 | /// 114 | /// [`Channel`]: ../sync/struct.Channel.html 115 | pub struct ChannelGuard(Box); 116 | 117 | impl Drop for ChannelGuard { 118 | /// Close the channel and drop any pending messages 119 | fn drop(&mut self) { 120 | self.0.close(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/fwd.rs: -------------------------------------------------------------------------------- 1 | use crate::rc::FwdRc; 2 | use crate::{Actor, Cx}; 3 | use static_assertions::assert_eq_size; 4 | 5 | /// Forwarder for messages of type `M` 6 | /// 7 | /// Typically this would be created using one of the `fwd_*!` macros. 8 | /// This may be called many times to forward different messages to the 9 | /// same end-point. For situations where a callback can be used at 10 | /// most one time, prefer [`Ret`], because it is less restrictive on 11 | /// the types of data that may be captured by the closure. 12 | /// 13 | /// This is a fat pointer to a ref-counted dynamic `Fn`, so it 14 | /// consumes two `usize` locally, and the size of the `Fn` closure 15 | /// plus the ref-count (a `usize`) on the heap. It may be cloned 16 | /// cheaply if another identical [`Fwd`] is required. 17 | /// 18 | /// For zero arguments, use `Fwd<()>`. For one argument, use 19 | /// `Fwd`, where `type` is the type of the argument. For two or 20 | /// more use a tuple: `Fwd<(type1, type2...)>`. Call the [`Fwd::fwd`] 21 | /// method to send a message or use the [`fwd!`] macro. Sending a 22 | /// message typically results in the asynchronous execution of an 23 | /// actor call, but may have other effects depending on the type of 24 | /// forwarder. 25 | /// 26 | /// [`Fwd::fwd`]: struct.Fwd.html#method.fwd 27 | /// [`Fwd`]: struct.Fwd.html 28 | /// [`Ret`]: struct.Ret.html 29 | /// [`fwd!`]: macro.fwd.html 30 | pub struct Fwd(FwdRc); 31 | 32 | assert_eq_size!(Fwd<()>, [usize; 2]); 33 | 34 | // Design note: Adding an `is_valid` method was considered, which 35 | // could allow stale `Fwd` instances to be dropped if the target goes 36 | // away. However this adds overhead to all `Fwd` types, and in the 37 | // cases where this might have been useful, it would require scanning 38 | // potentially long lists at intervals, i.e. O(N). It is better to 39 | // make sure that proper cleanup occurs instead. For actors, that 40 | // means when the actor dies, cleaning up the whole group of related 41 | // actors. 42 | 43 | impl Fwd { 44 | /// Forward a message through the [`Fwd`] instance 45 | /// 46 | /// [`Fwd`]: struct.Fwd.html 47 | pub fn fwd(&self, msg: M) { 48 | self.0.inner()(msg); 49 | } 50 | 51 | /// Create a [`Fwd`] instance that performs an arbitrary action 52 | /// with the message on being called. The call is made 53 | /// synchronously at the point that the message is forwarded. 54 | /// 55 | /// [`Fwd`]: struct.Fwd.html 56 | #[inline] 57 | pub fn new(f: impl Fn(M) + 'static) -> Self { 58 | Self(FwdRc::new(f)) 59 | } 60 | 61 | /// Create a [`Fwd`] instance that queues calls to an actor. The 62 | /// `Fn` provided must be `Copy` because on each invocation a new 63 | /// copy is made and put on the queue. Use [`Ret`] instead if 64 | /// this is too restrictive. 65 | /// 66 | /// [`Fwd`]: struct.Fwd.html 67 | /// [`Ret`]: struct.Ret.html 68 | #[allow(clippy::wrong_self_convention)] 69 | #[inline] 70 | pub fn to_actor( 71 | actor: Actor, 72 | f: impl Fn(&mut A, &mut Cx<'_, A>, M) + Copy + 'static, 73 | ) -> Self { 74 | // This relies on closures being inlined. We expect this to 75 | // result in just two fully-inlined closures: one boxed and 76 | // returned as the Fwd value, the other appended to the defer 77 | // queue when that is called. 78 | Self::new(move |m| { 79 | let actor2 = actor.clone(); 80 | actor.defer(move |s| actor2.apply(s, move |a, cx| f(a, cx, m))); 81 | }) 82 | } 83 | 84 | /// Create a [`Fwd`] instance that queues calls to an actor whilst 85 | /// in the **Prep** phase. Once the actor is **Ready**, any 86 | /// queued prep calls are dropped. 87 | /// 88 | /// [`Fwd`]: struct.Fwd.html 89 | #[allow(clippy::wrong_self_convention)] 90 | #[inline] 91 | pub fn to_actor_prep( 92 | actor: Actor, 93 | f: impl Fn(&mut Cx<'_, A>, M) -> Option + Copy + 'static, 94 | ) -> Self { 95 | Self::new(move |m| { 96 | let actor2 = actor.clone(); 97 | actor.defer(move |s| actor2.apply_prep(s, move |cx| f(cx, m))); 98 | }) 99 | } 100 | 101 | /// Create a [`Fwd`] instance that panics with the given message 102 | /// when called 103 | /// 104 | /// [`Fwd`]: struct.Fwd.html 105 | #[inline] 106 | pub fn panic(msg: impl Into) -> Self { 107 | let msg: String = msg.into(); 108 | Self::new(move |_| panic!("{}", msg)) 109 | } 110 | } 111 | 112 | impl Clone for Fwd { 113 | fn clone(&self) -> Self { 114 | Self(self.0.clone()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/tutorial.rs: -------------------------------------------------------------------------------- 1 | //! The tutorial example from: 2 | //! https://docs.rs/stakker/*/stakker/#tutorial-example 3 | 4 | use stakker::*; 5 | use std::time::{Duration, Instant}; 6 | 7 | // An actor is represented as a struct which holds the actor state 8 | struct Light { 9 | start: Instant, 10 | on: bool, 11 | } 12 | 13 | impl Light { 14 | // This is a "Prep" method which is used to create a Self value 15 | // for the actor. `cx` is the actor context and gives access to 16 | // Stakker `Core`. (`CX![]` expands to `&mut Cx<'_, Self>`.) 17 | // 18 | // A "Prep" method doesn't have to return a Self value right away. 19 | // For example it might asynchronously attempt a connection to a 20 | // remote server first before arranging a call to another "Prep" 21 | // function which returns the Self value. Once a value is 22 | // returned, the actor is "Ready" and any queued-up operations on 23 | // the actor will be executed. 24 | pub fn init(cx: CX![]) -> Option { 25 | // Use cx.now() instead of Instant::now() to allow execution 26 | // in virtual time if supported by the environment. 27 | let start = cx.now(); 28 | Some(Self { start, on: false }) 29 | } 30 | 31 | // Methods that may be called once the actor is "Ready" have a 32 | // `&mut self` or `&self` first argument. 33 | pub fn set(&mut self, cx: CX![], on: bool) { 34 | self.on = on; 35 | let time = cx.now() - self.start; 36 | println!( 37 | "{:04}.{:03} Light on: {}", 38 | time.as_secs(), 39 | time.subsec_millis(), 40 | on 41 | ); 42 | } 43 | 44 | // A `Fwd` or `Ret` allows passing data to arbitrary destinations, 45 | // like an async callback. Here we use it to return a value. 46 | pub fn query(&self, _cx: CX![], ret: Ret) { 47 | ret!([ret], self.on); 48 | } 49 | } 50 | 51 | // This is another actor that holds a reference to a Light actor. 52 | struct Flasher { 53 | light: Actor, 54 | interval: Duration, 55 | count: usize, 56 | } 57 | 58 | impl Flasher { 59 | pub fn init(cx: CX![], light: Actor, interval: Duration, count: usize) -> Option { 60 | // Defer first switch to the queue 61 | call!([cx], switch(true)); 62 | Some(Self { 63 | light, 64 | interval, 65 | count, 66 | }) 67 | } 68 | 69 | pub fn switch(&mut self, cx: CX![], on: bool) { 70 | // Change the light state 71 | call!([self.light], set(on)); 72 | 73 | self.count -= 1; 74 | if self.count != 0 { 75 | // Call switch again after a delay 76 | after!(self.interval, [cx], switch(!on)); 77 | } else { 78 | // Terminate the actor successfully, causing StopCause handler to run 79 | cx.stop(); 80 | } 81 | 82 | // Query the light state, receiving the response in the method 83 | // `recv_state`, which has both fixed and forwarded arguments. 84 | let ret = ret_some_to!([cx], recv_state(self.count) as (bool)); 85 | call!([self.light], query(ret)); 86 | } 87 | 88 | fn recv_state(&self, _: CX![], count: usize, state: bool) { 89 | println!(" (at count {} received: {})", count, state); 90 | } 91 | } 92 | 93 | fn main() { 94 | // Contains all the queues and timers, and controls access to the 95 | // state of all the actors. 96 | let mut stakker0 = Stakker::new(Instant::now()); 97 | let stakker = &mut stakker0; 98 | 99 | // Create and initialise the Light and Flasher actors. The 100 | // Flasher actor is given a reference to the Light. Use a 101 | // StopCause handler to shutdown when the Flasher terminates. 102 | let light = actor!(stakker, Light::init(), ret_nop!()); 103 | 104 | let _flasher = actor!( 105 | stakker, 106 | Flasher::init(light.clone(), Duration::from_secs(1), 6), 107 | ret_shutdown!(stakker) 108 | ); 109 | 110 | // Since we're not in virtual time, we use `Instant::now()` in 111 | // this loop, which is then passed on to all the actors as 112 | // `cx.now()`. (If you want to run time faster or slower you 113 | // could use another source of time.) So all calls in a batch of 114 | // processing get the same `cx.now()` value. Also note that 115 | // `Instant::now()` uses a Mutex on some platforms so it saves 116 | // cycles to call it less often. 117 | stakker.run(Instant::now(), false); 118 | while stakker.not_shutdown() { 119 | // Wait for next timer to expire. Here there's no I/O polling 120 | // required to wait for external events, so just `sleep` 121 | let maxdur = stakker.next_wait_max(Instant::now(), Duration::from_secs(60), false); 122 | std::thread::sleep(maxdur); 123 | 124 | // Run queue and timers 125 | stakker.run(Instant::now(), false); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/test/test.rs: -------------------------------------------------------------------------------- 1 | //! Since 1.67 `RUST_TEST_THREADS=1` no longer runs all tests in a 2 | //! single thread. Since the Stakker single-threaded tests require 3 | //! that, we can no longer use `RUST_TEST_THREADS=1`. See: 4 | //! https://github.com/rust-lang/cargo/issues/11896 and 5 | //! https://github.com/rust-lang/rust/issues/104053. 6 | //! 7 | //! So the solution is to run a worker thread and have the tests send 8 | //! work to it and wait for an okay or panic to come back. This works 9 | //! well for normal `cargo test`. 10 | //! 11 | //! However MIRI testing adds a further complication. If using a 12 | //! worker thread, MIRI insists that you wait for your worker thread 13 | //! to finish, or else disable MIRI leak checking. But if you have a 14 | //! test that waits for the worker thread to finish, that blocks a 15 | //! whole thread, and that seems to cause MIRI to block further tests 16 | //! from running. So as a workaround, MIRI tests have to be run one 17 | //! at a time from outside of the `cargo miri` invocation. This code 18 | //! assumes that that approach is being taken, and disables the worker 19 | //! thread for MIRI. 20 | //! 21 | //! To use this: Put `test_fn!( ... )` around the whole test function 22 | //! definition, including any outer attributes such as `#[cfg(...)]` 23 | //! or `#[should_panic]`. This simply adds a `#[test]` in the normal 24 | //! case, or else redirects the code to run in the worker thread in 25 | //! the single-threaded case. 26 | 27 | // Simple case, where tests can be run in the normal way, in parallel 28 | #[cfg(any(miri, feature = "multi-stakker", feature = "multi-thread"))] 29 | macro_rules! test_fn { 30 | ($(#[$attr:meta])* fn $name:ident() $code:expr) => { 31 | #[test] 32 | $(#[$attr])* 33 | fn $name() { 34 | $code 35 | } 36 | }; 37 | } 38 | 39 | // Case where everything has to be sent to a single worker thread 40 | #[cfg(not(any(miri, feature = "multi-stakker", feature = "multi-thread")))] 41 | macro_rules! test_fn { 42 | ($(#[$attr:meta])* fn $name:ident() $code:expr) => { 43 | #[test] 44 | $(#[$attr])* 45 | fn $name() { 46 | crate::test::test::worker::run_on_worker(|| $code); 47 | } 48 | }; 49 | } 50 | 51 | #[cfg(not(any(miri, feature = "multi-stakker", feature = "multi-thread")))] 52 | pub(crate) mod worker { 53 | use std::sync::mpsc::{channel, Sender}; 54 | use std::sync::Mutex; 55 | 56 | struct Job { 57 | cb: Box, 58 | ret: Sender, 59 | } 60 | 61 | struct JobRet { 62 | panic: Option, 63 | } 64 | 65 | struct Worker { 66 | send: Sender, 67 | } 68 | 69 | static WORKER: Mutex> = Mutex::new(None); 70 | 71 | fn start_worker() { 72 | let mut guard = WORKER.lock().expect("Worker lock poisoned"); 73 | if guard.is_none() { 74 | let (send, recv) = channel::(); 75 | std::thread::spawn(move || { 76 | while let Ok(job) = recv.recv() { 77 | let panic = 78 | std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (job.cb)())) 79 | .map_err(|e| { 80 | // Pass through panic message if it is a `String` or 81 | // `&str`, else generate some debugging output 82 | match e.downcast::() { 83 | Ok(v) => *v, 84 | Err(e) => match e.downcast::<&str>() { 85 | Ok(v) => v.to_string(), 86 | Err(e) => { 87 | format!("Panicked with unknown type: {:?}", e.type_id()) 88 | } 89 | }, 90 | } 91 | }) 92 | .err(); 93 | let ret = JobRet { panic }; 94 | job.ret.send(ret).expect("Test not waiting for result"); 95 | } 96 | }); 97 | *guard = Some(Worker { send }); 98 | } 99 | } 100 | 101 | #[track_caller] 102 | pub(crate) fn run_on_worker(cb: impl FnOnce() + Send + 'static) { 103 | start_worker(); 104 | 105 | let (ret_send, ret_recv) = channel(); 106 | let job = Job { 107 | cb: Box::new(cb), 108 | ret: ret_send, 109 | }; 110 | 111 | let mut guard = WORKER.lock().expect("Worker lock poisoned"); 112 | if let Some(ref mut worker) = *guard { 113 | worker.send.send(job).expect("Worker thread died?"); 114 | 115 | let ret = ret_recv.recv().expect("Worker thread died?"); 116 | if let Some(msg) = ret.panic { 117 | drop(guard); 118 | panic!("{msg}"); 119 | } 120 | } else { 121 | unreachable!(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/rc/actorrc_packed.rs: -------------------------------------------------------------------------------- 1 | // Variant of ActorRc using our own Rc implementation (MinRc). Saves 2 | // 8 bytes compared to safe version because we can pack the state into 3 | // the count value. 4 | // 5 | // TODO: We could save another 8 bytes by using ptr::read/write 6 | // instead of the Inner enum. However that is impossible right now 7 | // because `union` only supports Copy types. Also maybe better make 8 | // that a separate implementation because the unsafe review would be 9 | // harder (i.e. some people would prefer this one). 10 | 11 | use crate::actor::{Prep, State}; 12 | use crate::cell::cell::{ActorCell, ActorCellOwner}; 13 | use crate::queue::FnOnceQueue; 14 | use crate::rc::count::CountAndState; 15 | use crate::rc::minrc::MinRc; 16 | use crate::{Core, Deferrer, Fwd, LogID, Ret, Stakker, StopCause}; 17 | use static_assertions::{assert_eq_size, const_assert}; 18 | use std::cell::Cell; 19 | use std::mem; 20 | use std::mem::size_of; 21 | 22 | // Memory overhead on 64-bit is around 40 to 64 + size-of-A. We still 23 | // have the State enum info duplicated both inside and outside of 24 | // ActorCell (outside it's packed in CountAndState, inside it's an 25 | // 8-byte enum discriminant). 26 | // 27 | // The decision about what goes inside or outside the ActorCell is 28 | // critical. Anything outside can be accessed without needing a 29 | // Stakker reference, e.g. drop() or clone() or owned() or any other 30 | // calls that don't have Stakker access. 31 | // 32 | // - `strong` is outside because owned/drop need it. It could be put 33 | // inside but that would rely on slower deferred calls to do inc/dec. 34 | // 35 | // - `deferrer` is outside because drop needs it. It is also useful 36 | // for user-side drop handlers, through the Actor::defer call. 37 | // 38 | // - `notify` is outside because of separation of concerns. Putting 39 | // it inside and making it part of the actor interface, and letting 40 | // the actor optimise that might be possible, but this makes testing 41 | // harder since every actor might handle `notify` differently, or even 42 | // have it hardcoded. So it is best that notify remains outside the 43 | // implementation of the actor. 44 | struct ActorBox { 45 | // 8, plus ref-counts outside: 8 or 16 46 | strong: Cell, 47 | // 0 or 8 48 | deferrer: Deferrer, 49 | // 16 50 | notify: Cell>>, 51 | #[cfg(feature = "logger")] 52 | id: LogID, 53 | // CellID(0 or 8) + Enum-determinant(8) + Data(max(24, A)) # 24 is for FnOnceQueue 54 | inner: ActorCell>, 55 | } 56 | 57 | assert_eq_size!(Cell>>, [usize; 2]); 58 | const_assert!(size_of::() <= size_of::()); 59 | 60 | enum Inner { 61 | Prep(Prep), 62 | Ready(A), 63 | Zombie, 64 | } 65 | 66 | pub(crate) struct ActorRc(MinRc>); 67 | 68 | impl ActorRc { 69 | pub fn new(core: &mut Core, notify: Option>, _parent_id: LogID) -> Self { 70 | Self(MinRc::new(ActorBox { 71 | strong: Cell::new(CountAndState::new()), 72 | deferrer: core.deferrer(), 73 | notify: Cell::new(notify), 74 | #[cfg(feature = "logger")] 75 | id: core.log_span_open(std::any::type_name::(), _parent_id, |_| {}), 76 | inner: core.actor_maker.cell(Inner::Prep(Prep { 77 | queue: FnOnceQueue::new(), 78 | })), 79 | })) 80 | } 81 | 82 | #[inline] 83 | pub fn id(&self) -> LogID { 84 | #[cfg(feature = "logger")] 85 | { 86 | self.0.inner().id 87 | } 88 | #[cfg(not(feature = "logger"))] 89 | 0 90 | } 91 | 92 | // Accessors for convenience 93 | #[inline] 94 | fn strong(&self) -> &Cell { 95 | &self.0.inner().strong 96 | } 97 | #[inline] 98 | fn inner(&self) -> &ActorCell> { 99 | &self.0.inner().inner 100 | } 101 | #[inline] 102 | fn notify(&self) -> &Cell>> { 103 | &self.0.inner().notify 104 | } 105 | #[inline] 106 | fn deferrer(&self) -> &Deferrer { 107 | &self.0.inner().deferrer 108 | } 109 | 110 | pub fn strong_inc(&self) { 111 | self.strong().replace(self.strong().get().inc()); 112 | } 113 | pub fn strong_dec(&self) -> bool { 114 | let (count, went_to_zero) = self.strong().get().dec(); 115 | self.strong().replace(count); 116 | went_to_zero 117 | } 118 | 119 | #[inline] 120 | pub fn is_zombie(&self) -> bool { 121 | self.strong().get().is_zombie() 122 | } 123 | 124 | #[inline] 125 | pub fn is_prep(&self) -> bool { 126 | self.strong().get().is_prep() 127 | } 128 | 129 | pub fn to_ready(&self, s: &mut Stakker, val: A) { 130 | let inner = s.actor_owner.rw(self.inner()); 131 | match mem::replace(inner, Inner::Ready(val)) { 132 | Inner::Prep(mut prep) => { 133 | self.strong() 134 | .replace(self.strong().get().set_state(State::Ready)); 135 | prep.queue.execute(s); 136 | } 137 | Inner::Ready(_) => panic!("Actor::to_ready() called twice"), 138 | Inner::Zombie => *inner = Inner::Zombie, 139 | } 140 | } 141 | 142 | pub fn to_zombie(&self, s: &mut Stakker) -> Option> { 143 | self.strong() 144 | .replace(self.strong().get().set_state(State::Zombie)); 145 | *s.actor_owner.rw(self.inner()) = Inner::Zombie; 146 | self.notify().replace(None) 147 | } 148 | 149 | pub fn borrow_ready<'a>(&'a self, o: &'a mut ActorCellOwner) -> Option<&'a mut A> { 150 | match o.rw(self.inner()) { 151 | Inner::Ready(val) => Some(val), 152 | _ => None, 153 | } 154 | } 155 | 156 | pub fn borrow_prep<'a>(&'a self, o: &'a mut ActorCellOwner) -> Option<&'a mut Prep> { 157 | match o.rw(self.inner()) { 158 | Inner::Prep(prep) => Some(prep), 159 | _ => None, 160 | } 161 | } 162 | 163 | #[inline] 164 | pub fn access_deferrer(&self) -> &Deferrer { 165 | self.deferrer() 166 | } 167 | } 168 | 169 | impl Clone for ActorRc { 170 | fn clone(&self) -> Self { 171 | Self(self.0.clone()) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/ret.rs: -------------------------------------------------------------------------------- 1 | use crate::{Actor, Cx}; 2 | use static_assertions::assert_eq_size; 3 | 4 | // TODO: Consider `Ret` with built-in timeout that returns None if a 5 | // response is not generated within a certain time. This would be 6 | // used for connections to unreliable components, e.g. ones over TCP 7 | // links etc. However, if we have support for remote actors, then 8 | // timeouts would be handled at a higher level, i.e. a whole remote 9 | // host would be considered "Lost" and all references to it would 10 | // becomes "dead". So a timeout just for `Ret` is not necessary. 11 | 12 | /// Returner for messages of type `M` 13 | /// 14 | /// Typically this would be created using one of the `ret_*!` macros. 15 | /// This can be called only once, and if dropped, it will return a 16 | /// message of `None`. 17 | /// 18 | /// This is a fat pointer to a boxed dynamic `FnOnce`, so it consumes 19 | /// two `usize` locally, and the size of the `FnOnce` closure on the 20 | /// heap. It is a "move" type, so does not support `Copy` or `Clone`. 21 | /// It must be passed around by moving, and pulled out of composite 22 | /// types by destructuring (if the compiler doesn't automatically 23 | /// destructure for you). The [`Ret::ret`] call takes `self` which 24 | /// gives a compile-time guarantee that it is used only once. 25 | /// 26 | /// For zero arguments, use `Ret<()>`. For one argument, use 27 | /// `Ret`, where `type` is the type of the argument. For two or 28 | /// more use a tuple: `Ret<(type1, type2...)>`. Call the [`Ret::ret`] 29 | /// method to send a message or use the [`ret!`] macro. Sending a 30 | /// message typically results in the asynchronous execution of an 31 | /// actor call, but may have other effects depending on the type of 32 | /// returner. 33 | /// 34 | /// [`Ret::ret`]: struct.Ret.html#method.ret 35 | /// [`ret!`]: macro.ret.html 36 | pub struct Ret(Option)>>); 37 | 38 | assert_eq_size!(Ret<()>, [usize; 2]); 39 | 40 | impl Ret { 41 | /// Return a message through the [`Ret`] instance. This uses 42 | /// `self` which guarantees that it will be called only once. So 43 | /// this will not work on a reference to the [`Ret`] instance. 44 | /// The tuple or struct containing the [`Ret`] instance needs to 45 | /// be destructured to move the [`Ret`] instance out first. 46 | /// 47 | /// [`Ret`]: struct.Ret.html 48 | pub fn ret(mut self, msg: M) { 49 | if let Some(f) = self.0.take() { 50 | f(Some(msg)); 51 | } 52 | } 53 | 54 | /// Create a [`Ret`] instance that performs an arbitrary action 55 | /// with the message on being called. The call is made 56 | /// synchronously at the point that the message is forwarded. 57 | /// `None` is passed if the instance is dropped. 58 | /// 59 | /// [`Ret`]: struct.Ret.html 60 | #[inline] 61 | pub fn new(f: impl FnOnce(Option) + 'static) -> Self { 62 | Self(Some(Box::new(f))) 63 | } 64 | 65 | /// Create a [`Ret`] instance that queues a call to an actor. 66 | /// 67 | /// [`Ret`]: struct.Ret.html 68 | #[allow(clippy::wrong_self_convention)] 69 | #[inline] 70 | pub fn to_actor( 71 | actor: Actor, 72 | f: impl FnOnce(&mut A, &mut Cx<'_, A>, Option) + 'static, 73 | ) -> Self { 74 | // This relies on closures being inlined. We expect this to 75 | // result in just two fully-inlined closures: one boxed and 76 | // returned as the [`Ret`] value, the other appended to the 77 | // defer queue when that is called. 78 | Self::new(move |m| { 79 | let actor2 = actor.clone(); 80 | actor.defer(move |s| actor2.apply(s, move |a, cx| f(a, cx, m))); 81 | }) 82 | } 83 | 84 | /// Create a [`Ret`] instance that queues calls to an actor whilst 85 | /// in the **Prep** phase. Once the actor is **Ready**, any 86 | /// queued prep calls are dropped. 87 | /// 88 | /// [`Ret`]: struct.Ret.html 89 | #[allow(clippy::wrong_self_convention)] 90 | #[inline] 91 | pub fn to_actor_prep( 92 | actor: Actor, 93 | f: impl FnOnce(&mut Cx<'_, A>, Option) -> Option + 'static, 94 | ) -> Self { 95 | Self::new(move |m| { 96 | let actor2 = actor.clone(); 97 | actor.defer(move |s| actor2.apply_prep(s, move |cx| f(cx, m))); 98 | }) 99 | } 100 | 101 | /// Create a [`Ret`] instance that queues a call to an actor if a 102 | /// value is returned, but not if the [`Ret`] instance is dropped. 103 | /// 104 | /// [`Ret`]: struct.Ret.html 105 | #[inline] 106 | pub fn some_to_actor( 107 | actor: Actor, 108 | f: impl FnOnce(&mut A, &mut Cx<'_, A>, M) + 'static, 109 | ) -> Self { 110 | Self::new(move |m| { 111 | if let Some(m) = m { 112 | let actor2 = actor.clone(); 113 | actor.defer(move |s| actor2.apply(s, move |a, cx| f(a, cx, m))); 114 | } 115 | }) 116 | } 117 | 118 | /// Create a [`Ret`] instance that queues calls for returned 119 | /// values (but not if the [`Ret`] instance is dropped) to an 120 | /// actor whilst in the **Prep** phase. Once the actor is 121 | /// **Ready**, any queued prep calls are dropped. 122 | /// 123 | /// [`Ret`]: struct.Ret.html 124 | #[inline] 125 | pub fn some_to_actor_prep( 126 | actor: Actor, 127 | f: impl FnOnce(&mut Cx<'_, A>, M) -> Option + 'static, 128 | ) -> Self { 129 | Self::new(move |m| { 130 | if let Some(m) = m { 131 | let actor2 = actor.clone(); 132 | actor.defer(move |s| actor2.apply_prep(s, move |cx| f(cx, m))); 133 | } 134 | }) 135 | } 136 | 137 | /// Create a [`Ret`] instance that panics with the given message 138 | /// when called 139 | /// 140 | /// [`Ret`]: struct.Ret.html 141 | #[inline] 142 | pub fn panic(msg: impl Into) -> Self { 143 | let msg: String = msg.into(); 144 | Self::new(move |m| { 145 | if m.is_some() { 146 | panic!("{}", msg); 147 | } 148 | }) 149 | } 150 | } 151 | 152 | impl Drop for Ret { 153 | fn drop(&mut self) { 154 | if let Some(f) = self.0.take() { 155 | f(None); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/waker.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inter-thread")] 2 | 3 | //! Test `Waker` functionality 4 | //! 5 | //! This just checks that wakes come through as expected, and that the 6 | //! drop handler runs okay, with a large combination of orderings. A 7 | //! longer test to stress the code multi-threaded for races can be 8 | //! found under `extra/`. 9 | 10 | use crate::time::Instant; 11 | use crate::*; 12 | use std::cell::RefCell; 13 | use std::collections::HashSet; 14 | use std::rc::Rc; 15 | use std::sync::atomic::{AtomicUsize, Ordering}; 16 | use std::sync::Arc; 17 | 18 | // Test it all in one thread so that we have total control over the 19 | // order of operations, to exercise many combinations but reliably and 20 | // repeatably. 21 | // 22 | // Rather than try to specifically test particular cases, instead use 23 | // a pseudo-random generator with a fixed seed to drive the test, and 24 | // then check the output to see that this exercises things 25 | // sufficiently. 26 | // 27 | // MIRI takes 4GB+ and much too long to run this test, so skip. 28 | test_fn!( 29 | #[cfg_attr(miri, ignore)] 30 | fn waker_test() { 31 | let now = Instant::now(); 32 | let mut stakker = Stakker::new(now); 33 | let s = &mut stakker; 34 | 35 | let notified1 = Arc::new(AtomicUsize::new(0)); 36 | let notified2 = notified1.clone(); 37 | s.set_poll_waker(move || notified2.store(1, Ordering::SeqCst)); 38 | let notified = move || 0 != notified1.swap(0, Ordering::SeqCst); 39 | 40 | #[derive(Default)] 41 | struct State { 42 | awaiting: HashSet, 43 | running: HashSet, 44 | } 45 | let state = Rc::new(RefCell::new(State::default())); 46 | 47 | let check_awaiting = || { 48 | if !state.borrow_mut().awaiting.is_empty() { 49 | panic!("Still awaiting call for: {:?}", state.borrow_mut().awaiting); 50 | } 51 | }; 52 | let check_running = || { 53 | if !state.borrow_mut().running.is_empty() { 54 | panic!("Still awaiting drop for: {:?}", state.borrow_mut().running); 55 | } 56 | }; 57 | 58 | let handle_notify = |s: &mut Stakker| { 59 | if notified() { 60 | s.poll_wake(); 61 | s.run(now, false); 62 | } 63 | }; 64 | 65 | // ZX Spectrum 16-bit pseudo-random number generator 66 | let mut seed: usize = 12345; 67 | let mut rand = |n: usize| { 68 | seed = ((seed + 1) * 75) % 65537 - 1; 69 | ((seed * n) >> 16) as usize 70 | }; 71 | 72 | struct IdAndWaker { 73 | id: u32, 74 | waker: Waker, 75 | } 76 | impl std::fmt::Debug for IdAndWaker { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | write!(f, "{}", self.id) 79 | } 80 | } 81 | 82 | let mut id = 0; 83 | let mut new_waker = |s: &mut Stakker| { 84 | id += 1; 85 | let state = state.clone(); 86 | let id = id; 87 | state.borrow_mut().running.insert(id); 88 | IdAndWaker { 89 | id, 90 | waker: s.waker(move |_, deleted| { 91 | let mut st = state.borrow_mut(); 92 | if deleted { 93 | // Should only be deleted once 94 | println!("handle drop {}", id); 95 | assert!(st.running.remove(&id)); 96 | } else { 97 | // Spurious wakeups are okay, so long as there is 98 | // at least one 99 | println!("handle wake {}", id); 100 | st.awaiting.remove(&id); 101 | } 102 | }), 103 | } 104 | }; 105 | 106 | // Start with 320 wakers. We need at least 64 to use more than 107 | // one Leaf in the bitmap. 108 | let mut wakers = Vec::new(); 109 | for _ in 0..320 { 110 | wakers.push(new_waker(s)); 111 | } 112 | 113 | while !wakers.is_empty() { 114 | match rand(4) { 115 | 0 => { 116 | // If there has been a notify, run handlers. This 117 | // should leave the `awaiting` set empty. 118 | println!("RUN"); 119 | handle_notify(s); 120 | check_awaiting(); 121 | } 122 | 1 => { 123 | // Wake a random waker 124 | let w = &wakers[rand(wakers.len())]; 125 | println!("WAKE {}", w.id); 126 | state.borrow_mut().awaiting.insert(w.id); 127 | w.waker.wake(); 128 | } 129 | 2 => { 130 | // Drop a random waker. If we drop a waker and it 131 | // has a call outstanding, that call is lost. 132 | // (Well it is not lost but the drop gets executed 133 | // first.) So remove it from `awaiting`. 134 | let w = wakers.remove(rand(wakers.len())); 135 | state.borrow_mut().awaiting.remove(&w.id); 136 | println!("DROP {}", w.id); 137 | drop(w); 138 | } 139 | _ => { 140 | if rand(100) < 80 { 141 | // Add another waker. This tests re-use of slots. 142 | // Runs 80% of the time, vs 100% for drop, so this 143 | // is biased towards reducing the number of 144 | // wakers, until there are none (to end the test). 145 | let w = new_waker(s); 146 | println!("ADD {}", w.id); 147 | wakers.push(w); 148 | } 149 | } 150 | } 151 | } 152 | handle_notify(s); 153 | 154 | // Expect all waker handlers and all drop handlers to have run 155 | check_awaiting(); 156 | check_running(); 157 | 158 | // Check the number of slots in use in the Slab. The drop handler 159 | // will still be there, but all others should have been cleaned up 160 | assert!(s.poll_waker_handler_count() <= 1); 161 | } 162 | ); 163 | -------------------------------------------------------------------------------- /fixup-doc-links.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | # Version: 2023-07-21, specific to Stakker (special handling of sync/ 4 | # and task/) 5 | # 6 | # This code assumes that it owns all the `[...]: ...` definitions at 7 | # the end of each doc comment block. It deletes them and rewrites 8 | # them according to the links found in the comment block. (Any other 9 | # links must be made inline to avoid being deleted.) 10 | # 11 | # Links in certain fixed formats are recognised and converted into 12 | # links within this same crate or to other crates in the generated 13 | # documentation. A warning is given if the corresponding 14 | # documentation files are not found, so this requires that 15 | # documentation has already been generated. 16 | # 17 | # Recognises structs, enums, methods and macros, optionally with a 18 | # path. By checking generated documentation files, guesses when a 19 | # path refers to an enum rather than a struct, or another crate rather 20 | # than this one. For example: 21 | # 22 | # [`Actor`] 23 | # [`Actor::apply`] 24 | # [`actor!`] 25 | # [`mio::Poll`] 26 | # [`mio::Poll::poll`] 27 | # [`mio::net::TcpStream`] 28 | # [`stakker::actor!`] 29 | 30 | use Cwd qw(cwd); 31 | 32 | my $CRATE = cwd(); 33 | $CRATE =~ s|^.*/([^/]+)/?|$1|; 34 | $CRATE =~ s/-/_/g; 35 | 36 | my $TARGET = $ENV{CARGO_TARGET_DIR}; 37 | $TARGET = "./target" unless defined $TARGET; 38 | 39 | my $DOCDIR = "$TARGET/doc/$CRATE"; 40 | 41 | sub readfile { 42 | my $fn = shift; 43 | die "Can't open file: $fn" unless open READFILE, "<$fn"; 44 | local $/ = undef; 45 | my $data = ; 46 | die unless close READFILE; 47 | return $data; 48 | } 49 | 50 | system("cargo doc"); 51 | 52 | my $inplace = 1; 53 | 54 | my @rs = (); 55 | push @rs, glob('src/*.rs'); 56 | push @rs, glob('src/*/*.rs'); 57 | push @rs, glob('src/*/*/*.rs'); 58 | 59 | my %unknown = (); 60 | my @curr = (); 61 | my $prefix = ''; 62 | my $top = ''; 63 | my @BASES; 64 | 65 | for my $dir ('', 'sync/', 'task/') { 66 | for my $typ (qw(struct enum trait type)) { 67 | push @BASES, "$dir$typ"; 68 | } 69 | } 70 | 71 | sub process_section { 72 | if ($prefix eq '') { 73 | print OUT @curr; 74 | return; 75 | } 76 | 77 | # Drop existing link-defs, and any trailing blank lines 78 | while (@curr > 0 && $curr[@curr-1] =~ m|^\s*//[/!]\s*\[.*\]:|) { 79 | pop @curr; 80 | } 81 | while (@curr > 0 && $curr[@curr-1] =~ m|^\s*//[/!]\s*$|) { 82 | pop @curr; 83 | } 84 | 85 | # Make a list of [`...`] links 86 | my %links = (); 87 | my $data = join('', @curr); 88 | $data =~ s/```.*?```//sg; 89 | while ($data =~ /(\[`[^`(]+`\])[^(]/g) { 90 | $links{$1} = 1; 91 | } 92 | my @linkdefs = (); 93 | 94 | # Generate defs for them 95 | for my $link (sort keys %links) { 96 | my $fn; 97 | my $id; 98 | my $href; 99 | 100 | my $path = ''; 101 | my $tmp = $link; 102 | while ($tmp =~ s/^\[`([a-z][a-z0-9_]*)::/[`/) { 103 | $path = "$path$1/"; 104 | } 105 | 106 | my @href = (); 107 | if ($tmp =~ /^\[`([A-Z][a-zA-Z0-9_]+)`\]$/) { 108 | for my $base (@BASES) { 109 | push @href, "${path}$base.$1.html"; 110 | push @href, "../${path}$base.$1.html" if $path ne ''; 111 | } 112 | } elsif ($tmp =~ /^\[`([A-Z][a-zA-Z0-9_]+)::([a-z][a-z0-9_]+)`\]$/) { 113 | for my $base (@BASES) { 114 | push @href, "${path}$base.$1.html#method.$2"; 115 | push @href, "../${path}$base.$1.html#method.$2" if $path ne ''; 116 | } 117 | } elsif ($tmp =~ /^\[`([A-Z][a-zA-Z0-9_]+)::([A-Z][a-zA-Z0-9_]+)`\]$/) { 118 | push @href, "${path}enum.$1.html#variant.$2"; 119 | push @href, "../${path}enum.$1.html#variant.$2" if $path ne ''; 120 | } elsif ($tmp =~ /^\[`([a-z][a-z0-9_]+)!`\]$/) { 121 | push @href, "${path}macro.$1.html"; 122 | push @href, "../${path}macro.$1.html" if $path ne ''; 123 | } else { 124 | $unknown{$link} = 1; 125 | } 126 | 127 | my $found = 0; 128 | for my $href (@href) { 129 | my $fn = $href; 130 | my $id; 131 | $id = $1 if $fn =~ s/#([^#]*)$//; 132 | my $fnpath = "$DOCDIR/$fn"; 133 | if (-f $fnpath) { 134 | push @linkdefs, "$link: $top$href"; 135 | if (defined $id) { 136 | my $html = readfile($fnpath); 137 | print "WARNING: Missing anchor '$id' in file: $fnpath\n" 138 | unless $html =~ /['"]$id['"]/; 139 | } 140 | $found = 1; 141 | last; 142 | } 143 | } 144 | if (!$found) { 145 | print("WARNING: Doc file not found in any of these locations:\n " . join("\n ", @href) . "\n"); 146 | } 147 | } 148 | 149 | # Append 150 | if (@linkdefs) { 151 | my $linepre = $curr[0]; 152 | $linepre =~ s|^(\s*//[/!]).*$|$1|s; 153 | push @curr, "$linepre\n"; 154 | push @curr, "$linepre $_\n" for (@linkdefs); 155 | } 156 | 157 | print OUT @curr; 158 | } 159 | 160 | my $changes = 0; 161 | for my $fnam (@rs) { 162 | my $ofnam = "$fnam-NEW"; 163 | @curr = (); 164 | $prefix = ''; 165 | 166 | $top = ''; 167 | $top = "../" if $fnam =~ m|/sync/| || $fnam =~ m|/task/|; 168 | 169 | die "Can't open file: $fnam" unless open IN, "<$fnam"; 170 | die "Can't create file: $ofnam" unless open OUT, ">$ofnam"; 171 | while () { 172 | my $pre = ''; 173 | $pre = $1 if m|^\s*(//[/!])|s; 174 | 175 | if ($pre ne $prefix) { 176 | process_section(); 177 | @curr = (); 178 | $prefix = $pre; 179 | } 180 | 181 | push @curr, $_; 182 | } 183 | process_section() if @curr; 184 | die "Failed reading file: $fnam" unless close IN; 185 | die "Failed writing file: $ofnam" unless close OUT; 186 | 187 | if ($inplace) { 188 | my $f1 = readfile($fnam); 189 | my $f2 = readfile($ofnam); 190 | if ($f1 ne $f2) { 191 | if (rename $ofnam, $fnam) { 192 | print "Updated $fnam\n"; 193 | $changes++; 194 | } else { 195 | print "Failed to rename file $ofnam to $fnam\n"; 196 | } 197 | } else { 198 | unlink $ofnam; 199 | } 200 | } 201 | } 202 | 203 | for (sort keys %unknown) { 204 | print "WARNING: Link format not known: $_\n"; 205 | } 206 | 207 | system("cargo doc") unless $changes == 0; 208 | -------------------------------------------------------------------------------- /src/share.rs: -------------------------------------------------------------------------------- 1 | use crate::cell::cell::ShareCell; 2 | use crate::Core; 3 | use std::rc::{Rc, Weak}; 4 | 5 | /// Ref-counted shared mutable data 6 | /// 7 | /// This allows synchronous modification of shared state from actor 8 | /// methods. Note that if you only wish to share immutable data 9 | /// between actors, it's less verbose to simply use `Rc` from the 10 | /// standard library. Also you might wish to handle very small 11 | /// amounts of shared mutable data with `Rc` instead. 12 | /// 13 | /// [`Share`] is provided as a compile-time-checked zero-cost 14 | /// alternative to `Rc`. If [`Share`] didn't exist, the 15 | /// temptation to use `Rc` occasionally would likely be too 16 | /// great. However `Rc` brings the risk of unexpected 17 | /// runtime panics. Modification of one piece of code might 18 | /// unexpectedly cause another distant piece of code to crash, but 19 | /// quite possibly only under certain conditions which makes that 20 | /// failure hard to anticipate in testing. In short `Rc` 21 | /// should be avoided wherever possible. With [`Share`] everything is 22 | /// checked at compile time. 23 | /// 24 | /// Note that using [`Share`], `Rc` or `Rc` breaks the 25 | /// actor model. Using them to share mutable data between actors 26 | /// effectively causes those actors to be bound together in a group. 27 | /// It would be impossible to split one of those actors off to another 28 | /// process or over a remote link. However the actor model still 29 | /// applies to the group's external interface. 30 | /// 31 | /// So this must be used with some knowledge of the trade-offs. It 32 | /// could easily lead to dependency on the order of execution of 33 | /// actors. Treat this as similar in danger level to shared memory 34 | /// between threads or IPC shared memory between processes. You don't 35 | /// need locking however because if you get a mutable reference to the 36 | /// contents you'll have exclusive access until you give it up thanks 37 | /// to Rust's borrow checker. 38 | /// 39 | /// To borrow more than one [`Share`] instance at a time, see 40 | /// [`Core::share_rw2`] and [`Core::share_rw3`]. 41 | /// 42 | /// It's not possible to pass a [`Core`] reference to methods on a 43 | /// [`Share`] item due to borrowing restrictions. If you need a 44 | /// [`Core`] reference, then use arguments of "`this: &Share, 45 | /// core: &mut Core`" instead of "`&mut self`", and do `self` access 46 | /// via `this.rw(core)`. (However if it's getting this complicated, 47 | /// maybe consider whether the shared data should be made into an 48 | /// actor instead, or whether some other approach would be better.) 49 | /// 50 | /// By default borrow-checking of access to the contents of the 51 | /// [`Share`] is handled at compile-time using a `TCell` or `TLCell` 52 | /// with its owner in [`Core`], so it is zero-cost and compiles down 53 | /// to a direct pointer access to the contents of a `struct`, just as 54 | /// if the [`Share`] wasn't there. However cloning a [`Share`] has 55 | /// the normal `Rc` overheads. 56 | /// 57 | /// [`Core::share_rw2`]: struct.Core.html#method.share_rw2 58 | /// [`Core::share_rw3`]: struct.Core.html#method.share_rw3 59 | /// [`Core`]: struct.Core.html 60 | /// [`Share`]: struct.Share.html 61 | pub struct Share { 62 | pub(crate) rc: Rc>, 63 | } 64 | 65 | impl Share { 66 | /// Create a new Share instance 67 | pub fn new(core: &Core, val: T) -> Self { 68 | Self { 69 | rc: Rc::new(core.sharecell_owner.cell(val)), 70 | } 71 | } 72 | 73 | /// Get a read-only (immutable, shared) reference to the contents 74 | /// of the [`Share`] instance. By default this is a static check, 75 | /// which compiles down to a direct access. 76 | /// 77 | /// [`Share`]: struct.Share.html 78 | #[inline] 79 | pub fn ro<'a>(&'a self, core: &'a Core) -> &'a T { 80 | core.sharecell_owner.ro(&self.rc) 81 | } 82 | 83 | /// Get a read-write (mutable, exclusive) reference to the 84 | /// contents of the [`Share`] instance. By default this is a 85 | /// static check, which compiles down to a direct access. 86 | /// 87 | /// To access more than one [`Share`] instance at the same time, 88 | /// see [`Core::share_rw2`] and [`Core::share_rw3`]. 89 | /// 90 | /// [`Core::share_rw2`]: struct.Core.html#method.share_rw2 91 | /// [`Core::share_rw3`]: struct.Core.html#method.share_rw3 92 | /// [`Share`]: struct.Share.html 93 | #[inline] 94 | pub fn rw<'a>(&'a self, core: &'a mut Core) -> &'a mut T { 95 | core.sharecell_owner.rw(&self.rc) 96 | } 97 | 98 | /// Create a [`ShareWeak`] reference to the contained object 99 | /// 100 | /// [`ShareWeak`]: struct.ShareWeak.html 101 | #[inline] 102 | pub fn downgrade(&self) -> ShareWeak { 103 | ShareWeak { 104 | weak: Rc::downgrade(&self.rc), 105 | } 106 | } 107 | 108 | /// Return the number of strong references to the shared data 109 | #[inline] 110 | pub fn strong_count(&self) -> usize { 111 | Rc::strong_count(&self.rc) 112 | } 113 | 114 | /// Return the number of weak references to the shared data 115 | #[inline] 116 | pub fn weak_count(&self) -> usize { 117 | Rc::weak_count(&self.rc) 118 | } 119 | } 120 | 121 | impl Clone for Share { 122 | /// Return another reference to the shared data 123 | fn clone(&self) -> Self { 124 | Self { 125 | rc: self.rc.clone(), 126 | } 127 | } 128 | } 129 | 130 | /// Weak reference to a [`Share`] 131 | /// 132 | /// This reference does not stop the [`Share`] from being dropped, but 133 | /// can be used to recover a [`Share`] reference if it is still alive 134 | /// due to a strong reference held elsewhere. 135 | /// 136 | /// [`Share`]: struct.Share.html 137 | pub struct ShareWeak { 138 | weak: Weak>, 139 | } 140 | 141 | impl ShareWeak { 142 | /// If there are still strong references to the shared data, 143 | /// returns a new [`Share`]. Otherwise the shared data has been 144 | /// dropped, and so returns `None`. 145 | /// 146 | /// [`Share`]: struct.Share.html 147 | pub fn upgrade(&self) -> Option> { 148 | self.weak.upgrade().map(|rc| Share { rc }) 149 | } 150 | 151 | /// Return the number of strong references to the shared data 152 | pub fn strong_count(&self) -> usize { 153 | self.weak.strong_count() 154 | } 155 | 156 | /// Return the number of weak references to the shared data, 157 | /// including this one, or 0 if there are no strong pointers 158 | /// remaining. 159 | pub fn weak_count(&self) -> usize { 160 | self.weak.weak_count() 161 | } 162 | } 163 | 164 | impl Clone for ShareWeak { 165 | /// Return another weak reference to the shared data 166 | fn clone(&self) -> Self { 167 | Self { 168 | weak: self.weak.clone(), 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/test/log.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::str::FromStr; 3 | 4 | test_fn!( 5 | fn loglevels() { 6 | macro_rules! test_conv { 7 | ($level:ident, $str1:literal, $str2:literal, $str3:literal) => { 8 | assert_eq!(Ok(LogLevel::$level), LogLevel::from_str($str1)); 9 | assert_eq!(Ok(LogLevel::$level), LogLevel::from_str($str2)); 10 | assert_eq!(Ok(LogLevel::$level), LogLevel::from_str($str3)); 11 | assert_eq!(format!("{}", LogLevel::$level), $str2); 12 | assert_eq!(LogLevel::$level.name(), $str2); 13 | }; 14 | } 15 | 16 | test_conv!(Trace, "trace", "TRACE", "Trace"); 17 | test_conv!(Debug, "debug", "DEBUG", "Debug"); 18 | test_conv!(Info, "info", "INFO", "Info"); 19 | test_conv!(Warn, "warn", "WARN", "Warn"); 20 | test_conv!(Error, "error", "ERROR", "Error"); 21 | test_conv!(Off, "off", "OFF", "Off"); 22 | test_conv!(Audit, "audit", "AUDIT", "Audit"); 23 | test_conv!(Open, "open", "OPEN", "Open"); 24 | test_conv!(Close, "close", "CLOSE", "Close"); 25 | 26 | assert_eq!(Err(LogLevelError), LogLevel::from_str("zzzzz")); 27 | assert_eq!(format!("{}", LogLevelError), "invalid logging level"); 28 | 29 | for level in LogLevel::all_levels() { 30 | assert_eq!(LogLevel::from_str(level.name()), Ok(*level)); 31 | } 32 | } 33 | ); 34 | 35 | test_fn!( 36 | fn logfilter() { 37 | macro_rules! test_filter { 38 | ($level:ident, $expect:literal) => { 39 | assert_eq!(format!("{}", LogFilter::from(LogLevel::$level)), $expect); 40 | }; 41 | } 42 | 43 | test_filter!(Trace, "LogFilter(TRACE,DEBUG,INFO,WARN,ERROR)"); 44 | test_filter!(Debug, "LogFilter(DEBUG,INFO,WARN,ERROR)"); 45 | test_filter!(Info, "LogFilter(INFO,WARN,ERROR)"); 46 | test_filter!(Warn, "LogFilter(WARN,ERROR)"); 47 | test_filter!(Error, "LogFilter(ERROR)"); 48 | test_filter!(Off, "LogFilter()"); 49 | test_filter!(Audit, "LogFilter(AUDIT)"); 50 | test_filter!(Open, "LogFilter(OPEN,CLOSE)"); 51 | test_filter!(Close, "LogFilter(OPEN,CLOSE)"); 52 | 53 | macro_rules! test_fromstr { 54 | ($in:literal, $expect:literal) => { 55 | assert_eq!(format!("{}", LogFilter::from_str($in).unwrap()), $expect); 56 | }; 57 | } 58 | 59 | test_fromstr!("warn", "LogFilter(WARN,ERROR)"); 60 | test_fromstr!("info,audit", "LogFilter(INFO,WARN,ERROR,AUDIT)"); 61 | test_fromstr!("error,open,audit", "LogFilter(ERROR,AUDIT,OPEN,CLOSE)"); 62 | 63 | macro_rules! test_all { 64 | ($all:expr, $expect:literal) => { 65 | assert_eq!(format!("{}", LogFilter::all(&$all)), $expect); 66 | }; 67 | } 68 | 69 | test_all!([LogLevel::Warn], "LogFilter(WARN,ERROR)"); 70 | test_all!( 71 | [LogLevel::Info, LogLevel::Audit], 72 | "LogFilter(INFO,WARN,ERROR,AUDIT)" 73 | ); 74 | test_all!( 75 | [LogLevel::Error, LogLevel::Open, LogLevel::Audit], 76 | "LogFilter(ERROR,AUDIT,OPEN,CLOSE)" 77 | ); 78 | 79 | assert_eq!( 80 | LogFilter::new() | LogFilter::from(LogLevel::Audit) | LogFilter::from(LogLevel::Error), 81 | LogFilter::from_str("audit,error").unwrap() 82 | ); 83 | 84 | assert_eq!(true, LogFilter::new().is_empty()); 85 | assert_eq!(false, LogFilter::from(LogLevel::Audit).is_empty()); 86 | } 87 | ); 88 | 89 | test_fn!( 90 | #[cfg(feature = "logger")] 91 | fn logger() { 92 | use crate::time::Instant; 93 | use std::fmt::Arguments; 94 | 95 | let now = Instant::now(); 96 | let mut stakker = Stakker::new(now); 97 | let s = &mut stakker; 98 | 99 | struct A; 100 | impl A { 101 | fn init(_: CX![]) -> Option { 102 | Some(Self) 103 | } 104 | fn warn(&self, cx: CX![]) { 105 | let id = cx.id(); 106 | cx.log( 107 | id, 108 | LogLevel::Warn, 109 | "target", 110 | format_args!("Warning"), 111 | |out| out.kv_i64(Some("num"), 1234), 112 | ); 113 | } 114 | fn fail(&self, cx: CX![]) { 115 | fail!(cx, "Called A::fail"); 116 | } 117 | } 118 | 119 | // Don't test the LogVisitor interface fully here. Leave that for 120 | // a logging crate. 121 | #[derive(Default)] 122 | struct CheckVisitor { 123 | found_num_i64: bool, 124 | found_failed_null: bool, 125 | } 126 | impl LogVisitor for CheckVisitor { 127 | fn kv_u64(&mut self, key: Option<&str>, _val: u64) { 128 | panic!("unexpected kv_u64: {:?}", key); 129 | } 130 | fn kv_i64(&mut self, key: Option<&str>, val: i64) { 131 | assert_eq!(key, Some("num")); 132 | assert_eq!(val, 1234); 133 | self.found_num_i64 = true; 134 | } 135 | fn kv_f64(&mut self, key: Option<&str>, _val: f64) { 136 | panic!("unexpected kv_f64: {:?}", key); 137 | } 138 | fn kv_bool(&mut self, key: Option<&str>, _val: bool) { 139 | panic!("unexpected kv_bool: {:?}", key); 140 | } 141 | fn kv_null(&mut self, key: Option<&str>) { 142 | assert_eq!(key, Some("failed")); 143 | self.found_failed_null = true; 144 | } 145 | fn kv_str(&mut self, key: Option<&str>, _val: &str) { 146 | panic!("unexpected kv_str: {:?}", key); 147 | } 148 | fn kv_fmt(&mut self, key: Option<&str>, _val: &Arguments<'_>) { 149 | panic!("unexpected kv_fmt: {:?}", key); 150 | } 151 | fn kv_map(&mut self, key: Option<&str>) { 152 | panic!("unexpected kv_map: {:?}", key); 153 | } 154 | fn kv_mapend(&mut self, key: Option<&str>) { 155 | panic!("unexpected kv_mapend: {:?}", key); 156 | } 157 | fn kv_arr(&mut self, key: Option<&str>) { 158 | panic!("unexpected kv_arr: {:?}", key); 159 | } 160 | fn kv_arrend(&mut self, key: Option<&str>) { 161 | panic!("unexpected kv_arrend: {:?}", key); 162 | } 163 | } 164 | 165 | let mut expect = 0; 166 | s.set_logger( 167 | LogFilter::all(&[LogLevel::Warn, LogLevel::Audit, LogLevel::Open]), 168 | move |core, r| { 169 | assert_eq!(r.id, 1); 170 | expect += 1; 171 | match expect { 172 | 1 => { 173 | assert_eq!(r.level, LogLevel::Open); 174 | assert_eq!(r.target, ""); 175 | assert_eq!(format!("{}", r.fmt), "stakker::test::log::logger::A"); 176 | } 177 | 2 => { 178 | assert_eq!(r.level, LogLevel::Warn); 179 | assert_eq!(r.target, "target"); 180 | assert_eq!(format!("{}", r.fmt), "Warning"); 181 | let mut visitor = CheckVisitor::default(); 182 | (r.kvscan)(&mut visitor); 183 | assert_eq!(true, visitor.found_num_i64); 184 | assert_eq!(false, visitor.found_failed_null); 185 | } 186 | _ => { 187 | assert_eq!(r.level, LogLevel::Close); 188 | assert_eq!(r.target, ""); 189 | assert_eq!(format!("{}", r.fmt), "Called A::fail"); 190 | let mut visitor = CheckVisitor::default(); 191 | (r.kvscan)(&mut visitor); 192 | assert_eq!(false, visitor.found_num_i64); 193 | assert_eq!(true, visitor.found_failed_null); 194 | core.shutdown(StopCause::Stopped); 195 | } 196 | } 197 | }, 198 | ); 199 | 200 | let a = actor!(s, A::init(), ret_nop!()); 201 | call!([a], warn()); 202 | call!([a], fail()); 203 | s.run(now, false); 204 | assert!(matches!(s.shutdown_reason(), Some(StopCause::Stopped))); 205 | } 206 | ); 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Significant feature changes and additions 2 | 3 | This project follows Rust semantic versioning. 4 | 5 | 6 | 7 | ## 0.2.13 (2025-09-08) 8 | 9 | - Fixups for Rust 1.89 clippy recommendations 10 | 11 | ### Added 12 | 13 | - Provisional support for WASM using `wasm-time` 14 | 15 | 16 | ## 0.2.12 (2025-03-11) 17 | 18 | - Fixups for Rust 1.85 warnings and clippy recommendations. In 19 | particular certain safe use of `static mut` is now discouraged 20 | (because it's too easy for beginners to use wrongly) and requires 21 | the warning to be disabled. 22 | 23 | ### Fixed 24 | 25 | - `at!` and `after!` weren't returning `FixedTimerKey` as documented. 26 | Thanks to [Deven Speyrer](https://github.com/dspeyrer) for providing 27 | the PR to fix this issue. 28 | 29 | 30 | ## 0.2.11 (2024-05-28) 31 | 32 | - Fixups for Rust 1.78 warnings and clippy recommendations 33 | 34 | ### Added 35 | 36 | - `Core::anymap_unset` to clear a stored anymap value 37 | 38 | 39 | ## 0.2.10 (2023-08-06) 40 | 41 | - MSRV changed to 1.63 from 1.60, because testing now requires const 42 | Mutex::new 43 | 44 | ### Fixed 45 | 46 | Big testing refactor and rework: 47 | - No longer depend on broken `RUST_TEST_THREADS=1` 48 | - No longer depend on previous script-based workaround 49 | - Get rid of old `test-multi-thread` workaround 50 | - Under `cargo test`, when single-threaded testing is required, run a 51 | worker thread to force all tests to run on a single thread 52 | - However MIRI testing continues to use script-based workaround 53 | 54 | 55 | ## 0.2.9 (2023-07-24) 56 | 57 | ### Added 58 | 59 | - `Channel` to send messages to a Stakker thread from any other thread 60 | 61 | 62 | ## 0.2.8 (2023-07-14) 63 | 64 | ### Added 65 | 66 | - Test MSRV of 1.60 67 | - Use Rust 2021 edition 68 | 69 | ### Fixed 70 | 71 | - Replaced unmaintained `anymap` crate with simple safe inline 72 | implementation 73 | 74 | 75 | ## 0.2.7 (2023-05-09) 76 | 77 | ### Added 78 | 79 | - `LogLevel::try_from(u8)` plus derives: Hash, Ord, PartialOrd 80 | 81 | 82 | ## 0.2.6 (2023-03-29) 83 | 84 | ### Fixed 85 | 86 | - Fixed build failure when "no-unsafe-queue" feature was enabled alone 87 | - Testing has been improved to catch any other similar issues 88 | 89 | 90 | ## 0.2.5 (2022-06-11) 91 | 92 | (maintenance) 93 | 94 | 95 | ## 0.2.4 (2021-05-14) 96 | 97 | ### Added 98 | 99 | - `ActorOwnSlab` / `actor_in_slab!` for basic child actor housekeeping 100 | - `query!` macro for convenient access to `Actor::query` 101 | - `Core::start_instant()` to get runtime startup time 102 | - `ShareWeak` for weak references to `Share` items 103 | 104 | ### Documentation 105 | 106 | - `examples/` folder 107 | 108 | 109 | ## 0.2.3 (2021-04-22) 110 | 111 | ### Fixed 112 | 113 | - On actor `stop!` or `fail!`, if further failures are reported before 114 | the actor method returns, the first stop/fail is now kept and later 115 | ones are discarded, since the first failure is closer to the origin 116 | of the problem. 117 | 118 | 119 | ## 0.2.2 (2021-04-06) 120 | 121 | ### Added 122 | 123 | - Minimum support for asynchronous tasks, in `task` module 124 | 125 | 126 | ## 0.2.1 (2021-02-25) 127 | 128 | (only documentation changes) 129 | 130 | 131 | ## 0.2.0 (2020-11-08) 132 | 133 | ### Breaking changes 134 | 135 | - `PipedThread::new` argument order changed, to avoid need for 136 | temporaries when called 137 | - `Actor::kill`, `Actor::kill_str` and `Actor::owned` moved to 138 | `ActorOwn`. This constrains the power to kill an actor and create 139 | new owners to just owners. Also `kill_str` now only handles 140 | `&'static str`, with `kill_string` added for `String`. If you need 141 | to kill an actor using just an `Actor` reference, then the actor 142 | needs a method added that calls `fail!` 143 | - `Cx::fail_str` now only handles `&'static str`. `Cx::fail_string` 144 | can be used for `String`. `fail!` macro now abstracts this. 145 | - `ActorOwn::new` now requires a parent-ID argument. `actor!` and 146 | `actor_new!` macros are unchanged. 147 | - `StopCause` adds `StopCause::Lost` to allow for future 148 | implementation of remote actors 149 | 150 | ### Added 151 | 152 | - "logger" feature for optional logging support. If enabled, every 153 | actor gets an span-ID. Actor start and stop are logged, with 154 | failure reasons. All logging from an actor uses the span-ID and 155 | supports formatted text and key-value pairs. Logging is 156 | synchronization-free and is directed to a per-Stakker logging 157 | handler, which can output directly, or forward to a logging crate, 158 | or forward to the logging system of the host event loop as required 159 | - `fail!`, `stop!` and `kill!` to express actor termination more 160 | conveniently, including with formatted strings 161 | - `ret_fail!` and `ret_failthru!` to easily cascade actor failure 162 | 163 | ### Changed 164 | 165 | - `Stakker::anymap_set` moved to `Core::anymap_set` so that actors can 166 | set anymap values more conveniently. 167 | 168 | ## 0.1.4 (2020-09-27) 169 | 170 | ### Added 171 | 172 | - `Core::share_rw2` and `Core::share_rw3` to allow borrowing 2 or 3 173 | `Share` instances at once 174 | - Allow using `_` in initial closure arguments within macros. For 175 | example `|_this, _cx|` can now be written `|_, _|` 176 | 177 | ### Fixed 178 | 179 | - `PipedThread` now notifies thread as documented if the actor holding 180 | the `PipedThread` dies. 181 | - `PipedThread` now reports back panic string correctly if thread 182 | panics and the panic was made with a `String` or `&str`. 183 | - A timer set at `cx.now()` in virtual time now executes correctly. 184 | - The flat `FnOnce` queue has been fixed to run cleanly under MIRI 185 | 186 | ### Testing 187 | 188 | - Stress tests of `Waker` across threads (to detect races) 189 | - Stress test of flat `FnOnce` queue expansion 190 | - Fuzz-testing of timer queue 191 | - Fuzz-testing of flat `FnOnce` queue 192 | - Unit tests of queue and timers using small corpora derived from fuzz tests 193 | - Unit tests for `Actor`, `StopCause`, `Share`, `Waker` and `PipedThread` 194 | - Coverage now at 91% 195 | - Tests pass when running under MIRI 196 | 197 | ## 0.1.3 (2020-08-27) 198 | 199 | ### Added 200 | 201 | - Virtual SystemTime: `cx.systime()` and `stakker.set_systime()` 202 | - Synchronous actor queries from outside runtime: `actor.query()` 203 | 204 | ### Changed 205 | 206 | - 'flat' FnOnce queue simplifications and improvements 207 | - Global-based Deferrer access is now branch-free 208 | - General optimisations on hot paths 209 | 210 | ### Fixed 211 | 212 | - Fixed memory leak in 'flat' FnOnce queue cleanup 213 | - Fixed memory leak in Stakker cleanup with inline Deferrer 214 | - Fixed issue with TLS-based Deferrer on cleanup 215 | 216 | ### Testing 217 | 218 | - Valgrind testing script 219 | 220 | ## 0.1.2 (2020-07-16) 221 | 222 | ### Added 223 | 224 | - `actor_of_trait!` 225 | - `ActorOwnAnon` 226 | 227 | ## 0.1.1 (2020-06-20) 228 | 229 | ### Changed 230 | 231 | - Use `$crate::` instead of `stakker::` for macros 232 | - Accept trailing comma in more places in macro args 233 | - `'static` on `Actor::apply_prep` is not necessary 234 | 235 | ## 0.1.0 (2020-04-04) 236 | 237 | ### Changed 238 | 239 | - For `call!`-family and `fwd_to!`-family macros using closures, no 240 | longer require `move` keyword and make all closures implicitly 241 | `move` so that they are `'static` 242 | 243 | ## 0.0.2 (2020-03-04) 244 | 245 | ### Added 246 | 247 | - `Ret` type for compile-time checking of single use, instead of old 248 | `fwd_once_to!` implementation (now dropped). 249 | 250 | - `cx: CX![]` to save on actor method boiler-plate 251 | 252 | ### Changed 253 | 254 | - Big change to notation within macros, using `[]` to contain the 255 | context (`Cx` / `Actor` / `Core`) that the call is targetted 256 | towards, which eliminates need for `__` arguments. This is still 257 | valid Rust syntax, so is formatted automatically by rustfmt. `Fwd` 258 | handling split out of `call!` into `fwd!`. Error reporting of 259 | errors in macro args improved. 260 | 261 | - Multiple `ActorOwn` refs to an actor are now permitted, and the 262 | ref'd actor is terminated only when the last one goes away 263 | 264 | - `Deferrer` is now the main queue, not the lazy queue. This is to 265 | avoid the lazy queue ballooning whilst there's a lot still going on 266 | on the main queue. 267 | 268 | - `Core::timer_max` dropped in favour of `timer_max!` macro which is 269 | more efficient. Similarly for `timer_min`. 270 | 271 | ### Testing 272 | 273 | - Coverage testing with kcov, and many more tests. Includes coverage 274 | testing of macros. 275 | 276 | ## 0.0.1 (2020-01-23) 277 | 278 | First public release 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /src/sync/thread.rs: -------------------------------------------------------------------------------- 1 | use crate::{Core, Fwd, Waker}; 2 | use std::collections::VecDeque; 3 | use std::mem; 4 | use std::panic::AssertUnwindSafe; 5 | use std::sync::{Arc, Condvar, Mutex}; 6 | 7 | // TODO: Reallocate queues occasionally in case they grow huge? 8 | // TODO: Fixed-size queue option to avoid allocations whilst locked? 9 | 10 | // Uses a Mutex internally. We expect contention to be very low, 11 | // since operations are quick and there are only two threads involved. 12 | // So hopefully almost all mutex locks will be handled in userspace, 13 | // not needing to go to the OS. 14 | struct Queues { 15 | mutex: Mutex>, 16 | condvar: Condvar, 17 | } 18 | 19 | struct QueuesInner { 20 | cancel: bool, // Set if PipedThread instance is dropped 21 | panic: Option, // Panic that occurred in the thread, or None 22 | send: VecDeque, 23 | recv: Vec, 24 | } 25 | 26 | /// A thread connected to the actor runtime via channels 27 | /// 28 | /// This takes care of starting a thread and transferring data to and 29 | /// from it via channels. Data sent to the thread has type `O`, and 30 | /// data received has type `I`. These would often be enums to handle 31 | /// different kinds of data (e.g. messages, commands or responses as 32 | /// required). 33 | /// 34 | /// This is useful for offloading synchronous or blocking work to 35 | /// another thread. So the normal pattern of use would be for the 36 | /// thread to block on [`PipedLink::recv`] until it gets something to 37 | /// process. Processing could involve sending messages on other 38 | /// channels or streams and waiting for replies, or running data in 39 | /// parallel through a thread pool. Processing the received message 40 | /// might or might not result in a message to send back with 41 | /// [`PipedLink::send`]. Another use could be for blocking input, 42 | /// where the thread waits on a device, and uses [`PipedLink::send`] 43 | /// to pass back received data. 44 | /// 45 | /// The only thing that this thread can't do is wait for both 46 | /// [`PipedLink::recv`] and some other input at the same time. If you 47 | /// need that, for now you'll need to write your own interface code to 48 | /// `crossbeam` or some other channel library, using [`Waker`] to 49 | /// interface back to **Stakker**. 50 | /// 51 | /// Cleanup is handled as follows: 52 | /// 53 | /// - If the thread terminates normally or panics, then the underlying 54 | /// [`Waker`] notifies the main thread and `fwd_term` is called with 55 | /// the panic error, or `None` if there was no panic. This handler 56 | /// can discard the [`PipedThread`] instance to complete the cleanup, 57 | /// and start a new thread if necessary. 58 | /// 59 | /// - If the [`PipedThread`] instance is dropped in the main thread, 60 | /// then a **cancel** flag is set which the thread will notice next 61 | /// time it tries to send or receive data. The thread should then 62 | /// terminate. So if the [`PipedThread`] instance is kept within the 63 | /// same actor that is handling the incoming data, then this takes 64 | /// care of thread cleanup automatically if the actor fails 65 | /// unexpectedly. 66 | /// 67 | /// [`PipedLink::recv`]: ../sync/struct.PipedLink.html#method.recv 68 | /// [`PipedLink::send`]: ../sync/struct.PipedLink.html#method.send 69 | /// [`PipedThread`]: ../sync/struct.PipedThread.html 70 | /// [`Waker`]: ../sync/struct.Waker.html 71 | pub struct PipedThread { 72 | queues: Arc>, 73 | } 74 | 75 | impl PipedThread { 76 | /// Spawn a new thread. `fwd_recv` will be called for each 77 | /// incoming message. `fwd_term` will be called when the thread 78 | /// terminates with the argument of `None` for normal termination, 79 | /// or `Some(msg)` for a panic. The `run` argument is the closure 80 | /// that will be run within the new thread. The [`PipedLink`] 81 | /// argument passed to it allows the new thread to send and 82 | /// receive messages. 83 | /// 84 | /// Note: `core` argument is third argument so that `fwd_to!` and 85 | /// similar macros can be used directly in the call arguments, 86 | /// without borrow errors. 87 | /// 88 | /// [`PipedLink`]: ../sync/struct.PipedLink.html 89 | pub fn spawn( 90 | fwd_recv: Fwd, 91 | fwd_term: Fwd>, 92 | core: &mut Core, 93 | run: impl FnOnce(&mut PipedLink) + Send + 'static, 94 | ) -> Self { 95 | let queues = Arc::new(Queues { 96 | mutex: Mutex::new(QueuesInner { 97 | cancel: false, 98 | panic: None, 99 | send: VecDeque::new(), 100 | recv: Vec::new(), 101 | }), 102 | condvar: Condvar::new(), 103 | }); 104 | 105 | let qu = queues.clone(); 106 | let waker = core.waker(move |_, deleted| { 107 | let mut panic = None; 108 | let mut lock = qu.mutex.lock().unwrap(); 109 | let recv = mem::take(&mut lock.recv); 110 | if deleted { 111 | panic = lock.panic.take(); 112 | } 113 | drop(lock); 114 | 115 | for msg in recv { 116 | fwd_recv.fwd(msg); 117 | } 118 | if deleted { 119 | fwd_term.fwd(panic); 120 | } 121 | }); 122 | 123 | let mut pipes = PipedLink { 124 | queues: queues.clone(), 125 | waker, 126 | }; 127 | 128 | std::thread::spawn(move || { 129 | if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| run(&mut pipes))) { 130 | // Pass through panic message if it is a `String` or 131 | // `&str`, else generate some debugging output 132 | let msg = match e.downcast::() { 133 | Ok(v) => *v, 134 | Err(e) => match e.downcast::<&str>() { 135 | Ok(v) => v.to_string(), 136 | Err(e) => format!("Panic with unknown type: {:?}", e.type_id()), 137 | }, 138 | }; 139 | pipes.queues.mutex.lock().unwrap().panic = Some(msg); 140 | } 141 | // The Waker is dropped here, which will notify the main 142 | // thread of termination 143 | }); 144 | 145 | Self { queues } 146 | } 147 | 148 | /// Send a message to the thread. If the thread is blocked on 149 | /// receive, wake it. 150 | pub fn send(&mut self, msg: O) { 151 | let mut lock = self.queues.mutex.lock().unwrap(); 152 | let empty = lock.send.is_empty(); 153 | lock.send.push_back(msg); 154 | drop(lock); 155 | 156 | if empty { 157 | self.queues.condvar.notify_all(); 158 | } 159 | } 160 | } 161 | 162 | impl Drop for PipedThread { 163 | fn drop(&mut self) { 164 | self.queues.mutex.lock().unwrap().cancel = true; 165 | self.queues.condvar.notify_all(); 166 | } 167 | } 168 | 169 | /// Link from a [`PipedThread`] thread back to the main thread 170 | /// 171 | /// [`PipedThread`]: ../sync/struct.PipedThread.html 172 | pub struct PipedLink { 173 | queues: Arc>, 174 | waker: Waker, 175 | } 176 | 177 | impl PipedLink { 178 | /// Send a message back to the main thread. Returns `true` on 179 | /// success. If `false` is returned, then the [`PipedThread`] has 180 | /// been dropped and this thread should terminate itself. 181 | /// 182 | /// [`PipedThread`]: ../sync/struct.PipedThread.html 183 | pub fn send(&mut self, msg: I) -> bool { 184 | let mut lock = self.queues.mutex.lock().unwrap(); 185 | let cancel = lock.cancel; 186 | let empty = lock.recv.is_empty(); 187 | lock.recv.push(msg); 188 | drop(lock); 189 | 190 | if empty { 191 | self.waker.wake(); 192 | } 193 | !cancel 194 | } 195 | 196 | /// Receive a message from the main thread. Blocks if there is no 197 | /// message already waiting. Returns `None` if the 198 | /// [`PipedThread`] has been dropped, in which case this thread 199 | /// should terminate itself. 200 | /// 201 | /// [`PipedThread`]: ../sync/struct.PipedThread.html 202 | pub fn recv(&mut self) -> Option { 203 | let mut lock = self.queues.mutex.lock().unwrap(); 204 | while !lock.cancel && lock.send.is_empty() { 205 | lock = self.queues.condvar.wait(lock).unwrap(); 206 | } 207 | if lock.cancel { 208 | None 209 | } else { 210 | Some(lock.send.pop_front().unwrap()) 211 | } 212 | } 213 | 214 | /// Check whether cancellation has been flagged by the main 215 | /// thread. When the [`PipedThread`] is dropped, the cancel flag 216 | /// is set to tell this thread to terminate. If the thread is 217 | /// doing a long-running operation or blocking, it should check 218 | /// the **cancel** flag from time to time to recognise this 219 | /// condition and to clean up in good time. 220 | /// 221 | /// [`PipedThread`]: ../sync/struct.PipedThread.html 222 | pub fn cancel(&mut self) -> bool { 223 | self.queues.mutex.lock().unwrap().cancel 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/timers/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{BoxedFnOnce, Duration, FnOnceQueue, Instant, MaxTimerKey, MinTimerKey, Timers}; 2 | const N_COUNTS: usize = 256; 3 | struct Aux { 4 | start: Instant, 5 | now: Instant, 6 | queue: FnOnceQueue, 7 | seed: u16, 8 | index: usize, 9 | counts: [u8; N_COUNTS], 10 | } 11 | impl Aux { 12 | fn new() -> Self { 13 | let now = Instant::now(); 14 | Self { 15 | start: now, 16 | now, 17 | queue: FnOnceQueue::new(), 18 | seed: 0x1234, 19 | index: 0, 20 | counts: [0; N_COUNTS], 21 | } 22 | } 23 | fn cb(&mut self) -> BoxedFnOnce { 24 | let index = self.index; 25 | self.index = (index + 1) % N_COUNTS; 26 | Box::new(move |aux| aux.counts[index] += 1) 27 | } 28 | fn cb_check(&mut self, check: Instant) -> BoxedFnOnce { 29 | let index = self.index; 30 | self.index = (index + 1) % N_COUNTS; 31 | Box::new(move |aux| { 32 | aux.counts[index] += 1; 33 | assert!( 34 | check <= aux.now, 35 | "Expected to run at {:?}, but now is {:?}", 36 | check, 37 | aux.now 38 | ); 39 | }) 40 | } 41 | fn cb_check_exact(&mut self, check: Instant) -> BoxedFnOnce { 42 | let index = self.index; 43 | self.index = (index + 1) % N_COUNTS; 44 | Box::new(move |aux| { 45 | aux.counts[index] += 1; 46 | assert!( 47 | within_17us_after(check, Some(aux.now)), 48 | "Expected to run at {:?}, but now is {:?}", 49 | check, 50 | aux.now 51 | ); 52 | }) 53 | } 54 | fn advance(&mut self, t: &mut Timers, now: Instant) { 55 | self.now = now; 56 | t.advance(now, &mut self.queue); 57 | let mut queue = std::mem::replace(&mut self.queue, FnOnceQueue::new()); 58 | queue.execute(self); 59 | self.queue = queue; 60 | } 61 | fn rand(&mut self) -> u32 { 62 | // ZX Spectrum random number generator! 63 | self.seed = (((self.seed as u32) + 1) * 75 % 65537 - 1) as u16; 64 | self.seed as u32 65 | } 66 | } 67 | 68 | // We expect the expiry time to be within 17us after the time we 69 | // requested, due to rounding up 70 | fn within_17us_after(target: Instant, actual: Option) -> bool { 71 | if let Some(actual) = actual { 72 | let e0 = target; 73 | let e1 = target + Duration::new(0, 16384); 74 | if e0 <= actual && actual <= e1 { 75 | return true; 76 | } 77 | println!("Out of range:"); 78 | println!(" 17us before: {:?}", e0); 79 | println!(" Actual: {:?}", actual); 80 | println!(" Target: {:?}", target); 81 | } else { 82 | println!("Out of range: Expiry was None"); 83 | } 84 | false 85 | } 86 | 87 | #[test] 88 | fn fixed_short_expire() { 89 | let mut aux = Aux::new(); 90 | let mut t = Timers::new(aux.start); 91 | assert_eq!(None, t.next_expiry()); 92 | let expire = aux.start + Duration::new(3, 123456789); 93 | t.add(expire, aux.cb_check(expire)); 94 | assert!(within_17us_after(expire, t.next_expiry())); 95 | assert_eq!(0, t.slots_used()); 96 | assert_eq!(0, aux.counts[0]); 97 | aux.advance(&mut t, aux.start + Duration::new(4, 0)); 98 | assert_eq!(1, aux.counts[0]); 99 | assert_eq!(None, t.next_expiry()); 100 | } 101 | 102 | #[test] 103 | fn fixed_short_delete() { 104 | let mut aux = Aux::new(); 105 | let mut t = Timers::new(aux.start); 106 | assert_eq!(None, t.next_expiry()); 107 | let expire = aux.start + Duration::new(3, 123456789); 108 | let key = t.add(expire, aux.cb_check(expire)); 109 | assert!(within_17us_after(expire, t.next_expiry())); 110 | assert_eq!(0, t.slots_used()); 111 | assert_eq!(0, aux.counts[0]); 112 | assert!(t.del(key)); 113 | assert_eq!(None, t.next_expiry()); 114 | aux.advance(&mut t, aux.start + Duration::new(4, 0)); 115 | assert_eq!(0, aux.counts[0]); 116 | } 117 | 118 | #[test] 119 | fn fixed_long_expire() { 120 | let mut aux = Aux::new(); 121 | let mut t = Timers::new(aux.start); 122 | assert_eq!(None, t.next_expiry()); 123 | let expire = aux.start + Duration::new(36000, 567891234); // 10+ hours 124 | let expire1 = aux.start + Duration::new(32767, 0); // ~9 hours 125 | t.add(expire, aux.cb_check(expire)); 126 | assert_eq!(1, t.slots_used()); 127 | assert!(within_17us_after(expire1, t.next_expiry())); 128 | assert_eq!(0, aux.counts[0]); 129 | aux.advance(&mut t, aux.start + Duration::new(32768, 0)); 130 | assert_eq!(1, t.slots_used()); 131 | assert!(within_17us_after(expire, t.next_expiry())); 132 | assert_eq!(0, aux.counts[0]); 133 | aux.advance(&mut t, aux.start + Duration::new(40000, 0)); 134 | assert_eq!(1, aux.counts[0]); 135 | assert_eq!(None, t.next_expiry()); 136 | assert_eq!(0, t.slots_used()); 137 | } 138 | 139 | #[test] 140 | fn fixed_long_delete() { 141 | let mut aux = Aux::new(); 142 | let mut t = Timers::new(aux.start); 143 | assert_eq!(None, t.next_expiry()); 144 | let expire = aux.start + Duration::new(36000, 987654321); // 10+ hours 145 | let expire1 = aux.start + Duration::new(32767, 0); // ~9 hours 146 | let key = t.add(expire, aux.cb_check(expire)); 147 | assert_eq!(1, t.slots_used()); 148 | assert!(within_17us_after(expire1, t.next_expiry())); 149 | assert_eq!(0, aux.counts[0]); 150 | assert!(t.del(key)); 151 | assert_eq!(0, t.slots_used()); 152 | assert_eq!(None, t.next_expiry()); 153 | assert_eq!(0, aux.counts[0]); 154 | aux.advance(&mut t, aux.start + Duration::new(40000, 0)); 155 | assert_eq!(0, aux.counts[0]); 156 | } 157 | 158 | #[test] 159 | fn max_expire() { 160 | let mut aux = Aux::new(); 161 | let mut t = Timers::new(aux.start); 162 | let expire1 = aux.start + Duration::new(20000, 567891234); 163 | let expire2 = aux.start + Duration::new(30000, 123456789); 164 | let key = MaxTimerKey::default(); 165 | assert!(!t.mod_max(key, expire1)); 166 | assert!(!t.max_is_active(key)); 167 | let key = t.add_max(expire1, aux.cb()); 168 | assert!(t.max_is_active(key)); 169 | assert_eq!(1, t.slots_used()); 170 | assert!(within_17us_after(expire1, t.next_expiry())); 171 | assert_eq!(0, aux.counts[0]); 172 | assert!(t.mod_max(key, expire2)); 173 | let key2 = t.add_max(expire2, aux.cb()); 174 | assert!(t.max_is_active(key2)); 175 | assert_eq!(2, t.slots_used()); 176 | aux.advance(&mut t, aux.start + Duration::new(22000, 0)); 177 | assert!(t.max_is_active(key)); 178 | assert_eq!(0, aux.counts[0]); 179 | assert_eq!(0, aux.counts[1]); 180 | assert!(within_17us_after(expire2, t.next_expiry())); 181 | assert!(t.del_max(key2)); 182 | assert!(!t.max_is_active(key2)); 183 | assert_eq!(1, t.slots_used()); 184 | aux.advance(&mut t, aux.start + Duration::new(32000, 0)); 185 | assert!(!t.max_is_active(key)); 186 | assert_eq!(1, aux.counts[0]); 187 | assert_eq!(0, aux.counts[1]); 188 | assert_eq!(0, t.slots_used()); 189 | assert_eq!(None, t.next_expiry()); 190 | } 191 | 192 | #[test] 193 | fn min_expire() { 194 | let mut aux = Aux::new(); 195 | let mut t = Timers::new(aux.start); 196 | let expire1 = aux.start + Duration::new(30000, 567891234); 197 | let expire2 = aux.start + Duration::new(20000, 123456789); 198 | let key = MinTimerKey::default(); 199 | assert!(!t.min_is_active(key)); 200 | assert!(!t.mod_min(key, expire1)); 201 | let key = t.add_min(expire1, aux.cb()); 202 | assert!(t.min_is_active(key)); 203 | assert_eq!(1, t.slots_used()); 204 | assert_eq!(0, aux.counts[0]); 205 | assert!(t.mod_min(key, expire2)); 206 | let key2 = t.add_min(expire1, aux.cb()); 207 | assert!(t.min_is_active(key2)); 208 | assert_eq!(2, t.slots_used()); 209 | aux.advance(&mut t, aux.start + Duration::new(22000, 0)); 210 | assert!(!t.min_is_active(key)); 211 | assert!(t.min_is_active(key2)); 212 | assert_eq!(1, aux.counts[0]); 213 | assert_eq!(0, aux.counts[1]); 214 | assert_eq!(1, t.slots_used()); 215 | assert!(t.del_min(key2)); 216 | assert_eq!(0, t.slots_used()); 217 | assert!(!t.min_is_active(key2)); 218 | assert_eq!(None, t.next_expiry()); 219 | aux.advance(&mut t, aux.start + Duration::new(32000, 0)); 220 | assert_eq!(1, aux.counts[0]); 221 | assert_eq!(0, aux.counts[1]); 222 | } 223 | 224 | #[test] 225 | fn min_asymptote() { 226 | let mut aux = Aux::new(); 227 | let mut t = Timers::new(aux.start); 228 | let expiry = aux.start + Duration::from_secs(60); 229 | t.add_min(expiry, aux.cb()); 230 | // Expect this to go roughly (T-15, T-4, T-1, T-0.125, T) 231 | for _ in 1..=5 { 232 | assert_eq!(0, aux.counts[0]); 233 | let exp = t.next_expiry().unwrap(); 234 | //println!("{:?}", expiry - exp); 235 | aux.advance(&mut t, exp); 236 | } 237 | assert_eq!(None, t.next_expiry()); 238 | assert_eq!(1, aux.counts[0]); 239 | } 240 | 241 | #[test] 242 | fn min_rand() { 243 | // Set up a large number of min timers, and check that they 244 | // all expire correctly. 245 | let mut aux = Aux::new(); 246 | let mut t = Timers::new(aux.start); 247 | let unit = Duration::from_secs_f64(60.0 / 65535.0); 248 | for _ in 0..N_COUNTS { 249 | let expire = aux.now + aux.rand() * unit; 250 | t.add_min(expire, aux.cb_check_exact(expire)); 251 | } 252 | while let Some(exp) = t.next_expiry() { 253 | aux.advance(&mut t, exp); 254 | } 255 | for i in 0..N_COUNTS { 256 | assert_eq!(aux.counts[i], 1); 257 | } 258 | } 259 | 260 | #[test] 261 | fn rand_queue() { 262 | // Pseudo-random addition and expiry of min/max/fixed timers 263 | // to exercise internals. Aims to keep timer queue level at 264 | // around 50. 265 | let level = 50.0; 266 | let mut aux = Aux::new(); 267 | let mut t = Timers::new(aux.start); 268 | let unit = Duration::from_secs_f64(54321.987654321 / 65535.0); 269 | let unit_adv = Duration::from_secs_f64(unit.as_secs_f64() / level); 270 | for _ in 0..N_COUNTS { 271 | let expire = aux.now + aux.rand() * unit; 272 | // Mostly fixed, with some min/max mixed in 273 | match aux.rand() % 7 { 274 | 0 => { 275 | t.add_min(expire, aux.cb_check(expire)); 276 | } 277 | 1 => { 278 | t.add_max(expire, aux.cb_check(expire)); 279 | } 280 | _ => { 281 | t.add(expire, aux.cb_check(expire)); 282 | } 283 | } 284 | let adv = aux.rand() * unit_adv; 285 | aux.advance(&mut t, aux.now + adv); 286 | //println!("Queue level: {}", t.queue.len()); 287 | } 288 | while let Some(exp) = t.next_expiry() { 289 | aux.advance(&mut t, exp); 290 | } 291 | for i in 0..N_COUNTS { 292 | assert_eq!(aux.counts[i], 1); 293 | } 294 | } 295 | 296 | #[test] 297 | fn rand_queue_exact() { 298 | // Pseudo-random addition and expiry of fixed timers, checking 299 | // exact end-time. 300 | let mut aux = Aux::new(); 301 | let mut t = Timers::new(aux.start); 302 | let unit = Duration::from_secs_f64(54321.987654321 / 65535.0); 303 | for _ in 0..N_COUNTS { 304 | let rand = aux.rand(); 305 | let expire = aux.now + rand * unit; 306 | t.add(expire, aux.cb_check_exact(expire)); 307 | if 0 != (rand & 1) { 308 | if let Some(exp) = t.next_expiry() { 309 | aux.advance(&mut t, exp); 310 | } 311 | } 312 | } 313 | while let Some(exp) = t.next_expiry() { 314 | aux.advance(&mut t, exp); 315 | } 316 | for i in 0..N_COUNTS { 317 | assert_eq!(aux.counts[i], 1); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2019-2020 Jim Peters 190 | Copyright (c) 2020 `stakker` crate contributors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/test/thread.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inter-thread")] 2 | 3 | //! Test `PipedThread` functionality 4 | 5 | use crate::sync::{Channel, ChannelGuard}; 6 | use crate::time::Instant; 7 | use crate::*; 8 | use std::sync::atomic::{AtomicBool, Ordering}; 9 | use std::sync::{Arc, Condvar, Mutex}; 10 | use std::time::Duration; 11 | 12 | /// Simple channel for sending and waiting for notification events. 13 | /// Returns (send, recv) closures. This means that we can simulate a 14 | /// `mio` waker (or equivalent) for testing without needing to pull in 15 | /// `stakker_mio`. 16 | fn notify_channel() -> (impl Fn() + Send + Sync, impl FnMut() + Send + Sync) { 17 | let pair1 = Arc::new((Mutex::new(0_usize), Condvar::new())); 18 | let pair2 = pair1.clone(); 19 | let mut count = 0; 20 | ( 21 | move || { 22 | let mut lock = pair1.0.lock().unwrap(); 23 | *lock = lock.wrapping_add(1); 24 | pair1.1.notify_one(); 25 | }, 26 | move || { 27 | let mut lock = pair2.0.lock().unwrap(); 28 | while *lock == count { 29 | lock = pair2.1.wait(lock).unwrap(); 30 | } 31 | count = *lock; 32 | }, 33 | ) 34 | } 35 | 36 | // Test thread that accepts a value, does some processing, and then 37 | // returns the result. Also test shutting down the thread from the 38 | // main thread. 39 | test_fn!( 40 | fn pipedthread_processing() { 41 | struct Test { 42 | thread: Option>, 43 | expect: usize, 44 | } 45 | impl Test { 46 | fn init(cx: CX![]) -> Option { 47 | let mut thread = PipedThread::spawn( 48 | fwd_to!([cx], recv() as (usize)), 49 | fwd_to!([cx], term() as (Option)), 50 | cx, 51 | move |link| { 52 | while let Some(v) = link.recv() { 53 | link.send(v * 5); 54 | } 55 | }, 56 | ); 57 | thread.send(1); 58 | Some(Self { 59 | thread: Some(thread), 60 | expect: 5, 61 | }) 62 | } 63 | fn recv(&mut self, _cx: CX![], mut v: usize) { 64 | if v != self.expect { 65 | panic!("Thread returned unexpected value: {} != {}", v, self.expect); 66 | } 67 | if v < 1000 { 68 | v += 1; 69 | self.expect = v * 5; 70 | if let Some(ref mut t) = self.thread { 71 | t.send(v); 72 | } 73 | } else { 74 | // Cause thread shutdown from actor end 75 | self.thread = None; 76 | } 77 | } 78 | fn term(&mut self, cx: CX![], panic: Option) { 79 | if let Some(msg) = panic { 80 | panic!("Unexpected thread failure: {}", msg); 81 | } 82 | cx.stop(); 83 | } 84 | } 85 | 86 | let now = Instant::now(); 87 | let mut stakker = Stakker::new(now); 88 | let s = &mut stakker; 89 | let (tx, mut rx) = notify_channel(); 90 | s.set_poll_waker(tx); 91 | 92 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 93 | s.run(now, false); 94 | while s.not_shutdown() { 95 | rx(); 96 | s.poll_wake(); 97 | s.run(now, false); 98 | } 99 | } 100 | ); 101 | 102 | // Test thread that accepts a value, does some processing, and then 103 | // returns the result. Terminates the thread early to test 104 | // notification from the thread back to the main thread. 105 | test_fn!( 106 | fn pipedthread_terminate() { 107 | struct Test { 108 | thread: PipedThread, 109 | expect: usize, 110 | } 111 | impl Test { 112 | fn init(cx: CX![]) -> Option { 113 | let mut thread = PipedThread::spawn( 114 | fwd_to!([cx], recv() as (usize)), 115 | fwd_to!([cx], term() as (Option)), 116 | cx, 117 | move |link| { 118 | while let Some(v) = link.recv() { 119 | if v > 1000 { 120 | break; // Terminate the thread 121 | } 122 | link.send(v * 5); 123 | } 124 | }, 125 | ); 126 | thread.send(1); 127 | Some(Self { thread, expect: 5 }) 128 | } 129 | fn recv(&mut self, _cx: CX![], mut v: usize) { 130 | if v != self.expect { 131 | panic!("Thread returned unexpected value: {} != {}", v, self.expect); 132 | } 133 | v += 1; 134 | self.expect = v * 5; 135 | self.thread.send(v); 136 | } 137 | fn term(&mut self, cx: CX![], panic: Option) { 138 | if let Some(msg) = panic { 139 | panic!("Unexpected thread failure: {}", msg); 140 | } 141 | cx.stop(); 142 | } 143 | } 144 | 145 | let now = Instant::now(); 146 | let mut stakker = Stakker::new(now); 147 | let s = &mut stakker; 148 | let (tx, mut rx) = notify_channel(); 149 | s.set_poll_waker(tx); 150 | 151 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 152 | s.run(now, false); 153 | while s.not_shutdown() { 154 | rx(); 155 | s.poll_wake(); 156 | s.run(now, false); 157 | } 158 | } 159 | ); 160 | 161 | // Test thread that generates values at intervals and then terminates. 162 | test_fn!( 163 | fn pipedthread_generate() { 164 | struct Test { 165 | _thread: PipedThread, 166 | expect: usize, 167 | } 168 | impl Test { 169 | fn init(cx: CX![]) -> Option { 170 | let thread = PipedThread::spawn( 171 | fwd_to!([cx], recv() as (usize)), 172 | fwd_to!([cx], term() as (Option)), 173 | cx, 174 | move |link| { 175 | for v in 0..10 { 176 | std::thread::sleep(Duration::from_millis(10)); 177 | link.send(v); 178 | } 179 | }, 180 | ); 181 | Some(Self { 182 | _thread: thread, 183 | expect: 0, 184 | }) 185 | } 186 | fn recv(&mut self, _cx: CX![], v: usize) { 187 | if v != self.expect { 188 | panic!("Thread returned unexpected value: {} != {}", v, self.expect); 189 | } 190 | self.expect += 1; 191 | } 192 | fn term(&mut self, cx: CX![], panic: Option) { 193 | if let Some(msg) = panic { 194 | panic!("Unexpected thread failure: {}", msg); 195 | } 196 | assert_eq!(self.expect, 10); 197 | cx.stop(); 198 | } 199 | } 200 | 201 | let now = Instant::now(); 202 | let mut stakker = Stakker::new(now); 203 | let s = &mut stakker; 204 | let (tx, mut rx) = notify_channel(); 205 | s.set_poll_waker(tx); 206 | 207 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 208 | s.run(now, false); 209 | while s.not_shutdown() { 210 | rx(); 211 | s.poll_wake(); 212 | s.run(now, false); 213 | } 214 | } 215 | ); 216 | 217 | // Test thread that accepts values at intervals and then terminates. 218 | test_fn!( 219 | fn pipedthread_sink() { 220 | struct Test { 221 | _thread: PipedThread, 222 | } 223 | impl Test { 224 | fn init(cx: CX![]) -> Option { 225 | let mut thread = PipedThread::spawn( 226 | fwd_panic!("Not expecting thread to send data"), 227 | fwd_to!([cx], term() as (Option)), 228 | cx, 229 | move |link| { 230 | let mut expect = 0; 231 | while let Some(v) = link.recv() { 232 | assert_eq!(expect, v); 233 | expect += 1; 234 | std::thread::sleep(Duration::from_millis(10)); 235 | if expect == 10 { 236 | break; 237 | } 238 | } 239 | }, 240 | ); 241 | for v in 0..10 { 242 | thread.send(v); 243 | } 244 | Some(Self { _thread: thread }) 245 | } 246 | fn term(&mut self, cx: CX![], panic: Option) { 247 | if let Some(msg) = panic { 248 | panic!("Unexpected thread failure: {}", msg); 249 | } 250 | cx.stop(); 251 | } 252 | } 253 | 254 | let now = Instant::now(); 255 | let mut stakker = Stakker::new(now); 256 | let s = &mut stakker; 257 | let (tx, mut rx) = notify_channel(); 258 | s.set_poll_waker(tx); 259 | 260 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 261 | s.run(now, false); 262 | while s.not_shutdown() { 263 | rx(); 264 | s.poll_wake(); 265 | s.run(now, false); 266 | } 267 | } 268 | ); 269 | 270 | // Test that the panic is notified back correctly when the thread 271 | // panics 272 | test_fn!( 273 | fn pipedthread_panic() { 274 | struct Test { 275 | _thread: PipedThread, 276 | } 277 | impl Test { 278 | fn init(cx: CX![]) -> Option { 279 | let thread = PipedThread::spawn( 280 | fwd_panic!("Not expecting thread to send data"), 281 | fwd_to!([cx], term() as (Option)), 282 | cx, 283 | move |_| { 284 | std::thread::sleep(Duration::from_millis(10)); 285 | panic!("TEST PANIC"); 286 | }, 287 | ); 288 | Some(Self { _thread: thread }) 289 | } 290 | fn term(&mut self, cx: CX![], panic: Option) { 291 | if let Some(msg) = panic { 292 | if msg != "TEST PANIC" { 293 | panic!("Unexpected thread failure: {}", msg); 294 | } 295 | } else { 296 | panic!("Unexpected successful completion of thread"); 297 | } 298 | cx.stop(); 299 | } 300 | } 301 | 302 | let now = Instant::now(); 303 | let mut stakker = Stakker::new(now); 304 | let s = &mut stakker; 305 | let (tx, mut rx) = notify_channel(); 306 | s.set_poll_waker(tx); 307 | 308 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 309 | s.run(now, false); 310 | while s.not_shutdown() { 311 | rx(); 312 | s.poll_wake(); 313 | s.run(now, false); 314 | } 315 | } 316 | ); 317 | 318 | // Test sending data with a `Channel`, and test "close" behaviour. 319 | test_fn!( 320 | fn channel() { 321 | struct Test { 322 | expect: usize, 323 | guard: Option, 324 | done: Arc, 325 | } 326 | impl Test { 327 | fn init(cx: CX![]) -> Option { 328 | let fwd = fwd_to!([cx], recv() as (usize)); 329 | let (channel, guard) = Channel::new(cx, fwd); 330 | let done = Arc::new(AtomicBool::new(false)); 331 | let done_clone = done.clone(); 332 | std::thread::spawn(move || { 333 | for value in 0..20 { 334 | std::thread::sleep(Duration::from_millis(10)); 335 | if !channel.send(value) { 336 | // Main thread closes channel after 9 337 | // received. Give it a generous 50ms for the 338 | // close to go through. 339 | assert!(value >= 10 && value < 15, "Test system overloaded?"); 340 | done_clone.store(true, Ordering::SeqCst); 341 | return; 342 | } 343 | } 344 | panic!("Test system overloaded?"); 345 | }); 346 | Some(Self { 347 | expect: 0, 348 | guard: Some(guard), 349 | done, 350 | }) 351 | } 352 | fn recv(&mut self, cx: CX![], value: usize) { 353 | assert_eq!(value, self.expect); 354 | self.expect += 1; 355 | if self.expect == 10 { 356 | // Drop guard (closing channel) 357 | self.guard = None; 358 | // Wait for thread to finish. This is blocking code 359 | // but it's just for testing 360 | for _ in 0..50 { 361 | if self.done.load(Ordering::SeqCst) { 362 | return cx.stop(); 363 | } 364 | std::thread::sleep(Duration::from_millis(1)); 365 | } 366 | panic!("Thread did not stop within 50ms"); 367 | } 368 | } 369 | } 370 | 371 | let now = Instant::now(); 372 | let mut stakker = Stakker::new(now); 373 | let s = &mut stakker; 374 | let (tx, mut rx) = notify_channel(); 375 | s.set_poll_waker(tx); 376 | 377 | let _actor = actor!(s, Test::init(), ret_shutdown!(s)); 378 | s.run(now, false); 379 | while s.not_shutdown() { 380 | rx(); 381 | s.poll_wake(); 382 | s.run(now, false); 383 | } 384 | } 385 | ); 386 | --------------------------------------------------------------------------------