Compare commits
14
Commits
301e3463e3
...
v0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741c10337b | ||
|
|
e570138da5 | ||
|
|
415effb2e9 | ||
|
|
849a424c8e | ||
|
|
6ceb138f5f | ||
|
|
250f31265b | ||
|
|
9c8f59ca53 | ||
|
|
1002777ef3 | ||
|
|
8f2d513940 | ||
|
|
ca1c98336e | ||
|
|
95306c7f60 | ||
|
|
1262cc30e3 | ||
|
|
461fe4b768 | ||
|
|
b937f1f50f |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "smarm"
|
name = "smarm"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
# smarm
|
# smarm
|
||||||
|
|
||||||
> SMARM — Smarm, Marks Actor Runtime Machinery. A proof-of-concept green-thread actor runtime for Rust.
|
> SMARM: Smarm, Marks Actor Runtime Machinery. A proof-of-concept green-thread actor runtime for Rust.
|
||||||
|
|
||||||
Implements the core ideas in [`Achitecture.md`](.docs/Architecture.md): green-thread actors on a
|
SMARM is my attempt to implement the erlang/OTP philosophy in the Rust programming language. This has yielded a fault-tolerant, fast, and scalable runtime. This runtime allows the creation of asynchronous applications in Rust without the function coloring associated with the async/await system. It encourages the writing of simple, synchronous code, and largely elides the need for lifetime annotations.
|
||||||
shared heap, scheduled cooperatively, communicating only by `Send` messages.
|
|
||||||
Erlang's isolation model without Erlang's copying GC, Rust's zero-copy
|
|
||||||
ownership transfers without async's function colouring.
|
|
||||||
|
|
||||||
The scheduler is multi-threaded — one OS thread per available CPU, all drawing
|
|
||||||
from a shared run queue. The single-threaded `run()` entry point is kept as a
|
|
||||||
convenience wrapper around `runtime::init(Config::exact(1)).run(f)`.
|
|
||||||
|
|
||||||
## What's here
|
## Overview
|
||||||
|
|
||||||
|
SMARM implements green-thread actors on a shared heap, communicating only by `Send` messages. By sharing the heap, SMARM avoids the copying overhead of Erlang, which is safe to do due to Rust's borrow checker.
|
||||||
|
|
||||||
|
On top of the core runtime mechanics, SMARM also provides a library of primitives for making applications closely inspired by erlang/OTP. This includes generic servers (gen_servers), generic state machines (gen_statem), and supervision trees.
|
||||||
|
|
||||||
|
Supervision trees are the core primitive to allow your application to survive an unexpected panic. Supervisors are processes dedicated to monitoring other processes, which can restart these should they fail. This means that when set up properly an application may 'self-heal' when encountering unforeseen circumstances.
|
||||||
|
|
||||||
|
SMARM is not cooperatively scheduled; it uses preemption. This means a heavy task will not starve out other lighter tasks. Everything will make steady progress, which translates to very beneficial behaviour under (over)load: average latency goes up, but tail latency does not blow up.
|
||||||
|
|
||||||
|
To help diagnose these unforeseen circumstances, smarm may be compiled with its `tracing` feature, which emits a full trace using [Perfetto](https://perfetto.dev/).
|
||||||
|
|
||||||
|
Should you want to optimize your application, SMARM is unusually well poised to help. As the runtime functionally controls time, SMARM comes with a built in causal profiler, under the `causal` feature.
|
||||||
|
|
||||||
|
I also built a Phoenix-Framework inspired HTTP 1.1 library on top of SMARM called [URUS](https://git.kalsbeek.dev/Markk116/urus), which implements Pub/Sub, Channels, and basic amenities like Websockets and Server Sent Events.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
This runtime requires naked assembly to function, and has thus far only been implemented for x86-64 assembly. It expects an operating system that supports virtual address space, and is therefore not (yet) suited for embedded targets. The IO implementation is currently based around the Linux kernel's `epoll` mechanism, meaning it requires a (GNU+)Linux distribution to run.
|
||||||
|
|
||||||
|
The preemption mechanism works by wrapping the memory allocator and checking how many CPU cycles you have used compared to your timeslice budget. This allows preemption to fire in most normal code, but tight zero-allocation loops do not get caught and require manual insertion of `check!()` if you want preemption to function.
|
||||||
|
|
||||||
|
This library is still in its early stages, and while I try my best with loom and tests, stable operation cannot be guaranteed. Therefore it is not (yet) recommended for production use.
|
||||||
|
|
||||||
|
At this moment, stack memory for each green thread is capped. Uncapping this may lead to performance benefits for deeply recursive algorithms that in a traditional async runtime might require pointer-chases through the heap. This is as yet unrealised.
|
||||||
|
|
||||||
|
At this stage, the codebase is largely LLM-generated, which is obvious if you start to read through the internals. While I did the design, and I keep the LLM under tight rein, the codebase is not in a state that I am very happy with. This also goes for the documentation.
|
||||||
|
|
||||||
| Module | What it does |
|
|
||||||
|--------------|------------------------------------------------------------------------|
|
|
||||||
| `stack` | `mmap`'d growable stack with guard page; SIGSEGV on overflow |
|
|
||||||
| `context` | `#[naked]` x86-64 context-switch shims, callee-saved regs only |
|
|
||||||
| `preempt` | Allocator-driven preemption; `check!()` macro for no-alloc loops |
|
|
||||||
| `pid` | `(index, generation)` PIDs; stale handles are detectable, not silent |
|
|
||||||
| `actor` | Trampoline + `catch_unwind` boundary at the actor entry point |
|
|
||||||
| `scheduler` | Run queue, slot table, spawn/join, parking, idle path |
|
|
||||||
| `channel` | Unbounded MPSC channel; `recv` parks the actor; `recv_timeout` bounds it; `select`/`select_timeout` park on many receivers at once (ready-index, priority order) |
|
|
||||||
| `mutex` | `Mutex<T>` with mandatory timeout; FIFO waiters; parks the green thread |
|
|
||||||
| `timer` | Min-heap of `(deadline, reason)`; `Sleep` and `WaitTimeout` reasons |
|
|
||||||
| `io` | `block_on_io` for blocking work; `wait_readable`/`wait_writable` + `read`/`write` via epoll |
|
|
||||||
| `supervisor` | `Signal::Exit`/`Panic`/`Stopped` funnelled to a parent; `OneForOne`/`OneForAll`/`RestForOne` strategies + restart-intensity cap |
|
|
||||||
| `monitor` | `monitor(pid)` → `Monitor { id, target, rx }`; one-shot `Down` via `rx`; `demonitor(&m)` tears one registration down; unidirectional death notice |
|
|
||||||
| `link` | bidirectional `link`/`unlink`; abnormal death propagates (cooperative stop, or an `ExitSignal` message under `trap_exit`) |
|
|
||||||
| `gen_server` | `call`/`call_timeout` (sync request-reply) / `cast` (async) over one inbox; `handle_info` over static info arms + `handle_down` via `Watcher`-fed monitors, selected ahead of the inbox; `ServerRef`/`ServerBuilder` + `init`/`terminate` hooks; server-down via channel closure |
|
|
||||||
| `registry` | `register`/`whereis`/`name_of`: name ↔ pid bimap; lazy generation-checked cleanup |
|
|
||||||
|
|
||||||
## Quick taste
|
## Quick taste
|
||||||
|
|
||||||
@@ -51,6 +54,29 @@ run(|| {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stopping actors
|
||||||
|
|
||||||
|
Two strengths, as in OTP. `request_stop(pid)` is `exit(Pid, kill)`: a cooperative
|
||||||
|
hard stop, unwinding at the actor's next observation point. `request_shutdown(pid)`
|
||||||
|
is `exit(Pid, shutdown)`: an actor that traps exits (`trap_exit()`, or
|
||||||
|
`ctx.trap_exit()` in a gen_server / `cx.trap_exit()` in a gen_statem) receives it
|
||||||
|
as a signal — `handle_shutdown` / a `shutdown` row — and may drain before stopping
|
||||||
|
itself; one that does not trap is stopped outright. Supervisors trap:
|
||||||
|
`request_shutdown(sup)` tears the tree down top-down, each child per its
|
||||||
|
`ChildSpec` `Shutdown` policy (`Timeout(d)`, `Infinity`, `BrutalKill`). The run's
|
||||||
|
root actor returning means "the program is done": every top-level actor gets a
|
||||||
|
`request_shutdown`, and `run()` returns when they are gone. From outside the
|
||||||
|
runtime (a signal thread), `Runtime::handle().request_shutdown(pid)` does the
|
||||||
|
same. `examples/graceful_shutdown.rs` shows all of it.
|
||||||
|
|
||||||
|
A gen_server or gen_statem lives until it stops, is shut down, or is killed; its
|
||||||
|
refs are addresses — dropping them never ends it (a forgotten one is swept at
|
||||||
|
root exit; with `--features smarm-trace` each such sweep is a `root_sweep`
|
||||||
|
trace line). The supervised shape is `GenServerBuilder::named(N).run()` /
|
||||||
|
`gen_statem::run_named(N, m)`: the server runs inline as the `ChildSpec` child
|
||||||
|
itself, so the supervisor's shutdown reaches it directly, a restart re-binds the
|
||||||
|
name, and the program addresses it by name.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -67,12 +93,9 @@ benches/
|
|||||||
|
|
||||||
## Building and running
|
## Building and running
|
||||||
|
|
||||||
Standard Cargo. Requires Rust 1.95 or newer (the `#[naked]` attribute went stable
|
Standard Cargo. Requires Rust 1.95 or newer (the `#[naked]` attribute went stable in 1.88; we use a few unrelated post-1.88 features). I have worked hard to keep this library as dependency-free as possible. `master` is x86-64 Linux only. An experimental, **untested** aarch64 context-switch backend lives on the `arm-port` branch (extracted into a `target_arch`-gated `src/arch/`); it has not been validated on hardware yet. macOS remains on the deferred list because of the epoll dependency.
|
||||||
in 1.88; we use a few unrelated post-1.88 features). `master` is x86-64 Linux
|
|
||||||
only. An experimental, **untested** aarch64 context-switch backend lives on the
|
|
||||||
`arm-port` branch (extracted into a `target_arch`-gated `src/arch/`); it has not
|
|
||||||
been validated on hardware yet. macOS remains on the deferred list because of the
|
|
||||||
epoll dependency.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test # all tests
|
cargo test # all tests
|
||||||
@@ -80,26 +103,29 @@ cargo test --test mutex # one module
|
|||||||
cargo bench # primes benchmark vs tokio
|
cargo bench # primes benchmark vs tokio
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's not here
|
|
||||||
|
|
||||||
See the **Defer** section of `Architecture.md`.
|
|
||||||
`join!` for handle groups, stack growth via remap,
|
|
||||||
hierarchical timer wheel, fd-wait timeouts, `Signal::Timeout`. Each is
|
|
||||||
mechanism we know how to add; none belongs in this iteration.
|
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
| Document | What it covers |
|
| Document | What it covers |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`Architecture.md`](./docs/Architecture.md) | Design intent, runtime model, and deferred work |
|
| [`Architecture.md`](./docs/Architecture.md) | Design intent, runtime model, and deferred work |
|
||||||
| [`smarm - Deep Dive.html`](./docs/smarm%20-%20Deep%20Dive.html) | Generated walkthrough of the system; good starting point |
|
| [`smarm - Deep Dive.html`](./docs/smarm%20-%20Deep%20Dive.html) | Generated walkthrough of the system; good starting point if you want to learn about the internals |
|
||||||
| [`BENCHMARKS_AND_TUNING.md`](./docs/BENCHMARKS_AND_TUNING.md) | Where smarm wins and loses vs tokio, preemption knob recommendations |
|
| [`BENCHMARKS_AND_TUNING.md`](./docs/BENCHMARKS_AND_TUNING.md) | Where smarm wins and loses vs tokio, preemption knob recommendations |
|
||||||
| [`benchmarks.md`](./docs/benchmarks.md) | Raw benchmark results, methodology, and tuning experiment log |
|
| [`benchmarks.md`](./docs/benchmarks.md) | Raw benchmark results, methodology, and tuning experiment log |
|
||||||
|
|
||||||
|
## Coming up
|
||||||
|
|
||||||
|
Clustering: clustering multiple SMARM nodes together is in the pipeline.
|
||||||
|
SMARM-BEAM Interop: Running SMARM as a supervised node under the BEAM via a Rustler NIF works, including message passing and supervision trees that span the runtimes. However, this library is still too unstable to release.
|
||||||
|
SMARM is an interesting platform for implementing a 'dataflow' library, but work on this has not yet started.
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This is a personal proof-of-concept. There's no PR workflow. If you fork it and do something interesting, just send me an email. If it's nice, I'll upstream the changes.
|
This started as a personal proof-of-concept, but it is starting to outgrow that name. If you want to contribute, please get in contact to discuss what you want to work on. Code without prior communication is not welcome.
|
||||||
|
|
||||||
|
|
||||||
|
## A note on open source
|
||||||
|
|
||||||
|
An open source project is a gift, and by giving it, it is no longer mine. I highly enourage you to fork it, to make it your own. This repository, however, is still mine.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+48
-4
@@ -77,10 +77,10 @@ Delivered surface:
|
|||||||
`ServerBuilder::start` untouched; free `call` / `cast` / `whereis_server`;
|
`ServerBuilder::start` untouched; free `call` / `cast` / `whereis_server`;
|
||||||
`ServerRef::shutdown` + free `shutdown` as the sys-style synchronous stop.
|
`ServerRef::shutdown` + free `shutdown` as the sys-style synchronous stop.
|
||||||
- **Root-exit teardown** (final phase): the run's initial actor is the root;
|
- **Root-exit teardown** (final phase): the run's initial actor is the root;
|
||||||
when it exits, the scheduler's idle verdict stops the parked-forever remainder
|
when it exits the run winds down. *(Reworked with the graceful-shutdown work:
|
||||||
(deferred past the queue drain, so actors with in-flight work finish rather
|
root exit now delivers `request_shutdown` to every forest root — see
|
||||||
than unwinding on the stop). Closes the "app actor blocks AllDone" stall — see
|
"Root exit" below and `tests/root_exit.rs`.)* Closes the "app actor blocks
|
||||||
Look into, below.
|
AllDone" stall — see Look into, below.
|
||||||
|
|
||||||
Extends — does not retire — the "select exists; a unified per-process mailbox
|
Extends — does not retire — the "select exists; a unified per-process mailbox
|
||||||
still does not" invariant: 014 adds addressable *delivery*, not a unified inbox;
|
still does not" invariant: 014 adds addressable *delivery*, not a unified inbox;
|
||||||
@@ -260,8 +260,52 @@ path the atomic-bool workaround stood in for. Re-check the urus crud repro to
|
|||||||
confirm the workaround can be retired (the teardown is cooperative — an actor in
|
confirm the workaround can be retired (the teardown is cooperative — an actor in
|
||||||
a tight loop with no observation point still can't be stopped).
|
a tight loop with no observation point still can't be stopped).
|
||||||
|
|
||||||
|
**Update (graceful shutdown):** the RFC 014 sweep was a hard `request_stop` of
|
||||||
|
every live slot, deferred until nothing was runnable — which killed a sleeping
|
||||||
|
actor (timer pending) but drained a queued one, for no principled reason. It is
|
||||||
|
now the OTP semantics: root exit = "the program is done" = `request_shutdown`
|
||||||
|
to every **forest root** (live actor whose parent is the run or is dead), run
|
||||||
|
synchronously on the root's finalize path. Supervisors cascade with their child
|
||||||
|
`Shutdown` policies; trapping actors may `Continue`/drain (timers keep working)
|
||||||
|
and end the run when they stop themselves; non-trapping actors are stopped
|
||||||
|
outright — `join` what you need finished. No forcing sweep follows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Open items from the graceful-shutdown work (not scheduled)
|
||||||
|
- ~~gen_server / gen_statem as a direct supervised child.~~ Done: lifetime is
|
||||||
|
the actor's (refs are addresses, the loop holds an inbox sender);
|
||||||
|
`NamedGenServerBuilder::run` / `gen_statem::run_named` run the loop inline as
|
||||||
|
the `ChildSpec` child; the root-exit sweep traces each leftover as
|
||||||
|
`root_sweep` under `smarm-trace`.
|
||||||
|
- Supervisor `Live` drop-guard sweep is `request_stop` (kill propagates as
|
||||||
|
kill); OTP would deliver a trappable `killed`. Chosen for boundedness.
|
||||||
|
- A root-exit shutdown reaches only actors live *at that instant*; a
|
||||||
|
non-trapping forest root that spawns before it unwinds leaves that spawn
|
||||||
|
to itself (Erlang: an unlinked spawn is nobody's child).
|
||||||
|
- **Supervisor start *order* is not start *readiness*.** `start_child`
|
||||||
|
spawns and moves straight on, so an earlier child is merely *scheduled*,
|
||||||
|
not initialised, when a later sibling starts. A later child that resolves
|
||||||
|
an earlier one by name (`whereis_server`) can therefore miss it — the
|
||||||
|
classic "named registry sibling, then its consumers" tree. Ordered
|
||||||
|
`OneForOne`/`RestForOne` shutdown is unaffected (reverse order is honoured
|
||||||
|
and each stop *is* awaited); this is a start-side gap only.
|
||||||
|
Making `spawn` itself block does NOT fix it — it would only shrink the
|
||||||
|
window to "child has begun executing", while the property callers need is
|
||||||
|
"child has bound its name / opened its socket", which only the child can
|
||||||
|
declare. It would also tax the hot path (one round-trip per accepted
|
||||||
|
connection) and turn every spawn into a context-switch point. OTP has the
|
||||||
|
same async `spawn` and puts the synchronisation one level up:
|
||||||
|
`gen_server:start_link` blocks the caller until `init/1` returns.
|
||||||
|
Fix shape when scheduled: a readiness ack in the supervisor's child-start
|
||||||
|
path (`ChildSpec` variant whose factory receives a ready-signal;
|
||||||
|
`NamedGenServerBuilder::run` acks after its name bind, gen_server default
|
||||||
|
acks after `init`; plain closures ack at spawn as today, i.e. opt-in with
|
||||||
|
no cost to existing children). Until then the workaround is structural:
|
||||||
|
have the registrar spawn its own consumers so the ordering is program
|
||||||
|
order inside one actor, not a cross-actor guarantee (urus v0.3 endpoint
|
||||||
|
does exactly this).
|
||||||
|
|
||||||
## Invariants & gotchas (respect these across all cycles)
|
## Invariants & gotchas (respect these across all cycles)
|
||||||
|
|
||||||
- **Shared mutex is non-reentrant.** `Sender::send` can call `unpark` →
|
- **Shared mutex is non-reentrant.** `Sender::send` can call `unpark` →
|
||||||
|
|||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
# urus / smarm handoff — updated 2026-08-19 (session 3)
|
||||||
|
|
||||||
|
## TL;DR for the next session
|
||||||
|
**smarm is done for now** (5 unpushed commits on local `master`, see below).
|
||||||
|
**Next = urus v0.3 endpoint refactor.** You should NOT need to read smarm
|
||||||
|
scheduler internals; the contract you build on is fully described here and in
|
||||||
|
`smarm_full/examples/graceful_shutdown.rs` (read that file first — it is the
|
||||||
|
exact shape urus's tree will take) plus `smarm_full/tests/root_exit.rs`.
|
||||||
|
|
||||||
|
### The smarm contract urus builds on (all on local master, verified by tests)
|
||||||
|
- `request_stop(pid)` = kill (cooperative hard stop). `request_shutdown(pid)` =
|
||||||
|
polite: trapping target gets `ExitSignal{reason: Shutdown}`, non-trapping is
|
||||||
|
stopped outright. `RuntimeHandle::{request_stop,request_shutdown}` do the same
|
||||||
|
from any OS thread (signal handler); grab `rt.handle()` before `rt.run`.
|
||||||
|
- Supervisor traps; `request_shutdown(sup)` = ordered reverse-start shutdown,
|
||||||
|
per-child `ChildSpec::shutdown(Shutdown::{Timeout(d)|Infinity|BrutalKill})`
|
||||||
|
(default Timeout(5s)); sup then returns normally. `request_stop(sup)`
|
||||||
|
hard-stops children too (no orphans).
|
||||||
|
- gen_server: `ctx.trap_exit()` in init; `handle_shutdown() -> Exit|Continue`;
|
||||||
|
`handle_exit(sig)`; `ctx.stop_handle().stop()` = normal self-exit;
|
||||||
|
`terminate()` may block only on the graceful path (Exit / stop / inbox close).
|
||||||
|
`GenServerRef::shutdown()` is graceful and waits.
|
||||||
|
- gen_statem: same in event clothes — `cx.trap_exit()` in initial enter,
|
||||||
|
`shutdown` rows (default `stop`), `exit sig` rows, `cx.stop()` / `stop` tail,
|
||||||
|
optional `terminate { }` block. `GenStatemRef::shutdown()`.
|
||||||
|
- **Root exit = program done**: when the root actor returns, the runtime
|
||||||
|
`request_shutdown`s every *forest root* (live actor whose parent is the run
|
||||||
|
or dead). Supervisors cascade; trapping actors may drain (timers keep
|
||||||
|
working) and end the run when they stop; non-trapping are stopped; **no
|
||||||
|
forcing sweep** (`join` what must finish). The old "wait until nothing
|
||||||
|
runnable then kill all" deferral is gone.
|
||||||
|
- **Gotcha for urus:** a gen_server's lifetime is governed by its refs — drop
|
||||||
|
the last `GenServerRef` and the inbox closes → clean exit *even mid-drain*.
|
||||||
|
The endpoint must be pinned (named, or its ref held by the supervisor
|
||||||
|
wrapper) or it will terminate the moment the root drops its ref.
|
||||||
|
- **Known gap (ROADMAP open item):** a gen_server can't be a direct `ChildSpec`
|
||||||
|
child; use the trapping wrapper pattern in `examples/graceful_shutdown.rs::
|
||||||
|
drainer_child` (starts `under(self_pid())`, forwards shutdown, waits). Doing
|
||||||
|
an inline `GenServerBuilder::run()` first may be worth a short smarm detour —
|
||||||
|
decide with Markk.
|
||||||
|
|
||||||
|
### smarm commits this session (local master, NOT pushed, NOT tagged)
|
||||||
|
`250f312` root-exit = graceful shutdown of forest roots (tests/root_exit.rs)
|
||||||
|
`6ceb138` gen_statem shutdown parity (tests/gen_statem_shutdown.rs)
|
||||||
|
`849a424` docs + examples/graceful_shutdown.rs + README "Stopping actors"
|
||||||
|
On top of `1002777` (cross-thread wake) and `9c8f59c` (graceful shutdown).
|
||||||
|
Cargo.toml still `0.6.1`. Release cut (push, tag — v0.7 is justified by the
|
||||||
|
API surface — version bump) is Markk's. Full suite, doc tests, examples,
|
||||||
|
`cargo fmt`, `cargo clippy --lib` all clean. (`clippy --tests` has pre-existing
|
||||||
|
unwrap lints in tests/fd_select.rs, untouched.)
|
||||||
|
|
||||||
|
### Decisions taken this session (Markk)
|
||||||
|
- Root exit means "program done" (Go/tokio/OTP), not "wait for pending work";
|
||||||
|
the previously agreed "sleep(50ms) must finish" test was dropped as encoding
|
||||||
|
the wrong contract (a timer-wheel gate would re-wedge periodic-timer daemons).
|
||||||
|
- No behaviour-preserving deferral, no forcing second sweep.
|
||||||
|
- Examples/docs done in the same session; urus next session.
|
||||||
|
|
||||||
|
---
|
||||||
|
# Previous handoff (still accurate where not superseded above)
|
||||||
|
|
||||||
|
|
||||||
|
## Next-session goal
|
||||||
|
Phase 1 is **done and committed**; Phase 2 is next:
|
||||||
|
1. **smarm v0.6.2** — cross-thread wake root fix. **DONE**, committed on `master`
|
||||||
|
as `1002777`. Not yet tagged, not yet version-bumped (Cargo.toml still reads
|
||||||
|
`0.6.1`), and **not yet pushed to origin** — it exists only in the delivered
|
||||||
|
snapshot zip and the local sandbox clone. Cutting the release (push + tag
|
||||||
|
`v0.6.2` + bump `0.6.1`→`0.6.2`) is Markk's step.
|
||||||
|
2. **urus v0.3** — endpoint refactor. Working against `smarm = { path = "../smarm_full" }`
|
||||||
|
with `git update-index --skip-worktree Cargo.toml` (Markk approved); release commit
|
||||||
|
swaps back to the tag once Markk cuts it. Phase 2 plan below is STALE where it says
|
||||||
|
drain-in-terminate; the endpoint is a trapping GenServer: `handle_shutdown` →
|
||||||
|
`Continue`, enter Draining, `StopHandle::stop()` when the conn set empties.
|
||||||
|
|
||||||
|
Decisions below are locked unless marked *(confirm)*.
|
||||||
|
|
||||||
|
## Reconstruction (the sandbox resets between sessions)
|
||||||
|
A fresh sandbox has an empty home and **no Rust toolchain**. To restore:
|
||||||
|
- Install rustup/cargo. smarm reformats under **rustc 1.97.1**; urus `rust-version`
|
||||||
|
is 1.95. Use 1.97.1.
|
||||||
|
- urus: `git clone https://git.kalsbeek.dev/Markk116/urus` — `origin` is registered
|
||||||
|
and public-read. master `8bdec97` = the v0.2.x line. (Zips in outputs are stale;
|
||||||
|
prefer the remote now.)
|
||||||
|
- smarm: `git clone https://git.kalsbeek.dev/Markk116/smarm`. Latest tag **v0.6.1**
|
||||||
|
(`ca1c983`). The cross-thread wake fix is committed as `1002777` on top of the
|
||||||
|
post-v0.6.1 README commit `8f2d513` (= origin/master). **It is NOT on origin
|
||||||
|
yet** — a fresh clone won't have it until Markk pushes. Restore it from the
|
||||||
|
snapshot zip if working before the push. **v0.6.2 is not yet tagged.**
|
||||||
|
- urus pins smarm by git **tag** in `Cargo.toml` (currently `v0.6.0`). A trivial
|
||||||
|
first commit bumps it to `v0.6.1` (also picks up `try_spawn` + monitor
|
||||||
|
terminal-outcome fixes).
|
||||||
|
|
||||||
|
## Why (context — the finding that drives the plan)
|
||||||
|
urus's shutdown machinery (the `AtomicBool` listener flag + the `SHUTDOWN_POLL`
|
||||||
|
loop in `serve.rs`) is scaffolding around two smarm properties. Their statuses
|
||||||
|
differ, which is the whole point:
|
||||||
|
|
||||||
|
- **Issue A — lossy stop vs a QUEUED actor: ALREADY FIXED in smarm.** Commit
|
||||||
|
`7bab4d2` added an entry-side `check_cancelled()` in `park_current`. A
|
||||||
|
`request_stop` against a listener parked in `wait_readable_timeout` now unwinds
|
||||||
|
cleanly (it parks via `try_select_timeout → park_current`). urus's flag + its
|
||||||
|
stale "smarm's lossy stop-while-QUEUED window" comment can be deleted.
|
||||||
|
- **Issue B — foreign-thread wake is a no-op: FIXED in `1002777` (was present
|
||||||
|
through v0.6.1).** The gap: `unpark`/`unpark_at`/`request_stop` all route through
|
||||||
|
`try_with_runtime`, which reads a thread-local that is `None` on any non-scheduler
|
||||||
|
thread, so no cross-thread wake worked — a signal handler / OS thread could not
|
||||||
|
wake *or* stop a parked actor, which is why `serve.rs` polls the shutdown signal
|
||||||
|
instead of parking on it. Now closed (see Phase 1 below): urus's `SHUTDOWN_POLL`
|
||||||
|
loop can be deleted and its `Handle::shutdown` can park on a handle-driven stop.
|
||||||
|
|
||||||
|
## Phase 1 — smarm cross-thread wake (root fix) — DONE (`1002777`)
|
||||||
|
Shipped as one commit generalizing RFC 018 (a producer reaches the runtime through
|
||||||
|
a `Weak` it holds) from the IO backend to channel senders and a new handle:
|
||||||
|
- **`Runtime::handle() -> RuntimeHandle`** (`Send + Sync`), holding a
|
||||||
|
`Weak<RuntimeInner>`. Grab it before `rt.run` and hand it to the signal thread.
|
||||||
|
- **`RuntimeHandle::request_stop<A>(Pid<A>)`** — upgrades the Weak and calls
|
||||||
|
`request_stop_inner` on the inner; no-op if the runtime is gone. This is the
|
||||||
|
signal-handler-drives-shutdown path; it cascades the ordered stop down the tree
|
||||||
|
exactly like an in-runtime `request_stop`.
|
||||||
|
- **Send-wake:** the receiver captures `scheduler::runtime_weak()` into its
|
||||||
|
`parked_receiver` tuple **at park time** (not at channel creation — the resolved
|
||||||
|
sub-decision; a parked receiver is a live actor so the Weak is provably upgradable,
|
||||||
|
and it scopes the capture to when a wake is possible). `send()` and last-sender
|
||||||
|
`drop` wake via `scheduler::unpark_at_via(pid, epoch, &weak)`: thread-local path
|
||||||
|
when on a scheduler thread (preempt-gated, slot-eligible), captured Weak otherwise.
|
||||||
|
In-runtime timer wakes (recv/select) were left on `scheduler::unpark_at`.
|
||||||
|
|
||||||
|
**API scope decision (signed off):** `RuntimeHandle` exposes **`request_stop` only**.
|
||||||
|
No public `unpark`/`unpark_at` on the handle — send-wake needs no user-facing handle,
|
||||||
|
and "unpark off-runtime" is covered because `request_stop` drives `unpark` on the
|
||||||
|
upgraded inner. No `is_alive()`. Both are one-line additions if a consumer appears.
|
||||||
|
|
||||||
|
**No RFC written** — pattern was already established (RFC 018), agreed not needed.
|
||||||
|
|
||||||
|
Tests: `tests/cross_thread_wake.rs` (foreign-thread send wakes a parked receiver;
|
||||||
|
foreign-thread `request_stop` wakes+stops a parked actor; a lingering handle never
|
||||||
|
blocks all-done and degrades to a no-op once the runtime drops). Full suite green;
|
||||||
|
`cargo fmt` + `cargo clippy --lib` clean.
|
||||||
|
|
||||||
|
**Remaining release step (Markk):** push `master`, tag `v0.6.2`, bump Cargo.toml
|
||||||
|
`0.6.1`→`0.6.2`. Left paired with the tag as the release cut, not done in `1002777`.
|
||||||
|
|
||||||
|
## Phase 1b — smarm graceful shutdown (OTP lift) — DONE (`9c8f59c`, on top of `1002777`)
|
||||||
|
Decided this session (Markk): B — fix at the smarm level rather than a two-stop
|
||||||
|
split in urus. No RFC (Markk: "just implement it"). Shipped, tested, committed on
|
||||||
|
the local `master`, **not pushed, not tagged**. It should ship as the same
|
||||||
|
release as 1002777 (v0.6.2, or v0.7 given the API surface — Markk's call).
|
||||||
|
- `request_shutdown(pid)` / `RuntimeHandle::request_shutdown` = `exit(Pid, shutdown)`;
|
||||||
|
`request_stop` = `exit(Pid, kill)`. Trapping target gets `ExitSignal{reason:
|
||||||
|
DownReason::Shutdown}`; non-trapping is stopped outright.
|
||||||
|
- `ChildSpec::shutdown(Shutdown::{BrutalKill, Timeout(d), Infinity})`, default 5s.
|
||||||
|
Supervisor traps exits; `request_shutdown(sup)` = ordered top-down shutdown,
|
||||||
|
returns normally. **Also fixed**: `request_stop(sup)` used to ORPHAN children
|
||||||
|
(probe-verified; the handoff's "cascade" claim was wrong) — `Live` drop guard now
|
||||||
|
hard-stops them.
|
||||||
|
- gen_server: `ctx.trap_exit()`, `handle_shutdown() -> ShutdownAction::{Exit,
|
||||||
|
Continue}`, `handle_exit(ExitSignal)`, `ctx.stop_handle().stop()` = normal
|
||||||
|
self-exit (`{stop, normal}`; previously impossible — only abnormal `Stopped`).
|
||||||
|
`GenServerRef::shutdown()` is graceful now.
|
||||||
|
- Root finding that forced this: gen_server `terminate()` runs from a Drop guard,
|
||||||
|
mid-unwind on the stop path; any park in it = double panic = abort. So
|
||||||
|
"drain-in-terminate()" (the old Phase 2 plan) was never viable.
|
||||||
|
|
||||||
|
### ~~Next-session smarm work~~ DONE this session (see TL;DR)
|
||||||
|
1. **Root-exit sweep**: make `Pop::RootDrain` also require an empty timer wheel
|
||||||
|
(and it already requires nothing runnable; io_out is only checked for AllDone —
|
||||||
|
check whether it should gate RootDrain too). TDD: an actor in `sleep(50ms)` when
|
||||||
|
the root returns must finish, not be swept. Then a `Reservoir`-style test that a
|
||||||
|
*truly* parked-forever daemon still gets swept.
|
||||||
|
2. **gen_statem parity**: `ctx.trap_exit()`, `handle_shutdown -> ShutdownAction`,
|
||||||
|
`handle_exit`, stop handle. Mirror gen_server; mechanical.
|
||||||
|
3. **Examples review**: `examples/*.rs` predate all of this. Rework where they show
|
||||||
|
shutdown/teardown to use `request_shutdown`, `Shutdown` policies, and
|
||||||
|
`StopHandle`; `named_genserver.rs` first (uses `shutdown`). Also
|
||||||
|
`docs/smarm - Deep Dive.html` says terminate() must be non-blocking — now only
|
||||||
|
true on the unwind paths; and README could use a "Stopping actors" paragraph
|
||||||
|
(request_stop = kill, request_shutdown = shutdown, Shutdown policy).
|
||||||
|
|
||||||
|
### Open smarm items found on the way (noted, not scheduled)
|
||||||
|
- (root-exit sweep and gen_statem parity moved up to the scheduled list.)
|
||||||
|
- Sweep in the supervisor `Live` drop guard is `request_stop` (kill propagates as
|
||||||
|
kill); OTP would deliver a trappable `killed`. Chosen for boundedness.
|
||||||
|
|
||||||
|
## Phase 2 — urus v0.3: endpoint refactor (after v0.6.2 is tagged)
|
||||||
|
Target = the spec's original shape (`urus-spec.md` §2.1/§6: `listener_sup` under the
|
||||||
|
**user's** root supervisor). Deviation to unwind: `serve` owning `rt.run`.
|
||||||
|
- App owns the runtime: `smarm::init(cfg).run(|| root_sup.run())`, root e.g.
|
||||||
|
`RestForOne[ app actors…, urus::endpoint(config, pipeline) ]`. This is what kills
|
||||||
|
the `Arc<OnceLock>` idiom for the right reason (app state born in-runtime as a
|
||||||
|
supervised, ordered child).
|
||||||
|
- `urus::endpoint` = one GenServer child owning the registry + an **internal**
|
||||||
|
listener sub-supervisor + drain-in-`terminate()`. Listeners stay internal, not
|
||||||
|
app-visible peers.
|
||||||
|
- Shutdown = `request_stop` the root supervisor (or via the runtime handle from a
|
||||||
|
signal thread) → cascades down → `endpoint.terminate()` runs the drain
|
||||||
|
(`drain_timeout`, force-stop sweep).
|
||||||
|
- **DELETE:** the `AtomicBool` listener flag (A fixed) and the `SHUTDOWN_POLL` loop
|
||||||
|
+ its apologetic comment (B fixed → park, don't poll).
|
||||||
|
- Nuance: `request_stop` → `Signal::Stopped` is *abnormal* → `Transient` restarts.
|
||||||
|
Stop-without-restart = stop the **supervisor**, not the children.
|
||||||
|
- *(confirm)* Keep `serve`/`serve_with`/`serve_with_shutdown` as thin wrappers that
|
||||||
|
build the one-child tree internally, so the simple case stays one line.
|
||||||
|
- *(confirm)* Keep `Handle`/`ShutdownSignal`? Now that cross-thread wake works,
|
||||||
|
`Handle::shutdown` can map to handle-driven `request_stop` on the endpoint.
|
||||||
|
- Breaking → cut **urus v0.3**; bump the smarm pin to `v0.6.2` here.
|
||||||
|
|
||||||
|
## Working norms
|
||||||
|
- Every bash call: `export PATH=$HOME/.cargo/bin:$PATH` (once the toolchain's in).
|
||||||
|
- **TDD**: failing test first, then implement; keep suites green.
|
||||||
|
- **Hammer ritual** for ANY connection-lifecycle change (the urus #2 shutdown work
|
||||||
|
qualifies): 35× subset (`shutdown timeout reaped slowloris streaming chunked sse
|
||||||
|
stalled ws_ channels session`) + 3× full + 1× trace. `scripts/hammer.sh` does NOT
|
||||||
|
pass feature flags — loop manually with `--features phoenix`. Subset filter must
|
||||||
|
NOT use `--test integration` (session tests live in lib).
|
||||||
|
- Example smoke tests: hold the server's stdin open (`mkfifo` + `sleep > fifo`) or
|
||||||
|
the Enter-to-shutdown thread fires on EOF instantly.
|
||||||
|
- Background procs are reaped BETWEEN bash calls; `pkill -f` matches your own shell.
|
||||||
|
- Artefact store (specs): `curl -H "Authorization: Bearer sk-llmingest-2e45d80c63db24c6781e761eb2a9a58e83d9f48ef77a42185bad311d07c80e68" https://artefacts.kalsbeek.dev/artifacts/<name>`
|
||||||
|
— `urus-spec.md`, `urus-bench-spec.md`, `rfc_008-implementation-notes.md`, …
|
||||||
|
- smarm feature flags: `smarm-trace`, `smarm-causal` (urus re-exports both).
|
||||||
|
|
||||||
|
## Local cross-repo testing — KEEP OUT OF COMMITS
|
||||||
|
To test urus #2 against un-tagged smarm 0.6.2, point urus's `Cargo.toml` smarm dep
|
||||||
|
at a local path (`smarm = { path = "../smarm" }`) instead of the git tag.
|
||||||
|
- Must NOT land in commits. Guard: `git update-index --skip-worktree Cargo.toml`
|
||||||
|
after editing (undo with `--no-skip-worktree`), or stash before committing.
|
||||||
|
- The committed `Cargo.toml` stays pinned to the git tag; restore the tag (bumped to
|
||||||
|
`v0.6.2`) for the release commit.
|
||||||
+67
-26
@@ -26,7 +26,9 @@ use std::time::Instant;
|
|||||||
const ITERS: u32 = 15;
|
const ITERS: u32 = 15;
|
||||||
|
|
||||||
fn available_threads() -> usize {
|
fn available_threads() -> usize {
|
||||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_sets() -> u32 {
|
fn env_sets() -> u32 {
|
||||||
@@ -108,17 +110,15 @@ fn bench_chained_smarm(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_chained_tokio_current() -> (u64, u128) {
|
fn bench_chained_tokio_current() -> (u64, u128) {
|
||||||
let counter = Arc::new(AtomicU64::new(0));
|
let counter = Arc::new(AtomicU64::new(0));
|
||||||
let c2 = counter.clone();
|
let c2 = counter.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
// Use a oneshot done channel like tokio's own chained_spawn bench.
|
// Use a oneshot done channel like tokio's own chained_spawn bench.
|
||||||
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
|
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
|
||||||
fn iter(
|
fn iter(c: Arc<AtomicU64>, done: tokio::sync::oneshot::Sender<()>, n: u64) {
|
||||||
c: Arc<AtomicU64>,
|
|
||||||
done: tokio::sync::oneshot::Sender<()>,
|
|
||||||
n: u64,
|
|
||||||
) {
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
let _ = done.send(());
|
let _ = done.send(());
|
||||||
} else {
|
} else {
|
||||||
@@ -186,7 +186,9 @@ fn bench_yield_smarm(threads: usize) -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_yield_tokio_current() -> (u64, u128) {
|
fn bench_yield_tokio_current() -> (u64, u128) {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -235,11 +237,22 @@ const PRIME_N: u64 = 400_000;
|
|||||||
const PRIME_WORKERS: u64 = 64;
|
const PRIME_WORKERS: u64 = 64;
|
||||||
|
|
||||||
fn is_prime(n: u64) -> bool {
|
fn is_prime(n: u64) -> bool {
|
||||||
if n < 2 { return false; }
|
if n < 2 {
|
||||||
if n < 4 { return true; }
|
return false;
|
||||||
if n % 2 == 0 { return false; }
|
}
|
||||||
|
if n < 4 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if n % 2 == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let mut i = 3u64;
|
let mut i = 3u64;
|
||||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
while i * i <= n {
|
||||||
|
if n % i == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +263,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
|
|||||||
fn primes_slice(w: u64) -> (u64, u64) {
|
fn primes_slice(w: u64) -> (u64, u64) {
|
||||||
let per = PRIME_N / PRIME_WORKERS;
|
let per = PRIME_N / PRIME_WORKERS;
|
||||||
let lo = w * per;
|
let lo = w * per;
|
||||||
let hi = if w + 1 == PRIME_WORKERS { PRIME_N } else { lo + per };
|
let hi = if w + 1 == PRIME_WORKERS {
|
||||||
|
PRIME_N
|
||||||
|
} else {
|
||||||
|
lo + per
|
||||||
|
};
|
||||||
(lo, hi)
|
(lo, hi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +284,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -275,7 +294,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_primes_tokio_current() -> (u64, u128) {
|
fn bench_primes_tokio_current() -> (u64, u128) {
|
||||||
let total = Arc::new(AtomicU64::new(0));
|
let total = Arc::new(AtomicU64::new(0));
|
||||||
let t2 = total.clone();
|
let t2 = total.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -287,7 +308,9 @@ fn bench_primes_tokio_current() -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -309,7 +332,9 @@ fn bench_primes_tokio_multi() -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -344,7 +369,9 @@ fn bench_pp_smarm(threads: usize) -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_pp_tokio_current() -> (u64, u128) {
|
fn bench_pp_tokio_current() -> (u64, u128) {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -395,7 +422,6 @@ fn bench_pp_tokio_multi() -> (u64, u128) {
|
|||||||
// main
|
// main
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||||
// so the sweep script can override the preemption knobs without recompiling.
|
// so the sweep script can override the preemption knobs without recompiling.
|
||||||
@@ -404,10 +430,14 @@ fn bench_pp_tokio_multi() -> (u64, u128) {
|
|||||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
if let Ok(n) = v.parse::<u32>() {
|
||||||
|
cfg = cfg.alloc_interval(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
if let Ok(n) = v.parse::<u64>() {
|
||||||
|
cfg = cfg.timeslice_cycles(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
@@ -417,7 +447,10 @@ fn main() {
|
|||||||
println!("smarm general benchmarks");
|
println!("smarm general benchmarks");
|
||||||
println!("available parallelism: {n} threads");
|
println!("available parallelism: {n} threads");
|
||||||
let sets = env_sets();
|
let sets = env_sets();
|
||||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
println!(
|
||||||
|
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||||
|
ITERS * sets
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"CHAIN_DEPTH={CHAIN_DEPTH}, YIELD_TASKS={YIELD_TASKS}×{YIELD_ROUNDS}, \
|
"CHAIN_DEPTH={CHAIN_DEPTH}, YIELD_TASKS={YIELD_TASKS}×{YIELD_ROUNDS}, \
|
||||||
PRIME_N={PRIME_N}/{PRIME_WORKERS} workers, PP_ROUNDS={PP_ROUNDS}"
|
PRIME_N={PRIME_N}/{PRIME_WORKERS} workers, PP_ROUNDS={PP_ROUNDS}"
|
||||||
@@ -426,21 +459,29 @@ fn main() {
|
|||||||
// ---- 1. chained_spawn ----
|
// ---- 1. chained_spawn ----
|
||||||
print_header(&format!("chained_spawn: depth {CHAIN_DEPTH}"));
|
print_header(&format!("chained_spawn: depth {CHAIN_DEPTH}"));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_chained_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_chained_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_chained_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_chained_smarm(n)
|
||||||
|
});
|
||||||
run_n("tokio current_thread", ITERS, bench_chained_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_chained_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_chained_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_chained_tokio_multi);
|
||||||
|
|
||||||
// ---- 2. yield_many ----
|
// ---- 2. yield_many ----
|
||||||
print_header(&format!("yield_many: {YIELD_TASKS} tasks × {YIELD_ROUNDS} yields"));
|
print_header(&format!(
|
||||||
|
"yield_many: {YIELD_TASKS} tasks × {YIELD_ROUNDS} yields"
|
||||||
|
));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_yield_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_yield_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_yield_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || bench_yield_smarm(n));
|
||||||
run_n("tokio current_thread", ITERS, bench_yield_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_yield_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_yield_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_yield_tokio_multi);
|
||||||
|
|
||||||
// ---- 3. fan_out_compute ----
|
// ---- 3. fan_out_compute ----
|
||||||
print_header(&format!("fan_out_compute: primes in [2, {PRIME_N}) across {PRIME_WORKERS}"));
|
print_header(&format!(
|
||||||
|
"fan_out_compute: primes in [2, {PRIME_N}) across {PRIME_WORKERS}"
|
||||||
|
));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_primes_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_primes_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_primes_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_primes_smarm(n)
|
||||||
|
});
|
||||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||||
|
|
||||||
|
|||||||
+90
-41
@@ -64,11 +64,22 @@ const PRIME_N: u64 = 400_000;
|
|||||||
const WORKERS: u64 = 64;
|
const WORKERS: u64 = 64;
|
||||||
|
|
||||||
fn is_prime(n: u64) -> bool {
|
fn is_prime(n: u64) -> bool {
|
||||||
if n < 2 { return false; }
|
if n < 2 {
|
||||||
if n < 4 { return true; }
|
return false;
|
||||||
if n % 2 == 0 { return false; }
|
}
|
||||||
|
if n < 4 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if n % 2 == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let mut i = 3u64;
|
let mut i = 3u64;
|
||||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
while i * i <= n {
|
||||||
|
if n % i == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +107,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -104,7 +117,9 @@ fn bench_primes_smarm(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_primes_tokio_current() -> (u64, u128) {
|
fn bench_primes_tokio_current() -> (u64, u128) {
|
||||||
let total = Arc::new(AtomicU64::new(0));
|
let total = Arc::new(AtomicU64::new(0));
|
||||||
let t2 = total.clone();
|
let t2 = total.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -116,7 +131,9 @@ fn bench_primes_tokio_current() -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -138,17 +155,21 @@ fn bench_primes_tokio_multi() -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_primes_baseline() -> (u64, u128) {
|
fn bench_primes_baseline() -> (u64, u128) {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let total: u64 = (0..WORKERS).map(|w| {
|
let total: u64 = (0..WORKERS)
|
||||||
let (lo, hi) = primes_slice(w);
|
.map(|w| {
|
||||||
count_primes(lo, hi)
|
let (lo, hi) = primes_slice(w);
|
||||||
}).sum();
|
count_primes(lo, hi)
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
(total, start.elapsed().as_micros())
|
(total, start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,15 +188,17 @@ fn bench_pingpong_smarm(threads: usize) -> (u64, u128) {
|
|||||||
tx_a.send(0).unwrap();
|
tx_a.send(0).unwrap();
|
||||||
loop {
|
loop {
|
||||||
let v = rx_b.recv().unwrap();
|
let v = rx_b.recv().unwrap();
|
||||||
if v >= PING_ROUNDS { break; }
|
if v >= PING_ROUNDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
tx_a.send(v + 1).unwrap();
|
tx_a.send(v + 1).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let hb = smarm::spawn(move || {
|
let hb = smarm::spawn(move || loop {
|
||||||
loop {
|
let v = rx_a.recv().unwrap();
|
||||||
let v = rx_a.recv().unwrap();
|
tx_b.send(v + 1).unwrap();
|
||||||
tx_b.send(v + 1).unwrap();
|
if v + 1 >= PING_ROUNDS {
|
||||||
if v + 1 >= PING_ROUNDS { break; }
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ha.join().unwrap();
|
ha.join().unwrap();
|
||||||
@@ -198,7 +221,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
|
|||||||
tx_a.send(0).unwrap();
|
tx_a.send(0).unwrap();
|
||||||
loop {
|
loop {
|
||||||
let v = rx_b.recv().await.unwrap();
|
let v = rx_b.recv().await.unwrap();
|
||||||
if v >= PING_ROUNDS { break; }
|
if v >= PING_ROUNDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
tx_a.send(v + 1).unwrap();
|
tx_a.send(v + 1).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,7 +231,9 @@ fn bench_pingpong_tokio_current() -> (u64, u128) {
|
|||||||
loop {
|
loop {
|
||||||
let v = rx_a.recv().await.unwrap();
|
let v = rx_a.recv().await.unwrap();
|
||||||
tx_b.send(v + 1).unwrap();
|
tx_b.send(v + 1).unwrap();
|
||||||
if v + 1 >= PING_ROUNDS { break; }
|
if v + 1 >= PING_ROUNDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let _ = ha.await;
|
let _ = ha.await;
|
||||||
@@ -229,7 +256,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
|
|||||||
tx_a.send(0).unwrap();
|
tx_a.send(0).unwrap();
|
||||||
loop {
|
loop {
|
||||||
let v = rx_b.recv().await.unwrap();
|
let v = rx_b.recv().await.unwrap();
|
||||||
if v >= PING_ROUNDS { break; }
|
if v >= PING_ROUNDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
tx_a.send(v + 1).unwrap();
|
tx_a.send(v + 1).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -237,7 +266,9 @@ fn bench_pingpong_tokio_multi() -> (u64, u128) {
|
|||||||
loop {
|
loop {
|
||||||
let v = rx_a.recv().await.unwrap();
|
let v = rx_a.recv().await.unwrap();
|
||||||
tx_b.send(v + 1).unwrap();
|
tx_b.send(v + 1).unwrap();
|
||||||
if v + 1 >= PING_ROUNDS { break; }
|
if v + 1 >= PING_ROUNDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let _ = ha.await;
|
let _ = ha.await;
|
||||||
@@ -264,7 +295,9 @@ fn bench_spawn_smarm(threads: usize) -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -272,7 +305,9 @@ fn bench_spawn_smarm(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_spawn_tokio_current() -> (u64, u128) {
|
fn bench_spawn_tokio_current() -> (u64, u128) {
|
||||||
let counter = Arc::new(AtomicU64::new(0));
|
let counter = Arc::new(AtomicU64::new(0));
|
||||||
let c = counter.clone();
|
let c = counter.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -283,7 +318,9 @@ fn bench_spawn_tokio_current() -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -304,7 +341,9 @@ fn bench_spawn_tokio_multi() -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -320,24 +359,34 @@ fn main() {
|
|||||||
println!("PRIME_N={PRIME_N}, WORKERS={WORKERS}, PING_ROUNDS={PING_ROUNDS}, SPAWN_COUNT={SPAWN_COUNT}");
|
println!("PRIME_N={PRIME_N}, WORKERS={WORKERS}, PING_ROUNDS={PING_ROUNDS}, SPAWN_COUNT={SPAWN_COUNT}");
|
||||||
|
|
||||||
// ---- Primes ----
|
// ---- Primes ----
|
||||||
print_header(&format!("Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers"));
|
print_header(&format!(
|
||||||
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
"Fan-out/fan-in: count primes in [2, {PRIME_N}) across {WORKERS} workers"
|
||||||
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_primes_smarm(n));
|
run_n("baseline (serial)", ITERS, bench_primes_baseline);
|
||||||
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
run_n("smarm single-thread", ITERS, || bench_primes_smarm(1));
|
||||||
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_primes_smarm(n)
|
||||||
|
});
|
||||||
|
run_n("tokio current_thread", ITERS, bench_primes_tokio_current);
|
||||||
|
run_n("tokio multi-thread", ITERS, bench_primes_tokio_multi);
|
||||||
|
|
||||||
// ---- Ping-pong ----
|
// ---- Ping-pong ----
|
||||||
print_header(&format!("Ping-pong: {PING_ROUNDS} round-trips between two actors"));
|
print_header(&format!(
|
||||||
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
"Ping-pong: {PING_ROUNDS} round-trips between two actors"
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_pingpong_smarm(n));
|
));
|
||||||
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
run_n("smarm single-thread", ITERS, || bench_pingpong_smarm(1));
|
||||||
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_pingpong_smarm(n)
|
||||||
|
});
|
||||||
|
run_n("tokio current_thread", ITERS, bench_pingpong_tokio_current);
|
||||||
|
run_n("tokio multi-thread", ITERS, bench_pingpong_tokio_multi);
|
||||||
|
|
||||||
// ---- Spawn throughput ----
|
// ---- Spawn throughput ----
|
||||||
print_header(&format!("Spawn throughput: {SPAWN_COUNT} actors spawned and joined"));
|
print_header(&format!(
|
||||||
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
"Spawn throughput: {SPAWN_COUNT} actors spawned and joined"
|
||||||
|
));
|
||||||
|
run_n("smarm single-thread", ITERS, || bench_spawn_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || bench_spawn_smarm(n));
|
||||||
run_n("tokio current_thread", ITERS, bench_spawn_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_spawn_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_spawn_tokio_multi);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-7
@@ -16,12 +16,20 @@ const WORKERS: u64 = 16;
|
|||||||
const ITERATIONS: u32 = 5;
|
const ITERATIONS: u32 = 5;
|
||||||
|
|
||||||
fn is_prime(n: u64) -> bool {
|
fn is_prime(n: u64) -> bool {
|
||||||
if n < 2 { return false; }
|
if n < 2 {
|
||||||
if n < 4 { return true; }
|
return false;
|
||||||
if n % 2 == 0 { return false; }
|
}
|
||||||
|
if n < 4 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if n % 2 == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let mut i = 3u64;
|
let mut i = 3u64;
|
||||||
while i * i <= n {
|
while i * i <= n {
|
||||||
if n % i == 0 { return false; }
|
if n % i == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -30,7 +38,9 @@ fn is_prime(n: u64) -> bool {
|
|||||||
fn count_primes_in(lo: u64, hi: u64) -> u64 {
|
fn count_primes_in(lo: u64, hi: u64) -> u64 {
|
||||||
let mut count = 0u64;
|
let mut count = 0u64;
|
||||||
for n in lo..hi {
|
for n in lo..hi {
|
||||||
if is_prime(n) { count += 1; }
|
if is_prime(n) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
@@ -38,7 +48,11 @@ fn count_primes_in(lo: u64, hi: u64) -> u64 {
|
|||||||
fn slice(worker: u64) -> (u64, u64) {
|
fn slice(worker: u64) -> (u64, u64) {
|
||||||
let per = N / WORKERS;
|
let per = N / WORKERS;
|
||||||
let lo = worker * per;
|
let lo = worker * per;
|
||||||
let hi = if worker + 1 == WORKERS { N } else { (worker + 1) * per };
|
let hi = if worker + 1 == WORKERS {
|
||||||
|
N
|
||||||
|
} else {
|
||||||
|
(worker + 1) * per
|
||||||
|
};
|
||||||
(lo, hi)
|
(lo, hi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +139,10 @@ fn main() {
|
|||||||
"Counting primes in [2, {}) across {} workers, {} iterations each\n",
|
"Counting primes in [2, {}) across {} workers, {} iterations each\n",
|
||||||
N, WORKERS, ITERATIONS
|
N, WORKERS, ITERATIONS
|
||||||
);
|
);
|
||||||
println!("{:>12} | {:>15} | {:>16} | {:>15} | {:>15}", "runtime", "primes found", "median", "min", "max");
|
println!(
|
||||||
|
"{:>12} | {:>15} | {:>16} | {:>15} | {:>15}",
|
||||||
|
"runtime", "primes found", "median", "min", "max"
|
||||||
|
);
|
||||||
println!("{}", "-".repeat(80));
|
println!("{}", "-".repeat(80));
|
||||||
|
|
||||||
run_n("baseline", ITERATIONS, bench_baseline);
|
run_n("baseline", ITERATIONS, bench_baseline);
|
||||||
|
|||||||
+44
-7
@@ -27,12 +27,19 @@ use std::sync::Arc;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
fn env_usize(key: &str, default: usize) -> usize {
|
fn env_usize(key: &str, default: usize) -> usize {
|
||||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
std::env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_threads() -> Vec<usize> {
|
fn env_threads() -> Vec<usize> {
|
||||||
std::env::var("SMARM_BENCH_THREADS")
|
std::env::var("SMARM_BENCH_THREADS")
|
||||||
.map(|v| v.split_whitespace().filter_map(|t| t.parse().ok()).collect())
|
.map(|v| {
|
||||||
|
v.split_whitespace()
|
||||||
|
.filter_map(|t| t.parse().ok())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.unwrap_or_else(|_| vec![1, 2, 4])
|
.unwrap_or_else(|_| vec![1, 2, 4])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +60,11 @@ fn drive<Q: Send + Sync + 'static>(
|
|||||||
for p in 0..producers {
|
for p in 0..producers {
|
||||||
let q = q.clone();
|
let q = q.clone();
|
||||||
// Give the last producer the remainder.
|
// Give the last producer the remainder.
|
||||||
let n = if p == producers - 1 { items - per * (producers - 1) } else { per };
|
let n = if p == producers - 1 {
|
||||||
|
items - per * (producers - 1)
|
||||||
|
} else {
|
||||||
|
per
|
||||||
|
};
|
||||||
hs.push(std::thread::spawn(move || {
|
hs.push(std::thread::spawn(move || {
|
||||||
let pid = Pid::new(p as u32, 0);
|
let pid = Pid::new(p as u32, 0);
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
@@ -132,7 +143,12 @@ fn main() {
|
|||||||
for &t in &threads_sweep {
|
for &t in &threads_sweep {
|
||||||
for (p, c) in ratios_for(t) {
|
for (p, c) in ratios_for(t) {
|
||||||
for s in ["mutex", "mpmc", "striped"] {
|
for s in ["mutex", "mpmc", "striped"] {
|
||||||
cases.push(Case { structure: s, threads: t, producers: p, consumers: c });
|
cases.push(Case {
|
||||||
|
structure: s,
|
||||||
|
threads: t,
|
||||||
|
producers: p,
|
||||||
|
consumers: c,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +163,14 @@ fn main() {
|
|||||||
if case.threads < 2 {
|
if case.threads < 2 {
|
||||||
drive_single(&*q, MutexQueue::push, MutexQueue::pop, items)
|
drive_single(&*q, MutexQueue::push, MutexQueue::pop, items)
|
||||||
} else {
|
} else {
|
||||||
drive(q, MutexQueue::push, MutexQueue::pop, case.producers, case.consumers, items)
|
drive(
|
||||||
|
q,
|
||||||
|
MutexQueue::push,
|
||||||
|
MutexQueue::pop,
|
||||||
|
case.producers,
|
||||||
|
case.consumers,
|
||||||
|
items,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"mpmc" => {
|
"mpmc" => {
|
||||||
@@ -155,7 +178,14 @@ fn main() {
|
|||||||
if case.threads < 2 {
|
if case.threads < 2 {
|
||||||
drive_single(&*q, MpmcRing::push, MpmcRing::pop, items)
|
drive_single(&*q, MpmcRing::push, MpmcRing::pop, items)
|
||||||
} else {
|
} else {
|
||||||
drive(q, MpmcRing::push, MpmcRing::pop, case.producers, case.consumers, items)
|
drive(
|
||||||
|
q,
|
||||||
|
MpmcRing::push,
|
||||||
|
MpmcRing::pop,
|
||||||
|
case.producers,
|
||||||
|
case.consumers,
|
||||||
|
items,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"striped" => {
|
"striped" => {
|
||||||
@@ -163,7 +193,14 @@ fn main() {
|
|||||||
if case.threads < 2 {
|
if case.threads < 2 {
|
||||||
drive_single(&*q, StripedRing::push, StripedRing::pop, items)
|
drive_single(&*q, StripedRing::push, StripedRing::pop, items)
|
||||||
} else {
|
} else {
|
||||||
drive(q, StripedRing::push, StripedRing::pop, case.producers, case.consumers, items)
|
drive(
|
||||||
|
q,
|
||||||
|
StripedRing::push,
|
||||||
|
StripedRing::pop,
|
||||||
|
case.producers,
|
||||||
|
case.consumers,
|
||||||
|
items,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
|||||||
+21
-4
@@ -54,12 +54,19 @@ fn variant() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn env_usize(key: &str, default: usize) -> usize {
|
fn env_usize(key: &str, default: usize) -> usize {
|
||||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
std::env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_threads() -> Vec<usize> {
|
fn env_threads() -> Vec<usize> {
|
||||||
std::env::var("SMARM_BENCH_THREADS")
|
std::env::var("SMARM_BENCH_THREADS")
|
||||||
.map(|v| v.split_whitespace().filter_map(|t| t.parse().ok()).collect())
|
.map(|v| {
|
||||||
|
v.split_whitespace()
|
||||||
|
.filter_map(|t| t.parse().ok())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.unwrap_or_else(|_| vec![1, 2, 4])
|
.unwrap_or_else(|_| vec![1, 2, 4])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +245,22 @@ fn main() {
|
|||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"RQCSV,runtime,{},{},{},{},{},{},{}",
|
"RQCSV,runtime,{},{},{},{},{},{},{}",
|
||||||
variant(), slot_str, name, t, work, mid.us, per_s
|
variant(),
|
||||||
|
slot_str,
|
||||||
|
name,
|
||||||
|
t,
|
||||||
|
work,
|
||||||
|
mid.us,
|
||||||
|
per_s
|
||||||
);
|
);
|
||||||
if slot {
|
if slot {
|
||||||
println!(
|
println!(
|
||||||
"RQSLOT,{},{},{},{},{}",
|
"RQSLOT,{},{},{},{},{}",
|
||||||
variant(), name, t, mid.hits, mid.displacements
|
variant(),
|
||||||
|
name,
|
||||||
|
t,
|
||||||
|
mid.hits,
|
||||||
|
mid.displacements
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-19
@@ -37,7 +37,9 @@ use std::time::Instant;
|
|||||||
const ITERS: u32 = 15;
|
const ITERS: u32 = 15;
|
||||||
|
|
||||||
fn available_threads() -> usize {
|
fn available_threads() -> usize {
|
||||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_sets() -> u32 {
|
fn env_sets() -> u32 {
|
||||||
@@ -116,7 +118,9 @@ fn bench_recurse_smarm(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_recurse_tokio_current() -> (u64, u128) {
|
fn bench_recurse_tokio_current() -> (u64, u128) {
|
||||||
let counter = Arc::new(AtomicU64::new(0));
|
let counter = Arc::new(AtomicU64::new(0));
|
||||||
let c2 = counter.clone();
|
let c2 = counter.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -199,7 +203,9 @@ fn bench_hot_smarm() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_hot_tokio_current() -> (u64, u128) {
|
fn bench_hot_tokio_current() -> (u64, u128) {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -249,7 +255,9 @@ fn bench_unc_smarm() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_unc_tokio_current() -> (u64, u128) {
|
fn bench_unc_tokio_current() -> (u64, u128) {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -297,8 +305,12 @@ fn bench_panic_smarm(threads: usize) -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
for h in handles {
|
for h in handles {
|
||||||
match h.join() {
|
match h.join() {
|
||||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
Ok(()) => {
|
||||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
ok2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
err2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -312,7 +324,9 @@ fn bench_panic_tokio_current() -> (u64, u128) {
|
|||||||
let err = Arc::new(AtomicU64::new(0));
|
let err = Arc::new(AtomicU64::new(0));
|
||||||
let ok2 = ok.clone();
|
let ok2 = ok.clone();
|
||||||
let err2 = err.clone();
|
let err2 = err.clone();
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let prev_hook = std::panic::take_hook();
|
let prev_hook = std::panic::take_hook();
|
||||||
std::panic::set_hook(Box::new(|_| {}));
|
std::panic::set_hook(Box::new(|_| {}));
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -328,8 +342,12 @@ fn bench_panic_tokio_current() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
for h in handles {
|
for h in handles {
|
||||||
match h.await {
|
match h.await {
|
||||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
Ok(()) => {
|
||||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
ok2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
err2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -361,8 +379,12 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
for h in handles {
|
for h in handles {
|
||||||
match h.await {
|
match h.await {
|
||||||
Ok(()) => { ok2.fetch_add(1, Ordering::Relaxed); }
|
Ok(()) => {
|
||||||
Err(_) => { err2.fetch_add(1, Ordering::Relaxed); }
|
ok2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
err2.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -375,7 +397,6 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
|||||||
// main
|
// main
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||||
// so the sweep script can override the preemption knobs without recompiling.
|
// so the sweep script can override the preemption knobs without recompiling.
|
||||||
@@ -384,10 +405,14 @@ fn bench_panic_tokio_multi() -> (u64, u128) {
|
|||||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
if let Ok(n) = v.parse::<u32>() {
|
||||||
|
cfg = cfg.alloc_interval(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
if let Ok(n) = v.parse::<u64>() {
|
||||||
|
cfg = cfg.timeslice_cycles(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
@@ -397,7 +422,10 @@ fn main() {
|
|||||||
println!("smarm smarm-favored benchmarks");
|
println!("smarm smarm-favored benchmarks");
|
||||||
println!("available parallelism: {n} threads");
|
println!("available parallelism: {n} threads");
|
||||||
let sets = env_sets();
|
let sets = env_sets();
|
||||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
println!(
|
||||||
|
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||||
|
ITERS * sets
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"RECURSE_DEPTH={RECURSE_DEPTH}, HOT_YIELDS={HOT_YIELDS}×2, \
|
"RECURSE_DEPTH={RECURSE_DEPTH}, HOT_YIELDS={HOT_YIELDS}×2, \
|
||||||
UNCONT_MSGS={UNCONT_MSGS}, PANIC_TASKS={PANIC_TASKS}"
|
UNCONT_MSGS={UNCONT_MSGS}, PANIC_TASKS={PANIC_TASKS}"
|
||||||
@@ -406,22 +434,30 @@ fn main() {
|
|||||||
// ---- 9. deep_recursion ----
|
// ---- 9. deep_recursion ----
|
||||||
print_header(&format!("deep_recursion: depth {RECURSE_DEPTH}"));
|
print_header(&format!("deep_recursion: depth {RECURSE_DEPTH}"));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_recurse_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_recurse_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_recurse_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_recurse_smarm(n)
|
||||||
|
});
|
||||||
run_n("tokio current_thread", ITERS, bench_recurse_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_recurse_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_recurse_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_recurse_tokio_multi);
|
||||||
|
|
||||||
// ---- 10. yield_in_hot_loop ----
|
// ---- 10. yield_in_hot_loop ----
|
||||||
print_header(&format!("yield_in_hot_loop: 2 actors × {HOT_YIELDS} yields (single thread)"));
|
print_header(&format!(
|
||||||
|
"yield_in_hot_loop: 2 actors × {HOT_YIELDS} yields (single thread)"
|
||||||
|
));
|
||||||
run_n("smarm 1-thread", ITERS, bench_hot_smarm);
|
run_n("smarm 1-thread", ITERS, bench_hot_smarm);
|
||||||
run_n("tokio current_thread", ITERS, bench_hot_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_hot_tokio_current);
|
||||||
|
|
||||||
// ---- 11. uncontended_channel ----
|
// ---- 11. uncontended_channel ----
|
||||||
print_header(&format!("uncontended_channel: 1→1, {UNCONT_MSGS} msgs (single thread)"));
|
print_header(&format!(
|
||||||
|
"uncontended_channel: 1→1, {UNCONT_MSGS} msgs (single thread)"
|
||||||
|
));
|
||||||
run_n("smarm 1-thread", ITERS, bench_unc_smarm);
|
run_n("smarm 1-thread", ITERS, bench_unc_smarm);
|
||||||
run_n("tokio current_thread", ITERS, bench_unc_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_unc_tokio_current);
|
||||||
|
|
||||||
// ---- 12. catch_unwind_panics ----
|
// ---- 12. catch_unwind_panics ----
|
||||||
print_header(&format!("catch_unwind_panics: {PANIC_TASKS} tasks, 50% panic"));
|
print_header(&format!(
|
||||||
|
"catch_unwind_panics: {PANIC_TASKS} tasks, 50% panic"
|
||||||
|
));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_panic_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_panic_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_panic_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || bench_panic_smarm(n));
|
||||||
run_n("tokio current_thread", ITERS, bench_panic_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_panic_tokio_current);
|
||||||
|
|||||||
+30
-5
@@ -73,7 +73,10 @@ fn variant() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn env_usize(key: &str, default: usize) -> usize {
|
fn env_usize(key: &str, default: usize) -> usize {
|
||||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
std::env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -226,7 +229,11 @@ fn main() {
|
|||||||
let mean_cyc = pooled_cyc.iter().map(|&v| v as f64).sum::<f64>() / n.max(1) as f64;
|
let mean_cyc = pooled_cyc.iter().map(|&v| v as f64).sum::<f64>() / n.max(1) as f64;
|
||||||
// Derived effective frequency: cycles per ns = GHz. Cross-checks the two
|
// Derived effective frequency: cycles per ns = GHz. Cross-checks the two
|
||||||
// lenses against the box's known base clock.
|
// lenses against the box's known base clock.
|
||||||
let derived_ghz = if mean_ns > 0.0 { mean_cyc / mean_ns } else { 0.0 };
|
let derived_ghz = if mean_ns > 0.0 {
|
||||||
|
mean_cyc / mean_ns
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
let p50 = pct(&pooled_ns, 50.0);
|
let p50 = pct(&pooled_ns, 50.0);
|
||||||
let p90 = pct(&pooled_ns, 90.0);
|
let p90 = pct(&pooled_ns, 90.0);
|
||||||
@@ -241,8 +248,14 @@ fn main() {
|
|||||||
" rounds={} warmup={} runs={} (instrumentation floor: {} ns / {} cyc, subtracted)",
|
" rounds={} warmup={} runs={} (instrumentation floor: {} ns / {} cyc, subtracted)",
|
||||||
rounds, warmup, runs, floor_ns, floor_cyc
|
rounds, warmup, runs, floor_ns, floor_cyc
|
||||||
);
|
);
|
||||||
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", "p50 ns", "p90 ns", "p99 ns", "min ns", "max ns");
|
println!(
|
||||||
println!(" {:<10} {:<10} {:<10} {:<10} {:<10}", p50, p90, p99, lo, hi);
|
" {:<10} {:<10} {:<10} {:<10} {:<10}",
|
||||||
|
"p50 ns", "p90 ns", "p99 ns", "min ns", "max ns"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {:<10} {:<10} {:<10} {:<10} {:<10}",
|
||||||
|
p50, p90, p99, lo, hi
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
" mean {:.1} ns | mean {:.0} cyc | derived {:.3} GHz",
|
" mean {:.1} ns | mean {:.0} cyc | derived {:.3} GHz",
|
||||||
mean_ns, mean_cyc, derived_ghz
|
mean_ns, mean_cyc, derived_ghz
|
||||||
@@ -251,6 +264,18 @@ fn main() {
|
|||||||
// Greppable line — same spirit as SPINCSV.
|
// Greppable line — same spirit as SPINCSV.
|
||||||
println!(
|
println!(
|
||||||
"SWITCHCSV,{},{},{},{},{},{},{},{},{},{},{:.1},{:.0},{:.3}",
|
"SWITCHCSV,{},{},{},{},{},{},{},{},{},{},{:.1},{:.0},{:.3}",
|
||||||
variant(), mode, rounds, runs, n, p50, p90, p99, lo, hi, mean_ns, mean_cyc, derived_ghz
|
variant(),
|
||||||
|
mode,
|
||||||
|
rounds,
|
||||||
|
runs,
|
||||||
|
n,
|
||||||
|
p50,
|
||||||
|
p90,
|
||||||
|
p99,
|
||||||
|
lo,
|
||||||
|
hi,
|
||||||
|
mean_ns,
|
||||||
|
mean_cyc,
|
||||||
|
derived_ghz
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-35
@@ -36,7 +36,9 @@ use std::time::{Duration, Instant};
|
|||||||
const ITERS: u32 = 15;
|
const ITERS: u32 = 15;
|
||||||
|
|
||||||
fn available_threads() -> usize {
|
fn available_threads() -> usize {
|
||||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
|
std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_sets() -> u32 {
|
fn env_sets() -> u32 {
|
||||||
@@ -84,8 +86,8 @@ fn run_n<F: FnMut() -> (u64, u128)>(name: &str, n: u32, mut f: F) {
|
|||||||
// 5. spawn_storm_busy — workers loaded, then storm of zero-work spawns
|
// 5. spawn_storm_busy — workers loaded, then storm of zero-work spawns
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
const STORM_BACKGROUND: u64 = 8; // number of background "busy" actors
|
||||||
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
const STORM_SPAWN: u64 = 10_000; // zero-work spawns to time
|
||||||
|
|
||||||
fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
||||||
let counter = Arc::new(AtomicU64::new(0));
|
let counter = Arc::new(AtomicU64::new(0));
|
||||||
@@ -114,11 +116,15 @@ fn bench_storm_smarm(threads: usize) -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
// Tear down background.
|
// Tear down background.
|
||||||
s2.store(true, Ordering::Relaxed);
|
s2.store(true, Ordering::Relaxed);
|
||||||
for h in bg_handles { h.join().unwrap(); }
|
for h in bg_handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -129,7 +135,9 @@ fn bench_storm_tokio_current() -> (u64, u128) {
|
|||||||
let c2 = counter.clone();
|
let c2 = counter.clone();
|
||||||
let s2 = stop.clone();
|
let s2 = stop.clone();
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -149,9 +157,13 @@ fn bench_storm_tokio_current() -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
s2.store(true, Ordering::Relaxed);
|
s2.store(true, Ordering::Relaxed);
|
||||||
for h in bg_handles { let _ = h.await; }
|
for h in bg_handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -184,9 +196,13 @@ fn bench_storm_tokio_multi() -> (u64, u128) {
|
|||||||
cc.fetch_add(1, Ordering::Relaxed);
|
cc.fetch_add(1, Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
s2.store(true, Ordering::Relaxed);
|
s2.store(true, Ordering::Relaxed);
|
||||||
for h in bg_handles { let _ = h.await; }
|
for h in bg_handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(counter.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -219,14 +235,21 @@ fn bench_mpsc_smarm(threads: usize) -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
let _ = count; // discard; run() closure must return ()
|
let _ = count; // discard; run() closure must return ()
|
||||||
});
|
});
|
||||||
for h in prod_handles { h.join().unwrap(); }
|
for h in prod_handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
let _ = consumer.join().unwrap();
|
let _ = consumer.join().unwrap();
|
||||||
});
|
});
|
||||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
(
|
||||||
|
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||||
|
start.elapsed().as_micros(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_mpsc_tokio_current() -> (u64, u128) {
|
fn bench_mpsc_tokio_current() -> (u64, u128) {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
@@ -248,10 +271,15 @@ fn bench_mpsc_tokio_current() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
count
|
count
|
||||||
});
|
});
|
||||||
for h in prod_handles { let _ = h.await; }
|
for h in prod_handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
let _ = consumer.await;
|
let _ = consumer.await;
|
||||||
});
|
});
|
||||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
(
|
||||||
|
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||||
|
start.elapsed().as_micros(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_mpsc_tokio_multi() -> (u64, u128) {
|
fn bench_mpsc_tokio_multi() -> (u64, u128) {
|
||||||
@@ -279,10 +307,15 @@ fn bench_mpsc_tokio_multi() -> (u64, u128) {
|
|||||||
}
|
}
|
||||||
count
|
count
|
||||||
});
|
});
|
||||||
for h in prod_handles { let _ = h.await; }
|
for h in prod_handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
let _ = consumer.await;
|
let _ = consumer.await;
|
||||||
});
|
});
|
||||||
(MPSC_PRODUCERS * MPSC_PER_PRODUCER, start.elapsed().as_micros())
|
(
|
||||||
|
MPSC_PRODUCERS * MPSC_PER_PRODUCER,
|
||||||
|
start.elapsed().as_micros(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -308,7 +341,9 @@ fn bench_timers_smarm(threads: usize) -> (u64, u128) {
|
|||||||
smarm::sleep(Duration::from_millis(ms));
|
smarm::sleep(Duration::from_millis(ms));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -328,7 +363,9 @@ fn bench_timers_tokio_current() -> (u64, u128) {
|
|||||||
tokio::time::sleep(Duration::from_millis(ms)).await;
|
tokio::time::sleep(Duration::from_millis(ms)).await;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -348,7 +385,9 @@ fn bench_timers_tokio_multi() -> (u64, u128) {
|
|||||||
tokio::time::sleep(Duration::from_millis(ms)).await;
|
tokio::time::sleep(Duration::from_millis(ms)).await;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(TIMER_ACTORS, start.elapsed().as_micros())
|
(TIMER_ACTORS, start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -361,11 +400,22 @@ const SCALING_N: u64 = 400_000;
|
|||||||
const SCALING_WORKERS: u64 = 64;
|
const SCALING_WORKERS: u64 = 64;
|
||||||
|
|
||||||
fn is_prime(n: u64) -> bool {
|
fn is_prime(n: u64) -> bool {
|
||||||
if n < 2 { return false; }
|
if n < 2 {
|
||||||
if n < 4 { return true; }
|
return false;
|
||||||
if n % 2 == 0 { return false; }
|
}
|
||||||
|
if n < 4 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if n % 2 == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let mut i = 3u64;
|
let mut i = 3u64;
|
||||||
while i * i <= n { if n % i == 0 { return false; } i += 2; }
|
while i * i <= n {
|
||||||
|
if n % i == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +426,11 @@ fn count_primes(lo: u64, hi: u64) -> u64 {
|
|||||||
fn scaling_slice(w: u64) -> (u64, u64) {
|
fn scaling_slice(w: u64) -> (u64, u64) {
|
||||||
let per = SCALING_N / SCALING_WORKERS;
|
let per = SCALING_N / SCALING_WORKERS;
|
||||||
let lo = w * per;
|
let lo = w * per;
|
||||||
let hi = if w + 1 == SCALING_WORKERS { SCALING_N } else { lo + per };
|
let hi = if w + 1 == SCALING_WORKERS {
|
||||||
|
SCALING_N
|
||||||
|
} else {
|
||||||
|
lo + per
|
||||||
|
};
|
||||||
(lo, hi)
|
(lo, hi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +447,9 @@ fn bench_scaling_smarm(threads: usize) -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -415,7 +471,9 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
|||||||
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
tc.fetch_add(count_primes(lo, hi), Ordering::Relaxed);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { let _ = h.await; }
|
for h in handles {
|
||||||
|
let _ = h.await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
(total.load(Ordering::Relaxed), start.elapsed().as_micros())
|
||||||
}
|
}
|
||||||
@@ -424,7 +482,6 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
|||||||
// main
|
// main
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
// Knob helper — reads SMARM_ALLOC_INTERVAL / SMARM_TIMESLICE_CYCLES env vars
|
||||||
// so the sweep script can override the preemption knobs without recompiling.
|
// so the sweep script can override the preemption knobs without recompiling.
|
||||||
@@ -433,10 +490,14 @@ fn bench_scaling_tokio_multi(threads: usize) -> (u64, u128) {
|
|||||||
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
fn bench_cfg(threads: usize) -> smarm::runtime::Config {
|
||||||
let mut cfg = smarm::runtime::Config::exact(threads);
|
let mut cfg = smarm::runtime::Config::exact(threads);
|
||||||
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
if let Ok(v) = std::env::var("SMARM_ALLOC_INTERVAL") {
|
||||||
if let Ok(n) = v.parse::<u32>() { cfg = cfg.alloc_interval(n); }
|
if let Ok(n) = v.parse::<u32>() {
|
||||||
|
cfg = cfg.alloc_interval(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
if let Ok(v) = std::env::var("SMARM_TIMESLICE_CYCLES") {
|
||||||
if let Ok(n) = v.parse::<u64>() { cfg = cfg.timeslice_cycles(n); }
|
if let Ok(n) = v.parse::<u64>() {
|
||||||
|
cfg = cfg.timeslice_cycles(n);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
@@ -446,7 +507,10 @@ fn main() {
|
|||||||
println!("smarm tokio-favored benchmarks");
|
println!("smarm tokio-favored benchmarks");
|
||||||
println!("available parallelism: {n} threads");
|
println!("available parallelism: {n} threads");
|
||||||
let sets = env_sets();
|
let sets = env_sets();
|
||||||
println!("ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)", ITERS * sets);
|
println!(
|
||||||
|
"ITERS={ITERS}×{sets} sets = {} samples (+1 warmup, discarded)",
|
||||||
|
ITERS * sets
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"STORM_BACKGROUND={STORM_BACKGROUND}, STORM_SPAWN={STORM_SPAWN}, \
|
"STORM_BACKGROUND={STORM_BACKGROUND}, STORM_SPAWN={STORM_SPAWN}, \
|
||||||
MPSC={MPSC_PRODUCERS}×{MPSC_PER_PRODUCER}, \
|
MPSC={MPSC_PRODUCERS}×{MPSC_PER_PRODUCER}, \
|
||||||
@@ -477,7 +541,9 @@ fn main() {
|
|||||||
"many_timers: {TIMER_ACTORS} actors sleeping {TIMER_MIN_MS}–{TIMER_MAX_MS} ms"
|
"many_timers: {TIMER_ACTORS} actors sleeping {TIMER_MIN_MS}–{TIMER_MAX_MS} ms"
|
||||||
));
|
));
|
||||||
run_n("smarm 1-thread", ITERS, || bench_timers_smarm(1));
|
run_n("smarm 1-thread", ITERS, || bench_timers_smarm(1));
|
||||||
run_n(&format!("smarm {n}-thread"), ITERS, || bench_timers_smarm(n));
|
run_n(&format!("smarm {n}-thread"), ITERS, || {
|
||||||
|
bench_timers_smarm(n)
|
||||||
|
});
|
||||||
run_n("tokio current_thread", ITERS, bench_timers_tokio_current);
|
run_n("tokio current_thread", ITERS, bench_timers_tokio_current);
|
||||||
run_n("tokio multi-thread", ITERS, bench_timers_tokio_multi);
|
run_n("tokio multi-thread", ITERS, bench_timers_tokio_multi);
|
||||||
|
|
||||||
@@ -487,13 +553,19 @@ fn main() {
|
|||||||
));
|
));
|
||||||
let sweep: Vec<usize> = {
|
let sweep: Vec<usize> = {
|
||||||
let mut v = vec![1usize, 2, 4];
|
let mut v = vec![1usize, 2, 4];
|
||||||
if n > 4 && !v.contains(&n) { v.push(n); }
|
if n > 4 && !v.contains(&n) {
|
||||||
|
v.push(n);
|
||||||
|
}
|
||||||
v.into_iter().filter(|t| *t <= n).collect()
|
v.into_iter().filter(|t| *t <= n).collect()
|
||||||
};
|
};
|
||||||
for t in &sweep {
|
for t in &sweep {
|
||||||
run_n(&format!("smarm {t}-thread"), ITERS, || bench_scaling_smarm(*t));
|
run_n(&format!("smarm {t}-thread"), ITERS, || {
|
||||||
|
bench_scaling_smarm(*t)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for t in &sweep {
|
for t in &sweep {
|
||||||
run_n(&format!("tokio multi {t}-thread"), ITERS, || bench_scaling_tokio_multi(*t));
|
run_n(&format!("tokio multi {t}-thread"), ITERS, || {
|
||||||
|
bench_scaling_tokio_multi(*t)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1620,8 +1620,9 @@
|
|||||||
wait: <code>select</code> priority is <strong>Down arms › Watcher arm › info channels (declaration order) ›
|
wait: <code>select</code> priority is <strong>Down arms › Watcher arm › info channels (declaration order) ›
|
||||||
inbox</strong>, rebuilt each turn. A hot inbox can't starve a death notice or a system message;
|
inbox</strong>, rebuilt each turn. A hot inbox can't starve a death notice or a system message;
|
||||||
conversely a hot info channel <em>can</em> starve the inbox — deliberately. A closed info arm is
|
conversely a hot info channel <em>can</em> starve the inbox — deliberately. A closed info arm is
|
||||||
silently dropped from the set; a closed <em>inbox</em> (every <code>ServerRef</code> gone) is graceful
|
silently dropped from the set. The inbox never closes — the loop holds one sender for its whole
|
||||||
shutdown.</p>
|
life, so a <code>GenServerRef</code> is an address, not an owner: the server ends only by
|
||||||
|
<code>StopHandle::stop</code>, a shutdown, a hard stop, or a panic.</p>
|
||||||
|
|
||||||
<h3>Death needs no monitor</h3>
|
<h3>Death needs no monitor</h3>
|
||||||
<p>Server death detection falls out of channel closure. Already dead → the inbox is closed and
|
<p>Server death detection falls out of channel closure. Already dead → the inbox is closed and
|
||||||
@@ -1700,7 +1701,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="module-card">
|
<div class="module-card">
|
||||||
<div class="module-name" style="color:var(--red)">Panics in <code>terminate()</code></div>
|
<div class="module-name" style="color:var(--red)">Panics in <code>terminate()</code></div>
|
||||||
<p>gen_server's <code>terminate()</code> runs from a drop guard, possibly mid-unwind. A panic inside it during an unwind is a double panic → process abort, no supervision tree to save you. Keep it cheap, non-blocking, non-panicking.</p>
|
<p>gen_server's <code>terminate()</code> runs from a drop guard, possibly mid-unwind. A panic inside it during an unwind is a double panic → process abort, no supervision tree to save you. On the panic and hard-stop paths keep it cheap, non-blocking, non-panicking. Only the graceful path (<code>handle_shutdown → Exit</code>, <code>StopHandle::stop</code>) runs it outside an unwind, where it may do real work.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="module-card">
|
<div class="module-card">
|
||||||
<div class="module-name" style="color:var(--yellow)">Cold locks are leaf locks</div>
|
<div class="module-name" style="color:var(--yellow)">Cold locks are leaf locks</div>
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ fn main() {
|
|||||||
println!("calibration: {per_us} work iters/µs");
|
println!("calibration: {per_us} work iters/µs");
|
||||||
let work_us = move |us: u64| work_iters(us * per_us);
|
let work_us = move |us: u64| work_iters(us * per_us);
|
||||||
|
|
||||||
let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
let cores = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1);
|
||||||
println!("cores: {cores}");
|
println!("cores: {cores}");
|
||||||
if cores < 4 {
|
if cores < 4 {
|
||||||
println!("probe: SKIPPED (needs the stages in parallel)");
|
println!("probe: SKIPPED (needs the stages in parallel)");
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
|
|
||||||
#![deny(dead_code, unreachable_patterns)]
|
#![deny(dead_code, unreachable_patterns)]
|
||||||
|
|
||||||
|
use smarm::gen_statem::{spawn, Cx, GenStatemRef, Machine, Reply, Resolution, Step};
|
||||||
use smarm::run;
|
use smarm::run;
|
||||||
use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, Step, GenStatemRef};
|
|
||||||
|
|
||||||
// === user types ============================================================
|
// === user types ============================================================
|
||||||
|
|
||||||
@@ -123,7 +123,11 @@ impl DoorSm {
|
|||||||
fn start(init: Door) -> GenStatemRef<DoorSm> {
|
fn start(init: Door) -> GenStatemRef<DoorSm> {
|
||||||
spawn(DoorSm {
|
spawn(DoorSm {
|
||||||
state: init,
|
state: init,
|
||||||
data: Data { enters: 0, pushes: 0, knocks: 0 },
|
data: Data {
|
||||||
|
enters: 0,
|
||||||
|
pushes: 0,
|
||||||
|
knocks: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,16 +197,12 @@ impl Machine for DoorSm {
|
|||||||
(Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled,
|
(Door::Closed, Ev::Cast(Cast::Push | Cast::Unlock(_))) => Resolution::Unhandled,
|
||||||
|
|
||||||
// --- Locked (branching row: handler picks within UnlockOutcome) -
|
// --- Locked (branching row: handler picks within UnlockOutcome) -
|
||||||
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => {
|
(Door::Locked, Ev::Cast(Cast::Unlock(key))) => Resolution::To(on_unlock(key).into()),
|
||||||
Resolution::To(on_unlock(key).into())
|
|
||||||
}
|
|
||||||
// Routed out in phase 1; listed only to keep this match total.
|
// Routed out in phase 1; listed only to keep this match total.
|
||||||
(Door::Locked, Ev::Cast(Cast::Knock)) => {
|
(Door::Locked, Ev::Cast(Cast::Knock)) => {
|
||||||
unreachable!("postponed event is replayed, not dispatched here")
|
unreachable!("postponed event is replayed, not dispatched here")
|
||||||
}
|
}
|
||||||
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => {
|
(Door::Locked, Ev::Cast(Cast::Push | Cast::Pull | Cast::Lock)) => Resolution::Unhandled,
|
||||||
Resolution::Unhandled
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- state-independent queries (reply, then stay) ---------------
|
// --- state-independent queries (reply, then stay) ---------------
|
||||||
(_, Ev::Call(Call::GetState(r))) => {
|
(_, Ev::Call(Call::GetState(r))) => {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// dispatch's own unreachable_patterns internally.
|
// dispatch's own unreachable_patterns internally.
|
||||||
|
|
||||||
use smarm::gen_statem;
|
use smarm::gen_statem;
|
||||||
use smarm::run;
|
|
||||||
use smarm::gen_statem::Reply;
|
use smarm::gen_statem::Reply;
|
||||||
|
use smarm::run;
|
||||||
|
|
||||||
// === user types (identical to gen_statem_expanded.rs) =========================
|
// === user types (identical to gen_statem_expanded.rs) =========================
|
||||||
|
|
||||||
@@ -135,7 +135,14 @@ gen_statem! {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
run(|| {
|
run(|| {
|
||||||
let door = DoorSm::start(Door::Closed, Data { enters: 0, pushes: 0, knocks: 0 });
|
let door = DoorSm::start(
|
||||||
|
Door::Closed,
|
||||||
|
Data {
|
||||||
|
enters: 0,
|
||||||
|
pushes: 0,
|
||||||
|
knocks: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked
|
door.send(Ev::Cast(Cast::Lock)).unwrap(); // Closed -> Locked
|
||||||
door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted)
|
door.send(Ev::Cast(Cast::Knock)).unwrap(); // Locked: postponed (not yet counted)
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! Graceful shutdown, end to end: a supervised app tree, a server that
|
||||||
|
//! drains before it exits, and the two ways the whole thing winds down.
|
||||||
|
//!
|
||||||
|
//! Stopping an actor comes in two strengths, as in OTP:
|
||||||
|
//! - `request_stop(pid)` = `exit(Pid, kill)`: cooperative hard stop,
|
||||||
|
//! unwinds at the next observation point.
|
||||||
|
//! - `request_shutdown(pid)` = `exit(Pid, shutdown)`: a trapping target gets
|
||||||
|
//! an `ExitSignal { reason: Shutdown }` and winds
|
||||||
|
//! down on its own terms; a non-trapping one is
|
||||||
|
//! stopped outright.
|
||||||
|
//!
|
||||||
|
//! A supervisor traps exits. `request_shutdown(sup)` runs its ordered
|
||||||
|
//! shutdown — children in reverse start order, each per its `ChildSpec`
|
||||||
|
//! `Shutdown` policy (`Timeout(d)` default 5s, `Infinity`, `BrutalKill`) —
|
||||||
|
//! and the supervisor then returns normally.
|
||||||
|
//!
|
||||||
|
//! Two triggers are shown:
|
||||||
|
//! 1. **Root exit.** The run's root actor returning means "the program is
|
||||||
|
//! done": the runtime delivers `request_shutdown` to every top-level actor
|
||||||
|
//! (here: the supervisor). Trapping actors may keep running to drain and
|
||||||
|
//! end the run when they stop themselves; non-trapping ones are stopped.
|
||||||
|
//! 2. **An outside thread** (e.g. a signal handler) driving it via
|
||||||
|
//! `RuntimeHandle::request_shutdown` on the supervisor — the root then
|
||||||
|
//! just waits for the tree to come down.
|
||||||
|
|
||||||
|
use smarm::gen_server::{
|
||||||
|
GenServer, GenServerBuilder, GenServerCtx, GenServerName, ShutdownAction, StopHandle,
|
||||||
|
TimerHandle,
|
||||||
|
};
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
|
||||||
|
use smarm::{sleep, spawn};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// A server with in-flight work: on shutdown it stops accepting, finishes what
|
||||||
|
/// it has (simulated with a ticking timer), then ends itself.
|
||||||
|
struct Drainer {
|
||||||
|
pending: u32,
|
||||||
|
stop: Option<StopHandle<Drainer>>,
|
||||||
|
timer: Option<TimerHandle<Drainer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenServer for Drainer {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
|
ctx.trap_exit(); // opt in: shutdown arrives as handle_shutdown
|
||||||
|
self.stop = Some(ctx.stop_handle());
|
||||||
|
self.timer = Some(ctx.timer());
|
||||||
|
}
|
||||||
|
fn handle_call(&mut self, _: ()) {}
|
||||||
|
fn handle_cast(&mut self, _: ()) {}
|
||||||
|
fn handle_shutdown(&mut self) -> ShutdownAction {
|
||||||
|
println!(
|
||||||
|
"drainer: shutdown requested, {} items pending",
|
||||||
|
self.pending
|
||||||
|
);
|
||||||
|
self.timer
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.tick_every(Duration::from_millis(20), ());
|
||||||
|
ShutdownAction::Continue // keep serving until drained
|
||||||
|
}
|
||||||
|
fn handle_timer(&mut self, _: ()) {
|
||||||
|
self.pending -= 1;
|
||||||
|
if self.pending == 0 {
|
||||||
|
println!("drainer: drained, stopping");
|
||||||
|
self.stop.as_ref().unwrap().stop(); // normal exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn terminate(&mut self) {
|
||||||
|
// Graceful path: this runs on the normal path and may block.
|
||||||
|
println!("drainer: terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The server's name: how the rest of the app reaches it (and the only handle
|
||||||
|
/// that survives a restart).
|
||||||
|
const DRAINER: GenServerName<Drainer> = GenServerName::new("drainer");
|
||||||
|
|
||||||
|
fn app_tree() -> OneForOne {
|
||||||
|
OneForOne::new()
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, || {
|
||||||
|
// A plain worker that does not trap: stopped outright on shutdown.
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Timeout(Duration::from_millis(100))),
|
||||||
|
)
|
||||||
|
// A gen_server is a direct child: `named(N).run()` runs the loop as
|
||||||
|
// the child actor itself, so the supervisor's shutdown arrives as
|
||||||
|
// `handle_shutdown` and a restart re-binds the name.
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, || {
|
||||||
|
GenServerBuilder::new(Drainer {
|
||||||
|
pending: 3,
|
||||||
|
stop: None,
|
||||||
|
timer: None,
|
||||||
|
})
|
||||||
|
.named(DRAINER)
|
||||||
|
.run()
|
||||||
|
.expect("drainer name is free");
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Infinity),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("--- 1. root exit drives the shutdown ---");
|
||||||
|
smarm::run(|| {
|
||||||
|
spawn(|| app_tree().run());
|
||||||
|
sleep(Duration::from_millis(50)); // the app "runs" for a while
|
||||||
|
// Returning here asks the supervisor to shut down; the run ends when
|
||||||
|
// the tree — drainer included — is gone.
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("--- 2. an outside thread drives the shutdown ---");
|
||||||
|
let rt = smarm::init(smarm::Config::default());
|
||||||
|
let handle = rt.handle(); // Send + Sync; grab it before run
|
||||||
|
rt.run(move || {
|
||||||
|
let sup = spawn(|| app_tree().run());
|
||||||
|
let sup_pid = sup.pid();
|
||||||
|
// Stand-in for a SIGTERM handler thread.
|
||||||
|
thread::spawn(move || {
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
println!("signal thread: requesting shutdown");
|
||||||
|
handle.request_shutdown(sup_pid);
|
||||||
|
});
|
||||||
|
sup.join()
|
||||||
|
.expect("supervisor returns normally after ordered shutdown");
|
||||||
|
println!("supervisor down; root returns");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
//! every use — so the address keeps working across a supervised restart, with
|
//! every use — so the address keeps working across a supervised restart, with
|
||||||
//! no stale [`GenServerRef`] to refresh.
|
//! no stale [`GenServerRef`] to refresh.
|
||||||
|
|
||||||
use smarm::{call, cast, run, whereis_server, GenServer, GenServerBuilder, GenServerName, GenServerRef};
|
use smarm::{
|
||||||
|
call, cast, run, whereis_server, GenServer, GenServerBuilder, GenServerName, GenServerRef,
|
||||||
|
};
|
||||||
|
|
||||||
/// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
|
/// A counter server: synchronous `Get`, asynchronous `Inc` / `Add`.
|
||||||
struct Counter {
|
struct Counter {
|
||||||
@@ -64,6 +66,12 @@ fn main() {
|
|||||||
let svc: Option<GenServerRef<Counter>> = whereis_server(COUNTER);
|
let svc: Option<GenServerRef<Counter>> = whereis_server(COUNTER);
|
||||||
if let Some(svc) = svc {
|
if let Some(svc) = svc {
|
||||||
let _ = svc.call(Query::Get);
|
let _ = svc.call(Query::Get);
|
||||||
|
// A named server is pinned alive by the registry, so dropping refs
|
||||||
|
// does not end it. Stop it explicitly: `shutdown()` asks politely
|
||||||
|
// (a trapping server drains first; this one is stopped outright)
|
||||||
|
// and waits until it is gone. Left running, the root's return
|
||||||
|
// would shut it down the same way — see examples/graceful_shutdown.rs.
|
||||||
|
svc.shutdown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -15,7 +15,9 @@
|
|||||||
//! `call`, nothing more.
|
//! `call`, nothing more.
|
||||||
|
|
||||||
use smarm::observer::{self, ObserverReply, ObserverRequest};
|
use smarm::observer::{self, ObserverReply, ObserverRequest};
|
||||||
use smarm::{channel, register, run, spawn, ActorState, Name, RuntimeSnapshot, RuntimeTree, TreeNode};
|
use smarm::{
|
||||||
|
channel, register, run, spawn, ActorState, Name, RuntimeSnapshot, RuntimeTree, TreeNode,
|
||||||
|
};
|
||||||
|
|
||||||
const ECHO: Name<u64> = Name::new("echo");
|
const ECHO: Name<u64> = Name::new("echo");
|
||||||
|
|
||||||
@@ -31,7 +33,11 @@ fn state_glyph(s: ActorState) -> &'static str {
|
|||||||
|
|
||||||
/// A `ps`-style table over the flat snapshot.
|
/// A `ps`-style table over the flat snapshot.
|
||||||
fn print_snapshot(snap: &RuntimeSnapshot) {
|
fn print_snapshot(snap: &RuntimeSnapshot) {
|
||||||
println!("snapshot (format v{}, {} actors)", snap.format_version, snap.actors.len());
|
println!(
|
||||||
|
"snapshot (format v{}, {} actors)",
|
||||||
|
snap.format_version,
|
||||||
|
snap.actors.len()
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
" {:<10} {:<9} {:<10} {:>4} {:>4} {:>4} {:>4} {:>5} {}",
|
" {:<10} {:<9} {:<10} {:>4} {:>4} {:>4} {:>4} {:>5} {}",
|
||||||
"pid", "state", "parent", "mon", "lnk", "joi", "mbox", "msgs", "names"
|
"pid", "state", "parent", "mon", "lnk", "joi", "mbox", "msgs", "names"
|
||||||
@@ -52,7 +58,11 @@ fn print_snapshot(snap: &RuntimeSnapshot) {
|
|||||||
a.joiners,
|
a.joiners,
|
||||||
a.mailbox_depth,
|
a.mailbox_depth,
|
||||||
a.messages_received,
|
a.messages_received,
|
||||||
if a.names.is_empty() { "-".to_string() } else { a.names.join(",") },
|
if a.names.is_empty() {
|
||||||
|
"-".to_string()
|
||||||
|
} else {
|
||||||
|
a.names.join(",")
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ pub extern "C-unwind" fn trampoline() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
|
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
|
||||||
Ok(()) => Outcome::Exit,
|
Ok(()) => Outcome::Exit,
|
||||||
Err(payload) => {
|
Err(payload) => {
|
||||||
if payload.is::<StopSentinel>() {
|
if payload.is::<StopSentinel>() {
|
||||||
Outcome::Stopped
|
Outcome::Stopped
|
||||||
|
|||||||
+20
-10
@@ -342,8 +342,7 @@ mod inner {
|
|||||||
// Count the loss in would-be delta terms so the audit's columns
|
// Count the loss in would-be delta terms so the audit's columns
|
||||||
// compare directly against `injected_cycles`.
|
// compare directly against `injected_cycles`.
|
||||||
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
DISCARD_OVERMAX_N.fetch_add(1, Ordering::Relaxed);
|
||||||
DISCARD_OVERMAX_CYCLES
|
DISCARD_OVERMAX_CYCLES.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
.fetch_add(interval.saturating_mul(pct) / 100, Ordering::Relaxed);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let delta = interval.saturating_mul(pct) / 100;
|
let delta = interval.saturating_mul(pct) / 100;
|
||||||
@@ -419,8 +418,7 @@ mod inner {
|
|||||||
let gap = preempt::rdtsc()
|
let gap = preempt::rdtsc()
|
||||||
.saturating_sub(desched_tsc)
|
.saturating_sub(desched_tsc)
|
||||||
.min(MAX_SAMPLE_CYCLES);
|
.min(MAX_SAMPLE_CYCLES);
|
||||||
OFFCPU_IN_SITE_CYCLES
|
OFFCPU_IN_SITE_CYCLES.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
||||||
.fetch_add(gap.saturating_mul(pct) / 100, Ordering::Relaxed);
|
|
||||||
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
OFFCPU_IN_SITE_N.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +531,9 @@ mod inner {
|
|||||||
park_forgiven_cycles: self
|
park_forgiven_cycles: self
|
||||||
.park_forgiven_cycles
|
.park_forgiven_cycles
|
||||||
.saturating_sub(before.park_forgiven_cycles),
|
.saturating_sub(before.park_forgiven_cycles),
|
||||||
drop_park_cycles: self.drop_park_cycles.saturating_sub(before.drop_park_cycles),
|
drop_park_cycles: self
|
||||||
|
.drop_park_cycles
|
||||||
|
.saturating_sub(before.drop_park_cycles),
|
||||||
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
drop_park_n: self.drop_park_n.saturating_sub(before.drop_park_n),
|
||||||
drop_yield_cycles: self
|
drop_yield_cycles: self
|
||||||
.drop_yield_cycles
|
.drop_yield_cycles
|
||||||
@@ -542,12 +542,18 @@ mod inner {
|
|||||||
discard_overmax_cycles: self
|
discard_overmax_cycles: self
|
||||||
.discard_overmax_cycles
|
.discard_overmax_cycles
|
||||||
.saturating_sub(before.discard_overmax_cycles),
|
.saturating_sub(before.discard_overmax_cycles),
|
||||||
discard_overmax_n: self.discard_overmax_n.saturating_sub(before.discard_overmax_n),
|
discard_overmax_n: self
|
||||||
discard_unarmed_n: self.discard_unarmed_n.saturating_sub(before.discard_unarmed_n),
|
.discard_overmax_n
|
||||||
|
.saturating_sub(before.discard_overmax_n),
|
||||||
|
discard_unarmed_n: self
|
||||||
|
.discard_unarmed_n
|
||||||
|
.saturating_sub(before.discard_unarmed_n),
|
||||||
offcpu_in_site_cycles: self
|
offcpu_in_site_cycles: self
|
||||||
.offcpu_in_site_cycles
|
.offcpu_in_site_cycles
|
||||||
.saturating_sub(before.offcpu_in_site_cycles),
|
.saturating_sub(before.offcpu_in_site_cycles),
|
||||||
offcpu_in_site_n: self.offcpu_in_site_n.saturating_sub(before.offcpu_in_site_n),
|
offcpu_in_site_n: self
|
||||||
|
.offcpu_in_site_n
|
||||||
|
.saturating_sub(before.offcpu_in_site_n),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +801,9 @@ mod inner {
|
|||||||
let cell = results
|
let cell = results
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
.find(|r| r.site == site && r.speedup_pct == speedup_pct)?;
|
||||||
let base = results.iter().find(|r| r.site == site && r.speedup_pct == 0)?;
|
let base = results
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.site == site && r.speedup_pct == 0)?;
|
||||||
let rate = normalized_rate(cell, point)?;
|
let rate = normalized_rate(cell, point)?;
|
||||||
let b = normalized_rate(base, point)?;
|
let b = normalized_rate(base, point)?;
|
||||||
if b <= 0.0 {
|
if b <= 0.0 {
|
||||||
@@ -946,7 +954,9 @@ macro_rules! progress {
|
|||||||
macro_rules! causal_site {
|
macro_rules! causal_site {
|
||||||
($name:literal) => {{
|
($name:literal) => {{
|
||||||
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
static __SMARM_SITE: ::std::sync::OnceLock<u32> = ::std::sync::OnceLock::new();
|
||||||
$crate::causal::SiteGuard::enter(*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)))
|
$crate::causal::SiteGuard::enter(
|
||||||
|
*__SMARM_SITE.get_or_init(|| $crate::causal::site_id($name)),
|
||||||
|
)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+85
-48
@@ -90,8 +90,9 @@
|
|||||||
|
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::raw_mutex::RawMutex;
|
use crate::raw_mutex::RawMutex;
|
||||||
|
use crate::runtime::RuntimeInner;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
/// Create a new channel and return its `(Sender, Receiver)` halves.
|
/// Create a new channel and return its `(Sender, Receiver)` halves.
|
||||||
///
|
///
|
||||||
@@ -104,17 +105,25 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
|||||||
senders: 1,
|
senders: 1,
|
||||||
receiver_alive: true,
|
receiver_alive: true,
|
||||||
}));
|
}));
|
||||||
(Sender { inner: inner.clone() }, Receiver { inner })
|
(
|
||||||
|
Sender {
|
||||||
|
inner: inner.clone(),
|
||||||
|
},
|
||||||
|
Receiver { inner },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Inner<T> {
|
struct Inner<T> {
|
||||||
queue: VecDeque<T>,
|
queue: VecDeque<T>,
|
||||||
/// The parked receiver's `(pid, park-epoch)`, if one is currently
|
/// The parked receiver's `(pid, park-epoch, runtime)`, if one is currently
|
||||||
/// waiting. The epoch identifies exactly which wait this is, so a waker
|
/// waiting. The epoch identifies exactly which wait this is, so a waker
|
||||||
/// left over from a wait that already ended (a losing `select` arm, a
|
/// left over from a wait that already ended (a losing `select` arm, a
|
||||||
/// `recv_timeout` whose timer fired after it was already satisfied) is
|
/// `recv_timeout` whose timer fired after it was already satisfied) is
|
||||||
/// inert and does nothing when it fires.
|
/// inert and does nothing when it fires. The `Weak<RuntimeInner>` is the
|
||||||
parked_receiver: Option<(Pid, u32)>,
|
/// receiver's runtime, captured while it parked (so provably alive then);
|
||||||
|
/// it lets a sender on a foreign OS thread wake the receiver without the
|
||||||
|
/// `RUNTIME` thread-local, which is unset off a scheduler thread.
|
||||||
|
parked_receiver: Option<(Pid, u32, Weak<RuntimeInner>)>,
|
||||||
senders: usize,
|
senders: usize,
|
||||||
receiver_alive: bool,
|
receiver_alive: bool,
|
||||||
}
|
}
|
||||||
@@ -178,7 +187,9 @@ impl std::error::Error for RecvTimeoutError {}
|
|||||||
impl<T> Clone for Sender<T> {
|
impl<T> Clone for Sender<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
self.inner.lock().senders += 1;
|
self.inner.lock().senders += 1;
|
||||||
Sender { inner: self.inner.clone() }
|
Sender {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +210,8 @@ impl<T> Drop for Sender<T> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some((pid, epoch)) = unpark {
|
if let Some((pid, epoch, rt)) = unpark {
|
||||||
crate::scheduler::unpark_at(pid, epoch);
|
crate::scheduler::unpark_at_via(pid, epoch, &rt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,11 +258,19 @@ impl<T> Sender<T> {
|
|||||||
g.queue.push_back(value);
|
g.queue.push_back(value);
|
||||||
g.parked_receiver.take()
|
g.parked_receiver.take()
|
||||||
};
|
};
|
||||||
if let Some((pid, epoch)) = unpark {
|
if let Some((pid, epoch, rt)) = unpark {
|
||||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: Some(pid) });
|
crate::te!(crate::trace::Event::Send {
|
||||||
crate::scheduler::unpark_at(pid, epoch);
|
sender: crate::actor::current_pid()
|
||||||
|
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||||
|
receiver: Some(pid)
|
||||||
|
});
|
||||||
|
crate::scheduler::unpark_at_via(pid, epoch, &rt);
|
||||||
} else {
|
} else {
|
||||||
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: None });
|
crate::te!(crate::trace::Event::Send {
|
||||||
|
sender: crate::actor::current_pid()
|
||||||
|
.unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)),
|
||||||
|
receiver: None
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -278,22 +297,28 @@ impl<T> Receiver<T> {
|
|||||||
None => panic!("smarm: recv() called outside an actor"),
|
None => panic!("smarm: recv() called outside an actor"),
|
||||||
};
|
};
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
g.parked_receiver.is_none_or(|(p, _)| p == me),
|
g.parked_receiver.as_ref().is_none_or(|(p, _, _)| *p == me),
|
||||||
"channel has more than one receiver"
|
"channel has more than one receiver"
|
||||||
);
|
);
|
||||||
// begin_wait is lock-free, so it's legal under the Channel lock;
|
// begin_wait is lock-free, so it's legal under the Channel lock;
|
||||||
// registering in the same critical section makes the epoch
|
// registering in the same critical section makes the epoch
|
||||||
// atomic with the senders' view of the registration.
|
// atomic with the senders' view of the registration.
|
||||||
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
g.parked_receiver = Some((
|
||||||
|
me,
|
||||||
|
crate::scheduler::begin_wait(),
|
||||||
|
crate::scheduler::runtime_weak(),
|
||||||
|
));
|
||||||
crate::te!(crate::trace::Event::RecvPark(me));
|
crate::te!(crate::trace::Event::RecvPark(me));
|
||||||
}
|
}
|
||||||
// Release the lock before parking: the unparker will need it.
|
// Release the lock before parking: the unparker will need it.
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
// Woken up. Record it before looping to check the queue.
|
// Woken up. Record it before looping to check the queue.
|
||||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
crate::te!(crate::trace::Event::RecvWake(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,11 +355,11 @@ impl<T> Receiver<T> {
|
|||||||
return Err(RecvTimeoutError::Disconnected);
|
return Err(RecvTimeoutError::Disconnected);
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
g.parked_receiver.is_none_or(|(p, _)| p == me),
|
g.parked_receiver.as_ref().is_none_or(|(p, _, _)| *p == me),
|
||||||
"channel has more than one receiver"
|
"channel has more than one receiver"
|
||||||
);
|
);
|
||||||
epoch = crate::scheduler::begin_wait();
|
epoch = crate::scheduler::begin_wait();
|
||||||
g.parked_receiver = Some((me, epoch));
|
g.parked_receiver = Some((me, epoch, crate::scheduler::runtime_weak()));
|
||||||
crate::te!(crate::trace::Event::RecvPark(me));
|
crate::te!(crate::trace::Event::RecvPark(me));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,10 +372,12 @@ impl<T> Receiver<T> {
|
|||||||
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
|
||||||
|
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
crate::te!(crate::trace::Event::RecvWake(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
let mut g = self.inner.lock();
|
let mut g = self.inner.lock();
|
||||||
if let Some(v) = g.queue.pop_front() {
|
if let Some(v) = g.queue.pop_front() {
|
||||||
crate::preempt::note_message_received();
|
crate::preempt::note_message_received();
|
||||||
@@ -404,18 +431,24 @@ impl<T> Receiver<T> {
|
|||||||
None => panic!("smarm: recv_match() called outside an actor"),
|
None => panic!("smarm: recv_match() called outside an actor"),
|
||||||
};
|
};
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
g.parked_receiver.is_none_or(|(p, _)| p == me),
|
g.parked_receiver.as_ref().is_none_or(|(p, _, _)| *p == me),
|
||||||
"channel has more than one receiver"
|
"channel has more than one receiver"
|
||||||
);
|
);
|
||||||
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
|
g.parked_receiver = Some((
|
||||||
|
me,
|
||||||
|
crate::scheduler::begin_wait(),
|
||||||
|
crate::scheduler::runtime_weak(),
|
||||||
|
));
|
||||||
crate::te!(crate::trace::Event::RecvPark(me));
|
crate::te!(crate::trace::Event::RecvPark(me));
|
||||||
}
|
}
|
||||||
// Release the lock before parking: the unparker will need it.
|
// Release the lock before parking: the unparker will need it.
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
crate::te!(crate::trace::Event::RecvWake(match crate::actor::current_pid() {
|
crate::te!(crate::trace::Event::RecvWake(
|
||||||
Some(p) => p,
|
match crate::actor::current_pid() {
|
||||||
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
Some(p) => p,
|
||||||
}));
|
None => panic!("smarm: RecvWake outside an actor (core corrupt)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,11 +509,12 @@ impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
|
|||||||
// keeps the registration bookkeeping exact.)
|
// keeps the registration bookkeeping exact.)
|
||||||
let unpark = {
|
let unpark = {
|
||||||
let mut g = self.lock();
|
let mut g = self.lock();
|
||||||
if g.parked_receiver == Some((pid, epoch)) {
|
match g.parked_receiver {
|
||||||
g.parked_receiver = None;
|
Some((p, e, _)) if p == pid && e == epoch => {
|
||||||
true
|
g.parked_receiver = None;
|
||||||
} else {
|
true
|
||||||
false
|
}
|
||||||
|
_ => false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Unpark outside the channel lock: it may take the run-queue lock;
|
// Unpark outside the channel lock: it may take the run-queue lock;
|
||||||
@@ -541,10 +575,10 @@ impl<T> Selectable for Receiver<T> {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
g.parked_receiver.is_none_or(|(p, _)| p == pid),
|
g.parked_receiver.as_ref().is_none_or(|(p, _, _)| *p == pid),
|
||||||
"channel has more than one receiver"
|
"channel has more than one receiver"
|
||||||
);
|
);
|
||||||
g.parked_receiver = Some((pid, epoch));
|
g.parked_receiver = Some((pid, epoch, crate::scheduler::runtime_weak()));
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +650,12 @@ pub fn try_select(arms: &[&dyn Selectable]) -> std::io::Result<usize> {
|
|||||||
// Channel-only selects skip all of it: `eager` is false, the guard
|
// Channel-only selects skip all of it: `eager` is false, the guard
|
||||||
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
// is disarmed, and the loser-arm self-cleaning story is unchanged.
|
||||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
let mut guard = UnregisterGuard {
|
||||||
|
arms,
|
||||||
|
me,
|
||||||
|
epoch,
|
||||||
|
armed: eager,
|
||||||
|
};
|
||||||
|
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
|
|
||||||
@@ -687,11 +726,7 @@ impl Drop for UnregisterGuard<'_> {
|
|||||||
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
// unregistered eagerly so none are left dangling. `Err` = an arm failed to
|
||||||
// register; same unwind (earlier fd arms unregistered, wait retired).
|
// register; same unwind (earlier fd arms unregistered, wait retired).
|
||||||
// `Ok(None)` = every arm registered successfully; the caller parks.
|
// `Ok(None)` = every arm registered successfully; the caller parks.
|
||||||
fn register_arms(
|
fn register_arms(me: Pid, epoch: u32, arms: &[&dyn Selectable]) -> std::io::Result<Option<usize>> {
|
||||||
me: Pid,
|
|
||||||
epoch: u32,
|
|
||||||
arms: &[&dyn Selectable],
|
|
||||||
) -> std::io::Result<Option<usize>> {
|
|
||||||
for (i, arm) in arms.iter().enumerate() {
|
for (i, arm) in arms.iter().enumerate() {
|
||||||
let registered = match arm.sel_register(me, epoch) {
|
let registered = match arm.sel_register(me, epoch) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -736,10 +771,7 @@ impl crate::timer::TimerTarget for SelectTimeout {
|
|||||||
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
/// Panics if `arms` is empty, if called outside an actor, or if an fd arm
|
||||||
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
/// fails to register (see [`try_select_timeout`] for the fallible form; a
|
||||||
/// channel-only select can never fail).
|
/// channel-only select can never fail).
|
||||||
pub fn select_timeout(
|
pub fn select_timeout(arms: &[&dyn Selectable], timeout: std::time::Duration) -> Option<usize> {
|
||||||
arms: &[&dyn Selectable],
|
|
||||||
timeout: std::time::Duration,
|
|
||||||
) -> Option<usize> {
|
|
||||||
match try_select_timeout(arms, timeout) {
|
match try_select_timeout(arms, timeout) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => panic!(
|
Err(e) => panic!(
|
||||||
@@ -776,7 +808,12 @@ pub fn try_select_timeout(
|
|||||||
// would leave those fds unusable until a kernel event happened to
|
// would leave those fds unusable until a kernel event happened to
|
||||||
// clear them.
|
// clear them.
|
||||||
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
let eager = arms.iter().any(|a| a.sel_eager_cleanup());
|
||||||
let mut guard = UnregisterGuard { arms, me, epoch, armed: eager };
|
let mut guard = UnregisterGuard {
|
||||||
|
arms,
|
||||||
|
me,
|
||||||
|
epoch,
|
||||||
|
armed: eager,
|
||||||
|
};
|
||||||
|
|
||||||
crate::scheduler::park_current();
|
crate::scheduler::park_current();
|
||||||
|
|
||||||
|
|||||||
+26
-11
@@ -16,10 +16,18 @@ thread_local! {
|
|||||||
static ACTOR_SP: Cell<usize> = const { Cell::new(0) };
|
static ACTOR_SP: Cell<usize> = const { Cell::new(0) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_scheduler_sp() -> usize { SCHEDULER_SP.with(|c| c.get()) }
|
fn get_scheduler_sp() -> usize {
|
||||||
fn set_scheduler_sp(v: usize) { SCHEDULER_SP.with(|c| c.set(v)) }
|
SCHEDULER_SP.with(|c| c.get())
|
||||||
pub fn get_actor_sp() -> usize { ACTOR_SP.with(|c| c.get()) }
|
}
|
||||||
pub fn set_actor_sp(v: usize) { ACTOR_SP.with(|c| c.set(v)) }
|
fn set_scheduler_sp(v: usize) {
|
||||||
|
SCHEDULER_SP.with(|c| c.set(v))
|
||||||
|
}
|
||||||
|
pub fn get_actor_sp() -> usize {
|
||||||
|
ACTOR_SP.with(|c| c.get())
|
||||||
|
}
|
||||||
|
pub fn set_actor_sp(v: usize) {
|
||||||
|
ACTOR_SP.with(|c| c.set(v))
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Initial stack layout
|
// Initial stack layout
|
||||||
@@ -49,13 +57,20 @@ pub fn set_actor_sp(v: usize) { ACTOR_SP.with(|c| c.set(v)) }
|
|||||||
pub fn init_actor_stack(top: *mut u8, entry: extern "C-unwind" fn()) -> usize {
|
pub fn init_actor_stack(top: *mut u8, entry: extern "C-unwind" fn()) -> usize {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut sp = (top as usize & !15) - 8;
|
let mut sp = (top as usize & !15) - 8;
|
||||||
sp -= 8; (sp as *mut usize).write(entry as usize); // ret target
|
sp -= 8;
|
||||||
sp -= 8; (sp as *mut usize).write(0); // rbx
|
(sp as *mut usize).write(entry as usize); // ret target
|
||||||
sp -= 8; (sp as *mut usize).write(0); // rbp
|
sp -= 8;
|
||||||
sp -= 8; (sp as *mut usize).write(0); // r12
|
(sp as *mut usize).write(0); // rbx
|
||||||
sp -= 8; (sp as *mut usize).write(0); // r13
|
sp -= 8;
|
||||||
sp -= 8; (sp as *mut usize).write(0); // r14
|
(sp as *mut usize).write(0); // rbp
|
||||||
sp -= 8; (sp as *mut usize).write(0); // r15
|
sp -= 8;
|
||||||
|
(sp as *mut usize).write(0); // r12
|
||||||
|
sp -= 8;
|
||||||
|
(sp as *mut usize).write(0); // r13
|
||||||
|
sp -= 8;
|
||||||
|
(sp as *mut usize).write(0); // r14
|
||||||
|
sp -= 8;
|
||||||
|
(sp as *mut usize).write(0); // r15
|
||||||
sp
|
sp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+293
-69
@@ -127,16 +127,51 @@
|
|||||||
//! - [`GenServer::init`] runs once before the first message. Use it to start
|
//! - [`GenServer::init`] runs once before the first message. Use it to start
|
||||||
//! timers or set up monitors; see the [`GenServerCtx`] it receives.
|
//! timers or set up monitors; see the [`GenServerCtx`] it receives.
|
||||||
//! - [`GenServer::terminate`] runs when the server is about to exit. It fires
|
//! - [`GenServer::terminate`] runs when the server is about to exit. It fires
|
||||||
//! on every exit path (all `GenServerRef`s dropped, a handler panic, or an
|
//! on every exit path (a self-stop, a graceful shutdown, a cooperative hard
|
||||||
//! explicit [`GenServerRef::shutdown`]), not only on clean shutdown. Keep it
|
//! stop, or a handler panic), not only on clean shutdown.
|
||||||
//! short and non-blocking: if `terminate` panics while the server is already
|
//! Keep it non-panicking: on the panic and hard-stop paths it runs
|
||||||
//! unwinding from a handler panic, the process aborts.
|
//! mid-unwind, where a second panic aborts the process and where it must
|
||||||
|
//! not block (any park re-observes the stop). Only on the graceful path
|
||||||
|
//! (see below) may it do real work.
|
||||||
//!
|
//!
|
||||||
//! ## When the server stops
|
//! ## When the server stops
|
||||||
//!
|
//!
|
||||||
//! The server runs as long as at least one [`GenServerRef`] exists. When the last
|
//! A server lives until it stops, is shut down, or is killed — as an OTP
|
||||||
//! one is dropped, the inbox closes and the loop exits gracefully. To stop a
|
//! process does. A [`GenServerRef`] is an *address*: cloning and dropping it
|
||||||
//! server explicitly and wait for it to finish, call [`GenServerRef::shutdown`].
|
//! never changes the server's lifetime, and a ref that nobody holds is not a
|
||||||
|
//! leak — a forgotten server idles until the run ends, when the root-exit
|
||||||
|
//! shutdown (see [`Runtime::run`](crate::Runtime::run)) takes it down with
|
||||||
|
//! every other unsupervised actor. Anything meant to live long should be
|
||||||
|
//! supervised (see *Supervised servers* below); [`start`] / [`start_under`]
|
||||||
|
//! are for scripts, tests and short-lived helpers, and the explicit close is
|
||||||
|
//! [`GenServerRef::shutdown`].
|
||||||
|
//!
|
||||||
|
//! A server can end itself: clone a [`StopHandle`] from
|
||||||
|
//! [`GenServerCtx::stop_handle`] in `init` and call [`StopHandle::stop`] from
|
||||||
|
//! any handler — the loop breaks after the current message and exits
|
||||||
|
//! *normally* (OTP's `{stop, normal}`). This is distinct from
|
||||||
|
//! `request_stop(self_pid())`, which is an abnormal `Stopped` and gets a
|
||||||
|
//! `Transient` child restarted.
|
||||||
|
//!
|
||||||
|
//! ## Graceful shutdown
|
||||||
|
//!
|
||||||
|
//! From outside, [`GenServerRef::shutdown`] (or a plain
|
||||||
|
//! [`request_shutdown`](crate::request_shutdown), which is what a supervisor
|
||||||
|
//! sends) asks the server to stop. What happens next is the server's choice:
|
||||||
|
//!
|
||||||
|
//! - By default a server does not trap exits, and the request stops it
|
||||||
|
//! outright at its next observation point — `terminate` runs mid-unwind.
|
||||||
|
//! - A server that calls [`GenServerCtx::trap_exit`] in `init` receives the
|
||||||
|
//! request as [`GenServer::handle_shutdown`]. Return
|
||||||
|
//! [`ShutdownAction::Exit`] (the default) to have the loop break and
|
||||||
|
//! `terminate` run on the normal path, where it may block; return
|
||||||
|
//! [`ShutdownAction::Continue`] to keep serving — e.g. to drain in-flight
|
||||||
|
//! work — and end the server later with a [`StopHandle`]. The supervisor's
|
||||||
|
//! [`Shutdown`](crate::supervisor::Shutdown) policy bounds how long that
|
||||||
|
//! may take before it falls back to a hard stop.
|
||||||
|
//!
|
||||||
|
//! A trapping server also receives the deaths of its linked peers as
|
||||||
|
//! [`GenServer::handle_exit`] messages instead of dying with them.
|
||||||
//!
|
//!
|
||||||
//! If the server panics inside a handler, the panic unwinds the server thread.
|
//! If the server panics inside a handler, the panic unwinds the server thread.
|
||||||
//! Any caller currently waiting in `call` sees `Err(ServerDown)`: the reply
|
//! Any caller currently waiting in `call` sees `Err(ServerDown)`: the reply
|
||||||
@@ -167,9 +202,23 @@
|
|||||||
//! Names and registration: a server can be given a static name so other
|
//! Names and registration: a server can be given a static name so other
|
||||||
//! actors can reach it without holding a `GenServerRef`. Use
|
//! actors can reach it without holding a `GenServerRef`. Use
|
||||||
//! [`GenServerBuilder::named`] to register on start, and the free functions
|
//! [`GenServerBuilder::named`] to register on start, and the free functions
|
||||||
//! [`call`], [`cast`], and [`whereis_server`] to address it by name. Registered
|
//! [`call`], [`cast`], and [`whereis_server`] to address it by name.
|
||||||
//! servers are a natural fit for supervision; see `supervisor` for how to
|
//!
|
||||||
//! build a tree that restarts servers on failure.
|
//! ## Supervised servers
|
||||||
|
//!
|
||||||
|
//! The supervised shape is [`NamedGenServerBuilder::run`]: it runs the loop
|
||||||
|
//! **inline, as the current actor**, so the closure of a
|
||||||
|
//! [`ChildSpec`](crate::supervisor::ChildSpec) *is* the server — the
|
||||||
|
//! supervisor's shutdown arrives as [`GenServer::handle_shutdown`], a restart
|
||||||
|
//! runs the factory again and re-binds the name, and the rest of the program
|
||||||
|
//! addresses it by name (a held ref would go stale on restart anyway).
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! const COUNTER: GenServerName<Counter> = GenServerName::new("counter");
|
||||||
|
//! OneForOne::new().child(ChildSpec::new(Restart::Permanent, || {
|
||||||
|
//! GenServerBuilder::new(Counter::default()).named(COUNTER).run().unwrap();
|
||||||
|
//! }));
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Limitations
|
//! ## Limitations
|
||||||
//!
|
//!
|
||||||
@@ -178,11 +227,15 @@
|
|||||||
//! from any handler via [`Watcher::watch`]) because monitors are inherently
|
//! from any handler via [`Watcher::watch`]) because monitors are inherently
|
||||||
//! created at runtime. The idle window is set once, in `init`.
|
//! created at runtime. The idle window is set once, in `init`.
|
||||||
|
|
||||||
use crate::channel::{channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender};
|
use crate::channel::{
|
||||||
|
channel, select, select_timeout, Receiver, RecvTimeoutError, Selectable, Sender,
|
||||||
|
};
|
||||||
|
use crate::link::ExitSignal;
|
||||||
|
use crate::monitor::DownReason;
|
||||||
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
use crate::monitor::{demonitor, monitor, Down, Monitor};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
||||||
use crate::scheduler::{cancel_timer, request_stop, send_after_to};
|
use crate::scheduler::{cancel_timer, request_shutdown, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -252,10 +305,34 @@ pub trait GenServer: Send + 'static {
|
|||||||
/// Default: no-op.
|
/// Default: no-op.
|
||||||
fn handle_idle(&mut self) {}
|
fn handle_idle(&mut self) {}
|
||||||
|
|
||||||
|
/// A graceful shutdown request (a [`request_shutdown`](crate::request_shutdown)
|
||||||
|
/// reaching this server), delivered only if `init` called
|
||||||
|
/// [`GenServerCtx::trap_exit`]. Return [`ShutdownAction::Exit`] to stop
|
||||||
|
/// now (the default), or [`ShutdownAction::Continue`] to keep serving and
|
||||||
|
/// end the server later with a [`StopHandle`].
|
||||||
|
fn handle_shutdown(&mut self) -> ShutdownAction {
|
||||||
|
ShutdownAction::Exit
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A linked peer's abnormal death (an [`ExitSignal`] that is not a
|
||||||
|
/// shutdown request), delivered only if `init` called
|
||||||
|
/// [`GenServerCtx::trap_exit`]. Default: drop it.
|
||||||
|
fn handle_exit(&mut self, _sig: ExitSignal) {}
|
||||||
|
|
||||||
/// Runs as the server actor exits, on any exit path (see module docs).
|
/// Runs as the server actor exits, on any exit path (see module docs).
|
||||||
fn terminate(&mut self) {}
|
fn terminate(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a server does with a shutdown request; see [`GenServer::handle_shutdown`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ShutdownAction {
|
||||||
|
/// Break the loop now. `terminate` runs on the normal path and may block.
|
||||||
|
Exit,
|
||||||
|
/// Keep dispatching. The server is expected to end itself with a
|
||||||
|
/// [`StopHandle`] once it is done winding down.
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
/// What travels the server's single inbox channel: a synchronous call (with a
|
/// What travels the server's single inbox channel: a synchronous call (with a
|
||||||
/// reply sender) or an asynchronous cast. Private — callers use [`GenServerRef`].
|
/// reply sender) or an asynchronous cast. Private — callers use [`GenServerRef`].
|
||||||
enum Envelope<G: GenServer> {
|
enum Envelope<G: GenServer> {
|
||||||
@@ -263,9 +340,9 @@ enum Envelope<G: GenServer> {
|
|||||||
Cast(G::Cast),
|
Cast(G::Cast),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A clonable handle to a running server. Cloning yields another sender to the
|
/// A clonable handle to a running server: an *address*, not an owner. Cloning
|
||||||
/// same inbox; the server lives until the last `GenServerRef` is dropped, at which
|
/// yields another sender to the same inbox; dropping refs never ends the
|
||||||
/// point its inbox closes and the loop exits normally.
|
/// server (see the module docs, *When the server stops*).
|
||||||
pub struct GenServerRef<G: GenServer> {
|
pub struct GenServerRef<G: GenServer> {
|
||||||
tx: Sender<Envelope<G>>,
|
tx: Sender<Envelope<G>>,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
@@ -273,7 +350,10 @@ pub struct GenServerRef<G: GenServer> {
|
|||||||
|
|
||||||
impl<G: GenServer> Clone for GenServerRef<G> {
|
impl<G: GenServer> Clone for GenServerRef<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
GenServerRef { tx: self.tx.clone(), pid: self.pid }
|
GenServerRef {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
pid: self.pid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,18 +437,22 @@ impl<G: GenServer> GenServerRef<G> {
|
|||||||
|
|
||||||
/// Stop the server and block until it has fully exited.
|
/// Stop the server and block until it has fully exited.
|
||||||
///
|
///
|
||||||
/// Sends a cooperative stop signal to the server actor and waits for it to
|
/// Asks the server to shut down (a [`request_shutdown`](crate::request_shutdown))
|
||||||
/// exit, so [`GenServer::terminate`] has run by the time this returns.
|
/// and waits for it to exit, so [`GenServer::terminate`] has run by the
|
||||||
/// Returns immediately if the server is already gone.
|
/// time this returns. A trapping server gets to wind down via
|
||||||
|
/// [`GenServer::handle_shutdown`]; any other is stopped outright. Returns
|
||||||
|
/// immediately if the server is already gone. Waits as long as the server
|
||||||
|
/// takes — the caller, not the server, decides whether that is acceptable;
|
||||||
|
/// a supervisor uses its child's [`Shutdown`](crate::supervisor::Shutdown)
|
||||||
|
/// policy to bound it.
|
||||||
///
|
///
|
||||||
/// This is the right teardown for a server kept alive by a registered
|
/// This is the explicit close: dropping refs never ends a server. Like
|
||||||
/// [`GenServerName`], where dropping every external `GenServerRef` is not enough
|
/// all cooperative cancellation, it is best-effort:
|
||||||
/// to close the inbox. Like all cooperative cancellation, it is best-effort:
|
|
||||||
/// a server wedged in a tight loop with no observation point cannot be
|
/// a server wedged in a tight loop with no observation point cannot be
|
||||||
/// stopped this way. Panics if called outside `Runtime::run()`.
|
/// stopped this way. Panics if called outside `Runtime::run()`.
|
||||||
pub fn shutdown(&self) {
|
pub fn shutdown(&self) {
|
||||||
let mon = monitor(self.pid);
|
let mon = monitor(self.pid);
|
||||||
request_stop(self.pid);
|
request_shutdown(self.pid);
|
||||||
// The Down lands when the server finalizes; an already-dead target makes
|
// The Down lands when the server finalizes; an already-dead target makes
|
||||||
// `monitor` deliver NoProc immediately, so this never blocks forever.
|
// `monitor` deliver NoProc immediately, so this never blocks forever.
|
||||||
let _ = mon.rx.recv();
|
let _ = mon.rx.recv();
|
||||||
@@ -391,6 +475,8 @@ enum Sys<G: GenServer> {
|
|||||||
/// the payload factory, dispatches it to [`GenServer::handle_timer`], and
|
/// the payload factory, dispatches it to [`GenServer::handle_timer`], and
|
||||||
/// re-arms the next tick before returning.
|
/// re-arms the next tick before returning.
|
||||||
Tick(crate::timer::TimerId),
|
Tick(crate::timer::TimerId),
|
||||||
|
/// The state asked to end the server (via [`StopHandle::stop`]).
|
||||||
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server loop's runtime hook, passed to [`GenServer::init`]. Hands out the
|
/// The server loop's runtime hook, passed to [`GenServer::init`]. Hands out the
|
||||||
@@ -406,13 +492,35 @@ pub struct GenServerCtx<G: GenServer> {
|
|||||||
/// because `init` holds only `&ctx`; not `Send`, but `GenServerCtx` is only ever
|
/// because `init` holds only `&ctx`; not `Send`, but `GenServerCtx` is only ever
|
||||||
/// borrowed on the actor's own stack during `init`, never sent.
|
/// borrowed on the actor's own stack during `init`, never sent.
|
||||||
idle: Cell<Option<Duration>>,
|
idle: Cell<Option<Duration>>,
|
||||||
|
/// Whether the loop should trap exits (set via [`trap_exit`](Self::trap_exit)
|
||||||
|
/// during `init`, read by the loop after).
|
||||||
|
trap: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: GenServer> GenServerCtx<G> {
|
impl<G: GenServer> GenServerCtx<G> {
|
||||||
|
/// Trap exits for the server's lifetime: shutdown requests then arrive as
|
||||||
|
/// [`GenServer::handle_shutdown`] and linked-peer deaths as
|
||||||
|
/// [`GenServer::handle_exit`], instead of stopping the server outright.
|
||||||
|
/// Call this once during `init`.
|
||||||
|
pub fn trap_exit(&self) {
|
||||||
|
self.trap.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clonable handle that lets the state end the server from any handler
|
||||||
|
/// (a normal exit; see the module docs). Store it on the state during
|
||||||
|
/// `init`.
|
||||||
|
pub fn stop_handle(&self) -> StopHandle<G> {
|
||||||
|
StopHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A clonable handle to the loop's monitor intake. Store it in the state
|
/// A clonable handle to the loop's monitor intake. Store it in the state
|
||||||
/// during `init` to watch monitors from later handlers.
|
/// during `init` to watch monitors from later handlers.
|
||||||
pub fn watcher(&self) -> Watcher<G> {
|
pub fn watcher(&self) -> Watcher<G> {
|
||||||
Watcher { tx: self.sys_tx.clone() }
|
Watcher {
|
||||||
|
tx: self.sys_tx.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
||||||
@@ -426,7 +534,10 @@ impl<G: GenServer> GenServerCtx<G> {
|
|||||||
/// [`tick_every`](TimerHandle::tick_every) /
|
/// [`tick_every`](TimerHandle::tick_every) /
|
||||||
/// [`cancel`](TimerHandle::cancel) from any later handler.
|
/// [`cancel`](TimerHandle::cancel) from any later handler.
|
||||||
pub fn timer(&self) -> TimerHandle<G> {
|
pub fn timer(&self) -> TimerHandle<G> {
|
||||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
TimerHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
reg: self.reg.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a quiet-period window: if the loop goes `after` without dispatching
|
/// Set a quiet-period window: if the loop goes `after` without dispatching
|
||||||
@@ -442,6 +553,30 @@ impl<G: GenServer> GenServerCtx<G> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lets a server's state end the server, cloned from
|
||||||
|
/// [`GenServerCtx::stop_handle`] during `init`. [`stop`](Self::stop) makes the
|
||||||
|
/// loop break after the current message and exit normally; `terminate` runs on
|
||||||
|
/// the normal path.
|
||||||
|
pub struct StopHandle<G: GenServer> {
|
||||||
|
sys_tx: Sender<Sys<G>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G: GenServer> Clone for StopHandle<G> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
StopHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G: GenServer> StopHandle<G> {
|
||||||
|
/// End the server after the current message. Idempotent; a no-op once the
|
||||||
|
/// server is gone.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
let _ = self.sys_tx.send(Sys::Stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-server timer bookkeeping, shared between the loop and every
|
/// Per-server timer bookkeeping, shared between the loop and every
|
||||||
/// [`TimerHandle`] clone. A gen_server actor is single-threaded — handlers
|
/// [`TimerHandle`] clone. A gen_server actor is single-threaded — handlers
|
||||||
/// and the loop never run concurrently — so this `Mutex` is always
|
/// and the loop never run concurrently — so this `Mutex` is always
|
||||||
@@ -518,7 +653,10 @@ pub struct TimerHandle<G: GenServer> {
|
|||||||
// Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
|
// Manual Clone for the same reason as `Watcher`: no `G: Clone` needed.
|
||||||
impl<G: GenServer> Clone for TimerHandle<G> {
|
impl<G: GenServer> Clone for TimerHandle<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
TimerHandle { sys_tx: self.sys_tx.clone(), reg: self.reg.clone() }
|
TimerHandle {
|
||||||
|
sys_tx: self.sys_tx.clone(),
|
||||||
|
reg: self.reg.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,8 +707,18 @@ impl<G: GenServer> TimerHandle<G> {
|
|||||||
// First instance fires after `every`; the payload is produced loop-side
|
// First instance fires after `every`; the payload is produced loop-side
|
||||||
// from `make` on fire, so the tick carries only the stable id.
|
// from `make` on fire, so the tick carries only the stable id.
|
||||||
let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
|
let sub = send_after_to(every, self.sys_tx.clone(), Sys::Tick(local));
|
||||||
reg.periodics.insert(local, Periodic { every, live: sub, make });
|
reg.periodics.insert(
|
||||||
debug_assert!(reg.rearm_tx.is_some(), "rearm_tx must be Some while periodics is non-empty");
|
local,
|
||||||
|
Periodic {
|
||||||
|
every,
|
||||||
|
live: sub,
|
||||||
|
make,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
reg.rearm_tx.is_some(),
|
||||||
|
"rearm_tx must be Some while periodics is non-empty"
|
||||||
|
);
|
||||||
local
|
local
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +765,9 @@ pub struct Watcher<G: GenServer> {
|
|||||||
// regardless of the server type (it clones only the inner sender).
|
// regardless of the server type (it clones only the inner sender).
|
||||||
impl<G: GenServer> Clone for Watcher<G> {
|
impl<G: GenServer> Clone for Watcher<G> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Watcher { tx: self.tx.clone() }
|
Watcher {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,9 +829,10 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the server actor and hand back its [`GenServerRef`]. The server's
|
/// Spawn the server actor and hand back its [`GenServerRef`] (an address;
|
||||||
/// lifetime is governed by its refs, not by joining, so the backing join
|
/// the server's lifetime is its own, see the module docs). The backing
|
||||||
/// handle is dropped.
|
/// join handle is dropped. For a supervised server use
|
||||||
|
/// [`named`](Self::named) + [`NamedGenServerBuilder::run`] instead.
|
||||||
pub fn start(self) -> GenServerRef<G> {
|
pub fn start(self) -> GenServerRef<G> {
|
||||||
self.spawn_server()
|
self.spawn_server()
|
||||||
}
|
}
|
||||||
@@ -691,7 +842,10 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
/// live server). Consumes the builder, carrying its `with_info` / `under`
|
||||||
/// configuration through.
|
/// configuration through.
|
||||||
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
pub fn named(self, name: GenServerName<G>) -> NamedGenServerBuilder<G> {
|
||||||
NamedGenServerBuilder { builder: self, name: name.as_str() }
|
NamedGenServerBuilder {
|
||||||
|
builder: self,
|
||||||
|
name: name.as_str(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Private shared body behind [`start`](Self::start) and
|
/// Private shared body behind [`start`](Self::start) and
|
||||||
@@ -700,18 +854,25 @@ impl<G: GenServer> GenServerBuilder<G> {
|
|||||||
/// under the name before returning.
|
/// under the name before returning.
|
||||||
fn spawn_server(self) -> GenServerRef<G> {
|
fn spawn_server(self) -> GenServerRef<G> {
|
||||||
let (tx, rx) = channel::<Envelope<G>>();
|
let (tx, rx) = channel::<Envelope<G>>();
|
||||||
let GenServerBuilder { state, infos, supervisor, stack_opts } = self;
|
let GenServerBuilder {
|
||||||
|
state,
|
||||||
|
infos,
|
||||||
|
supervisor,
|
||||||
|
stack_opts,
|
||||||
|
} = self;
|
||||||
|
let keep = tx.clone();
|
||||||
let handle = match supervisor {
|
let handle = match supervisor {
|
||||||
Some(sup) => {
|
Some(sup) => crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
||||||
crate::scheduler::spawn_under_with(sup, stack_opts, move || {
|
server_loop::<G>(keep, rx, state, infos)
|
||||||
server_loop::<G>(rx, state, infos)
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
None => crate::scheduler::spawn_with(stack_opts, move || {
|
None => crate::scheduler::spawn_with(stack_opts, move || {
|
||||||
server_loop::<G>(rx, state, infos)
|
server_loop::<G>(keep, rx, state, infos)
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
GenServerRef { tx, pid: handle.pid() }
|
GenServerRef {
|
||||||
|
tx,
|
||||||
|
pid: handle.pid(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,7 +900,10 @@ impl<G> GenServerName<G> {
|
|||||||
/// associated constants at call sites.
|
/// associated constants at call sites.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(name: &'static str) -> Self {
|
pub const fn new(name: &'static str) -> Self {
|
||||||
Self { name, _marker: PhantomData }
|
Self {
|
||||||
|
name,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The underlying registry key.
|
/// The underlying registry key.
|
||||||
@@ -791,19 +955,38 @@ impl<G: GenServer> NamedGenServerBuilder<G> {
|
|||||||
/// The inbox sender is published under the name **from the parent side,
|
/// The inbox sender is published under the name **from the parent side,
|
||||||
/// before this returns**, so a by-name `call` / `cast` resolves the instant
|
/// before this returns**, so a by-name `call` / `cast` resolves the instant
|
||||||
/// `start()` returns — no race with the server body. On a name clash the
|
/// `start()` returns — no race with the server body. On a name clash the
|
||||||
/// just-spawned server is wound down (its only ref is dropped, closing the
|
/// just-spawned server is stopped, so a failed bind leaks no actor.
|
||||||
/// inbox), so a failed bind leaks no actor.
|
|
||||||
pub fn start(self) -> Result<GenServerRef<G>, RegisterError> {
|
pub fn start(self) -> Result<GenServerRef<G>, RegisterError> {
|
||||||
let NamedGenServerBuilder { builder, name } = self;
|
let NamedGenServerBuilder { builder, name } = self;
|
||||||
let server = builder.spawn_server();
|
let server = builder.spawn_server();
|
||||||
match register_with::<Envelope<G>>(server.pid, name, server.tx.clone()) {
|
match register_with::<Envelope<G>>(server.pid, name, server.tx.clone()) {
|
||||||
Ok(()) => Ok(server),
|
Ok(()) => Ok(server),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
drop(server); // inbox closes → loop exits gracefully
|
crate::scheduler::request_stop(server.pid); // never ran init
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the server **inline, as the current actor**, bound to its name.
|
||||||
|
/// This is the supervised shape: the closure of a
|
||||||
|
/// [`ChildSpec`](crate::supervisor::ChildSpec) *is* the server, so the
|
||||||
|
/// supervisor's shutdown reaches it as [`GenServer::handle_shutdown`], a
|
||||||
|
/// restart runs the factory again and re-binds the name, and clients
|
||||||
|
/// address it by name ([`call`], [`cast`], [`whereis_server`]). Returns
|
||||||
|
/// when the server exits; [`RegisterError::NameTaken`] (before `init`) if
|
||||||
|
/// the name is held by a different live actor.
|
||||||
|
///
|
||||||
|
/// `under` / `stack_opts` are spawn options and do not apply here — the
|
||||||
|
/// actor already exists.
|
||||||
|
pub fn run(self) -> Result<(), RegisterError> {
|
||||||
|
let NamedGenServerBuilder { builder, name } = self;
|
||||||
|
let GenServerBuilder { state, infos, .. } = builder;
|
||||||
|
let (tx, rx) = channel::<Envelope<G>>();
|
||||||
|
register_with::<Envelope<G>>(crate::scheduler::self_pid(), name, tx.clone())?;
|
||||||
|
server_loop::<G>(tx, rx, state, infos);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a [`GenServerName`] to a [`GenServerRef`] when you want a handle to hold or
|
/// Resolve a [`GenServerName`] to a [`GenServerRef`] when you want a handle to hold or
|
||||||
@@ -862,10 +1045,17 @@ pub fn start_under<G: GenServer>(supervisor: Pid, state: G) -> GenServerRef<G> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn server_loop<G: GenServer>(
|
fn server_loop<G: GenServer>(
|
||||||
|
keep: Sender<Envelope<G>>,
|
||||||
rx: Receiver<Envelope<G>>,
|
rx: Receiver<Envelope<G>>,
|
||||||
state: G,
|
state: G,
|
||||||
mut infos: Vec<Receiver<G::Info>>,
|
mut infos: Vec<Receiver<G::Info>>,
|
||||||
) {
|
) {
|
||||||
|
// The loop holds one inbox sender for its whole life: the inbox never
|
||||||
|
// closes, so refs are addresses and the server's lifetime is the actor's
|
||||||
|
// (stop handle, shutdown, stop, panic). The `Disconnected` arms below are
|
||||||
|
// defensive only.
|
||||||
|
let _keep = keep;
|
||||||
|
|
||||||
// Drop guard — owns the server state and the timer registry.
|
// Drop guard — owns the server state and the timer registry.
|
||||||
//
|
//
|
||||||
// Why a guard rather than code after the loop:
|
// Why a guard rather than code after the loop:
|
||||||
@@ -933,9 +1123,18 @@ fn server_loop<G: GenServer>(
|
|||||||
// Bind the ctx so the idle window set during init can be read back, then
|
// Bind the ctx so the idle window set during init can be read back, then
|
||||||
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
// drop it — that drops the loop's own Sys sender, so a state that cloned no
|
||||||
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
// Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour).
|
||||||
let ctx = GenServerCtx { sys_tx, reg: reg.clone(), idle: Cell::new(None) };
|
let ctx = GenServerCtx {
|
||||||
|
sys_tx,
|
||||||
|
reg: reg.clone(),
|
||||||
|
idle: Cell::new(None),
|
||||||
|
trap: Cell::new(false),
|
||||||
|
};
|
||||||
guard.0.init(&ctx);
|
guard.0.init(&ctx);
|
||||||
let idle = ctx.idle.get();
|
let idle = ctx.idle.get();
|
||||||
|
// Trapping is opted into during init and fixed for the loop's life. The
|
||||||
|
// inbox is armed only when set: an untrapped server keeps the fast path,
|
||||||
|
// and a shutdown request simply stops it as `request_stop` would.
|
||||||
|
let exits: Option<Receiver<ExitSignal>> = ctx.trap.get().then(crate::link::trap_exit);
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
|
|
||||||
let mut monitors: Vec<Monitor> = Vec::new();
|
let mut monitors: Vec<Monitor> = Vec::new();
|
||||||
@@ -951,7 +1150,7 @@ fn server_loop<G: GenServer>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if monitors.is_empty() && !sys_open && infos.is_empty() {
|
if exits.is_none() && monitors.is_empty() && !sys_open && infos.is_empty() {
|
||||||
// Fast path: no extra arms, no select overhead — park directly on
|
// Fast path: no extra arms, no select overhead — park directly on
|
||||||
// the inbox. Mirrors the inbox arm of the select path below; any
|
// the inbox. Mirrors the inbox arm of the select path below; any
|
||||||
// change there must be applied here too.
|
// change there must be applied here too.
|
||||||
@@ -968,7 +1167,7 @@ fn server_loop<G: GenServer>(
|
|||||||
guard.0.handle_idle();
|
guard.0.handle_idle();
|
||||||
reset_idle(&mut idle_deadline);
|
reset_idle(&mut idle_deadline);
|
||||||
}
|
}
|
||||||
// All ServerRefs dropped → inbox closed → shutdown.
|
// Defensive: the loop holds a sender, so unreachable.
|
||||||
Err(RecvTimeoutError::Disconnected) => break,
|
Err(RecvTimeoutError::Disconnected) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -979,17 +1178,21 @@ fn server_loop<G: GenServer>(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Slow path: one or more extra arms live — build the arm slice and
|
// Slow path: one or more extra arms live — build the arm slice and
|
||||||
// select. Arm order encodes priority: downs → system → infos →
|
// select. Arm order encodes priority: exits → downs → system →
|
||||||
// inbox. The slice is rebuilt each iteration because the monitor
|
// infos → inbox (a shutdown request is noticed under any load).
|
||||||
// and info sets shrink/grow. Mirrors the fast-path inbox park
|
// The slice is rebuilt each iteration because the monitor and info
|
||||||
// above; keep them in sync.
|
// sets shrink/grow. Mirrors the fast-path inbox park above; keep
|
||||||
let nd = monitors.len(); // monitor band: [0, nd)
|
// them in sync.
|
||||||
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
let ne = exits.is_some() as usize; // exit arm: [0, ne)
|
||||||
// info band: [nd+nw, nd+nw+ni)
|
let nd = ne + monitors.len(); // monitor band: [ne, nd)
|
||||||
// inbox arm: [nd+nw+ni]
|
let nw = sys_open as usize; // system arm: [nd, nd+nw)
|
||||||
|
// info band: [nd+nw, nd+nw+ni)
|
||||||
|
// inbox arm: [nd+nw+ni]
|
||||||
let sel = {
|
let sel = {
|
||||||
let mut arms: Vec<&dyn Selectable> =
|
let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(nd + nw + infos.len() + 1);
|
||||||
Vec::with_capacity(nd + nw + infos.len() + 1);
|
if let Some(e) = &exits {
|
||||||
|
arms.push(e);
|
||||||
|
}
|
||||||
for m in &monitors {
|
for m in &monitors {
|
||||||
arms.push(&m.rx);
|
arms.push(&m.rx);
|
||||||
}
|
}
|
||||||
@@ -1001,9 +1204,7 @@ fn server_loop<G: GenServer>(
|
|||||||
}
|
}
|
||||||
arms.push(&rx);
|
arms.push(&rx);
|
||||||
match idle_deadline {
|
match idle_deadline {
|
||||||
Some(dl) => {
|
Some(dl) => select_timeout(&arms, dl.saturating_duration_since(Instant::now())),
|
||||||
select_timeout(&arms, dl.saturating_duration_since(Instant::now()))
|
|
||||||
}
|
|
||||||
None => Some(select(&arms)),
|
None => Some(select(&arms)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1017,10 +1218,25 @@ fn server_loop<G: GenServer>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if i < nd {
|
if i < ne {
|
||||||
|
// Exit arm: a shutdown request or a linked peer's death.
|
||||||
|
// The inbox lives for the loop's life, so it never closes.
|
||||||
|
let sig = exits.as_ref().and_then(|e| e.try_recv().ok().flatten());
|
||||||
|
if let Some(sig) = sig {
|
||||||
|
if sig.reason == DownReason::Shutdown {
|
||||||
|
match guard.0.handle_shutdown() {
|
||||||
|
ShutdownAction::Exit => break,
|
||||||
|
ShutdownAction::Continue => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guard.0.handle_exit(sig);
|
||||||
|
}
|
||||||
|
reset_idle(&mut idle_deadline);
|
||||||
|
}
|
||||||
|
} else if i < nd {
|
||||||
// Monitor band: a Down retires its arm either way (one-shot)
|
// Monitor band: a Down retires its arm either way (one-shot)
|
||||||
// or closes without delivering (defensive; shouldn't happen).
|
// or closes without delivering (defensive; shouldn't happen).
|
||||||
let m = monitors.remove(i);
|
let m = monitors.remove(i - ne);
|
||||||
if let Ok(Some(down)) = m.rx.try_recv() {
|
if let Ok(Some(down)) = m.rx.try_recv() {
|
||||||
guard.0.handle_down(down);
|
guard.0.handle_down(down);
|
||||||
reset_idle(&mut idle_deadline);
|
reset_idle(&mut idle_deadline);
|
||||||
@@ -1029,13 +1245,19 @@ fn server_loop<G: GenServer>(
|
|||||||
match sys_rx.try_recv() {
|
match sys_rx.try_recv() {
|
||||||
// Control intake, not a dispatched message: no idle reset.
|
// Control intake, not a dispatched message: no idle reset.
|
||||||
Ok(Some(Sys::Watch(m))) => monitors.push(m),
|
Ok(Some(Sys::Watch(m))) => monitors.push(m),
|
||||||
|
// The state ended the server: a normal exit.
|
||||||
|
Ok(Some(Sys::Stop)) => break,
|
||||||
Ok(Some(Sys::Timer(id, msg))) => {
|
Ok(Some(Sys::Timer(id, msg))) => {
|
||||||
// The one-shot fired: retire its registry entry so the
|
// The one-shot fired: retire its registry entry so the
|
||||||
// live set tracks only still-pending timers, then
|
// live set tracks only still-pending timers, then
|
||||||
// dispatch.
|
// dispatch.
|
||||||
match reg.lock() {
|
match reg.lock() {
|
||||||
Ok(mut g) => { g.oneshots.remove(&id); }
|
Ok(mut g) => {
|
||||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
g.oneshots.remove(&id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard.0.handle_timer(msg);
|
guard.0.handle_timer(msg);
|
||||||
reset_idle(&mut idle_deadline);
|
reset_idle(&mut idle_deadline);
|
||||||
@@ -1049,7 +1271,9 @@ fn server_loop<G: GenServer>(
|
|||||||
let msg = {
|
let msg = {
|
||||||
let mut g = match reg.lock() {
|
let mut g = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_server reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_server reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let r = &mut *g;
|
let r = &mut *g;
|
||||||
if let Some(p) = r.periodics.get_mut(&id) {
|
if let Some(p) = r.periodics.get_mut(&id) {
|
||||||
@@ -1057,9 +1281,9 @@ fn server_loop<G: GenServer>(
|
|||||||
let msg = (p.make)();
|
let msg = (p.make)();
|
||||||
let tx = match r.rearm_tx.as_ref() {
|
let tx = match r.rearm_tx.as_ref() {
|
||||||
Some(tx) => tx.clone(),
|
Some(tx) => tx.clone(),
|
||||||
None => panic!(
|
None => {
|
||||||
"smarm: live periodic without rearm_tx (logic bug)"
|
panic!("smarm: live periodic without rearm_tx (logic bug)")
|
||||||
),
|
}
|
||||||
};
|
};
|
||||||
p.live = send_after_to(every, tx, Sys::Tick(id));
|
p.live = send_after_to(every, tx, Sys::Tick(id));
|
||||||
Some(msg)
|
Some(msg)
|
||||||
|
|||||||
+494
-76
@@ -68,10 +68,38 @@
|
|||||||
//! inbox or timer event; a replayed event may postpone again (it re-queues for
|
//! inbox or timer event; a replayed event may postpone again (it re-queues for
|
||||||
//! the next transition). See the macro docs for the row surface and [`Step`]
|
//! the next transition). See the macro docs for the row surface and [`Step`]
|
||||||
//! for how a postpone surfaces to the loop.
|
//! for how a postpone surfaces to the loop.
|
||||||
|
//!
|
||||||
|
//! ## Stopping, shutdown, and exits
|
||||||
|
//!
|
||||||
|
//! A machine lives until it stops, is shut down, or is killed; a
|
||||||
|
//! [`GenStatemRef`] is an address, and dropping refs never ends it (the
|
||||||
|
//! gen_server rule — see its *When the server stops*). The supervised shape
|
||||||
|
//! is [`run_named`], which runs the machine inline as the current actor so it
|
||||||
|
//! is a direct `ChildSpec` child addressed by [`GenStatemName`]. A machine
|
||||||
|
//! can end itself: any row
|
||||||
|
//! body may call [`cx.stop()`](Cx::stop) (or use the `stop` tail keyword,
|
||||||
|
//! sugar for `{ cx.stop(); prev }`) — the loop breaks after that event and the
|
||||||
|
//! actor exits *normally* (OTP's `{stop, normal}`; a `Transient` child is not
|
||||||
|
//! restarted). [`terminate`](Machine::terminate) — the optional `terminate
|
||||||
|
//! { … }` macro block — runs on every exit path.
|
||||||
|
//!
|
||||||
|
//! From outside, [`GenStatemRef::shutdown`] (or a plain
|
||||||
|
//! [`request_shutdown`](crate::request_shutdown), which is what a supervisor
|
||||||
|
//! sends) asks the machine to stop. By default a machine does not trap exits
|
||||||
|
//! and the request stops it outright. A machine that calls
|
||||||
|
//! [`cx.trap_exit()`](Cx::trap_exit) in its initial `enter` instead receives
|
||||||
|
//! it as the **`shutdown` event**, routed by state like any other — a
|
||||||
|
//! `Connected` state may transition into `Draining` and stop later from a
|
||||||
|
//! timeout row, while a state with no `shutdown` row takes the macro's
|
||||||
|
//! default, `stop`. Linked-peer deaths reach a trapping machine as
|
||||||
|
//! `exit <pat>` events; an unmatched one is dropped like an unmatched info.
|
||||||
|
|
||||||
use crate::channel::{channel, select, Receiver, Sender};
|
use crate::channel::{channel, select, Receiver, Selectable, Sender};
|
||||||
|
use crate::link::ExitSignal;
|
||||||
|
use crate::monitor::{monitor, DownReason};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
use crate::scheduler::{cancel_timer, send_after_to};
|
use crate::registry::{register_with, resolve_named_sender, RegisterError};
|
||||||
|
use crate::scheduler::{cancel_timer, request_shutdown, send_after_to};
|
||||||
use crate::timer::TimerId;
|
use crate::timer::TimerId;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -119,6 +147,33 @@ pub trait Machine: Send + 'static {
|
|||||||
/// state's `enter`, and returns [`Step::Transitioned`]; a stay or unmatched
|
/// state's `enter`, and returns [`Step::Transitioned`]; a stay or unmatched
|
||||||
/// event returns [`Step::Stayed`].
|
/// event returns [`Step::Stayed`].
|
||||||
fn handle(&mut self, ev: Self::Ev, cx: &mut Cx<Self::Ev>) -> Step<Self::Ev>;
|
fn handle(&mut self, ev: Self::Ev, cx: &mut Cx<Self::Ev>) -> Step<Self::Ev>;
|
||||||
|
|
||||||
|
/// Wrap a graceful shutdown request into this machine's event, so a
|
||||||
|
/// trapping machine (see [`Cx::trap_exit`]) can route it **by state**. The
|
||||||
|
/// macro generates it as `Ev::Shutdown` and matches it in `shutdown` rows;
|
||||||
|
/// its default for a state that writes no such row is `stop`. A
|
||||||
|
/// hand-written machine that returns `None` (the default) is simply
|
||||||
|
/// stopped — the loop breaks and `terminate` runs on the normal path.
|
||||||
|
fn shutdown_ev() -> Option<Self::Ev> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap a linked peer's death (an [`ExitSignal`] that is not a shutdown
|
||||||
|
/// request, delivered only when trapping) into this machine's event. The
|
||||||
|
/// macro generates it as `Ev::Exit(sig)` and matches it in `exit <pat>`
|
||||||
|
/// rows; an unmatched exit is silently dropped, like an unmatched info.
|
||||||
|
/// A hand-written machine that returns `None` (the default) drops it.
|
||||||
|
fn exit_ev(_sig: ExitSignal) -> Option<Self::Ev> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs as the machine actor exits, on any exit path (a `stop`, a
|
||||||
|
/// graceful shutdown, a handler panic, a hard stop). Like
|
||||||
|
/// `gen_server::terminate`: on the panic and hard-stop paths it runs
|
||||||
|
/// mid-unwind — do not panic or park there; only on the normal path
|
||||||
|
/// (`stop`, shutdown rows) may it do real work. The macro's optional
|
||||||
|
/// `terminate { … }` block generates it.
|
||||||
|
fn terminate(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -219,7 +274,11 @@ struct Timers {
|
|||||||
|
|
||||||
impl Timers {
|
impl Timers {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Timers { next_local: 0, state: None, named: HashMap::new() }
|
Timers {
|
||||||
|
next_local: 0,
|
||||||
|
state: None,
|
||||||
|
named: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mint(&mut self) -> u64 {
|
fn mint(&mut self) -> u64 {
|
||||||
@@ -243,12 +302,41 @@ impl Timers {
|
|||||||
pub struct Cx<Ev> {
|
pub struct Cx<Ev> {
|
||||||
sys_tx: Sender<Sys>,
|
sys_tx: Sender<Sys>,
|
||||||
reg: Arc<Mutex<Timers>>,
|
reg: Arc<Mutex<Timers>>,
|
||||||
|
/// Set by [`trap_exit`](Self::trap_exit) during `on_start`; read once by
|
||||||
|
/// the loop right after, fixed for the machine's life.
|
||||||
|
trap: bool,
|
||||||
|
/// Set by [`stop`](Self::stop); the loop breaks after the current event.
|
||||||
|
stop: bool,
|
||||||
_ev: PhantomData<fn() -> Ev>,
|
_ev: PhantomData<fn() -> Ev>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ev> Cx<Ev> {
|
impl<Ev> Cx<Ev> {
|
||||||
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self {
|
fn new(sys_tx: Sender<Sys>, reg: Arc<Mutex<Timers>>) -> Self {
|
||||||
Cx { sys_tx, reg, _ev: PhantomData }
|
Cx {
|
||||||
|
sys_tx,
|
||||||
|
reg,
|
||||||
|
trap: false,
|
||||||
|
stop: false,
|
||||||
|
_ev: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trap exits for the machine's lifetime: a shutdown request then arrives
|
||||||
|
/// as the `shutdown` event (routed by state) and linked-peer deaths as
|
||||||
|
/// `exit` events, instead of stopping the machine outright. Call it in the
|
||||||
|
/// initial state's `enter` (i.e. during `on_start`); later calls have no
|
||||||
|
/// effect.
|
||||||
|
pub fn trap_exit(&mut self) {
|
||||||
|
self.trap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End the machine after the current event: the loop breaks and the actor
|
||||||
|
/// exits *normally* (OTP's `{stop, normal}`); `terminate` runs on the
|
||||||
|
/// normal path. Anything still queued or postponed is dropped. The `stop`
|
||||||
|
/// tail keyword in a macro row is sugar for `{ cx.stop(); prev }`.
|
||||||
|
/// Mirrors gen_server's [`StopHandle`](crate::gen_server::StopHandle).
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.stop = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arm the **state timeout**: fire a `state_timeout` event after `after` in
|
/// Arm the **state timeout**: fire a `state_timeout` event after `after` in
|
||||||
@@ -378,8 +466,7 @@ pub enum SendError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A clonable handle to a running machine. Cloning yields another sender to the
|
/// A clonable handle to a running machine. Cloning yields another sender to the
|
||||||
/// same inbox; the machine lives until the last `GenStatemRef` is dropped, at which
|
/// same inbox. An address, not an owner: dropping refs never ends the machine.
|
||||||
/// point its inbox closes and the loop exits.
|
|
||||||
pub struct GenStatemRef<M: Machine> {
|
pub struct GenStatemRef<M: Machine> {
|
||||||
tx: Sender<M::Ev>,
|
tx: Sender<M::Ev>,
|
||||||
pid: Pid,
|
pid: Pid,
|
||||||
@@ -387,7 +474,10 @@ pub struct GenStatemRef<M: Machine> {
|
|||||||
|
|
||||||
impl<M: Machine> Clone for GenStatemRef<M> {
|
impl<M: Machine> Clone for GenStatemRef<M> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
GenStatemRef { tx: self.tx.clone(), pid: self.pid }
|
GenStatemRef {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
pid: self.pid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +512,19 @@ impl<M: Machine> GenStatemRef<M> {
|
|||||||
self.send(ev).map_err(|_| CallError::Down)?;
|
self.send(ev).map_err(|_| CallError::Down)?;
|
||||||
rx.recv().map_err(|_| CallError::Down)
|
rx.recv().map_err(|_| CallError::Down)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask the machine to shut down and block until it has fully exited, so
|
||||||
|
/// [`Machine::terminate`] has run by the time this returns. A trapping
|
||||||
|
/// machine winds down through its `shutdown` rows; any other is stopped
|
||||||
|
/// outright. Returns immediately if the machine is already gone. Waits as
|
||||||
|
/// long as the machine takes — a supervisor bounds that with its child's
|
||||||
|
/// [`Shutdown`](crate::supervisor::Shutdown) policy. Mirrors
|
||||||
|
/// [`GenServerRef::shutdown`](crate::gen_server::GenServerRef::shutdown).
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
let mon = monitor(self.pid);
|
||||||
|
request_shutdown(self.pid);
|
||||||
|
let _ = mon.rx.recv();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -430,7 +533,8 @@ impl<M: Machine> GenStatemRef<M> {
|
|||||||
|
|
||||||
/// Spawn `machine` as an actor and hand back its [`GenStatemRef`]. Shape mirrors
|
/// Spawn `machine` as an actor and hand back its [`GenStatemRef`]. Shape mirrors
|
||||||
/// `gen_server::start`: make the inbox, spawn the loop, return the ref; the
|
/// `gen_server::start`: make the inbox, spawn the loop, return the ref; the
|
||||||
/// backing join handle is dropped (lifetime is governed by refs, not joining).
|
/// backing join handle is dropped (the machine's lifetime is its own). For a
|
||||||
|
/// supervised machine use [`run_named`].
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
||||||
@@ -443,42 +547,190 @@ pub fn spawn<M: Machine>(machine: M) -> GenStatemRef<M> {
|
|||||||
/// a `_with` variant like the scheduler's own spawns.
|
/// a `_with` variant like the scheduler's own spawns.
|
||||||
///
|
///
|
||||||
/// Panics if called outside `Runtime::run()`.
|
/// Panics if called outside `Runtime::run()`.
|
||||||
pub fn spawn_with<M: Machine>(
|
pub fn spawn_with<M: Machine>(opts: crate::scheduler::SpawnOpts, machine: M) -> GenStatemRef<M> {
|
||||||
opts: crate::scheduler::SpawnOpts,
|
|
||||||
machine: M,
|
|
||||||
) -> GenStatemRef<M> {
|
|
||||||
let (tx, rx) = channel::<M::Ev>();
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(rx, machine));
|
let keep = tx.clone();
|
||||||
GenStatemRef { tx, pid: handle.pid() }
|
let handle = crate::scheduler::spawn_with(opts, move || statem_loop(keep, rx, machine));
|
||||||
|
GenStatemRef {
|
||||||
|
tx,
|
||||||
|
pid: handle.pid(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A typed, static name for a gen_statem, used to address a machine through
|
||||||
|
/// the registry without holding a [`GenStatemRef`]. Mirrors
|
||||||
|
/// [`GenServerName`](crate::gen_server::GenServerName): declare it as a
|
||||||
|
/// constant and bind it with [`run_named`].
|
||||||
|
pub struct GenStatemName<M> {
|
||||||
|
name: &'static str,
|
||||||
|
_marker: PhantomData<fn() -> M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> GenStatemName<M> {
|
||||||
|
/// Bind a static string as a machine name.
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The underlying registry key.
|
||||||
|
#[inline]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> Copy for GenStatemName<M> {}
|
||||||
|
impl<M> Clone for GenStatemName<M> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `machine` **inline, as the current actor**, bound to `name`. The
|
||||||
|
/// supervised shape: the closure of a
|
||||||
|
/// [`ChildSpec`](crate::supervisor::ChildSpec) *is* the machine, so the
|
||||||
|
/// supervisor's shutdown reaches it as a `shutdown` row, a restart runs the
|
||||||
|
/// factory again and re-binds the name, and clients address it by name
|
||||||
|
/// ([`send`], [`call`], [`whereis_machine`]). Returns when the machine exits;
|
||||||
|
/// [`RegisterError::NameTaken`] (before `on_start`) if the name is held by a
|
||||||
|
/// different live actor. Mirrors
|
||||||
|
/// [`NamedGenServerBuilder::run`](crate::gen_server::NamedGenServerBuilder::run).
|
||||||
|
pub fn run_named<M: Machine>(name: GenStatemName<M>, machine: M) -> Result<(), RegisterError> {
|
||||||
|
let (tx, rx) = channel::<M::Ev>();
|
||||||
|
register_with::<M::Ev>(crate::scheduler::self_pid(), name.as_str(), tx.clone())?;
|
||||||
|
statem_loop(tx, rx, machine);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a [`GenStatemName`] to a [`GenStatemRef`]; `None` if no live
|
||||||
|
/// machine holds the name. Panics if called outside `Runtime::run()`.
|
||||||
|
pub fn whereis_machine<M: Machine>(name: GenStatemName<M>) -> Option<GenStatemRef<M>> {
|
||||||
|
resolve_named_sender::<M::Ev>(name.as_str()).map(|(pid, tx)| GenStatemRef { tx, pid })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push an event to the machine registered under `name`, resolving per send.
|
||||||
|
/// [`SendError::Down`] if no live machine holds the name.
|
||||||
|
pub fn send<M: Machine>(name: GenStatemName<M>, ev: M::Ev) -> Result<(), SendError> {
|
||||||
|
match whereis_machine(name) {
|
||||||
|
Some(m) => m.send(ev),
|
||||||
|
None => Err(SendError::Down),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronous request-reply to the machine registered under `name`,
|
||||||
|
/// resolving per call (a machine restarted under the same name is reached
|
||||||
|
/// transparently). [`CallError::Down`] if no live machine holds the name.
|
||||||
|
pub fn call<M, T, F>(name: GenStatemName<M>, make: F) -> Result<T, CallError>
|
||||||
|
where
|
||||||
|
M: Machine,
|
||||||
|
T: Send + 'static,
|
||||||
|
F: FnOnce(Reply<T>) -> M::Ev,
|
||||||
|
{
|
||||||
|
match whereis_machine(name) {
|
||||||
|
Some(m) => m.call(make),
|
||||||
|
None => Err(CallError::Down),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shut down the machine registered under `name` and wait for it (see
|
||||||
|
/// [`GenStatemRef::shutdown`]). A no-op if no live machine holds the name.
|
||||||
|
pub fn shutdown<M: Machine>(name: GenStatemName<M>) {
|
||||||
|
if let Some(m) = whereis_machine(name) {
|
||||||
|
m.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The machine actor body: `on_start`, then one `handle` per event until the
|
/// The machine actor body: `on_start`, then one `handle` per event until the
|
||||||
/// inbox closes (all refs dropped → graceful shutdown).
|
/// row resolves to `stop`, a shutdown row stops it, or the actor is stopped
|
||||||
|
/// from outside.
|
||||||
///
|
///
|
||||||
/// Two intake sources are selected each iteration with the **timer arm above
|
/// Intake arms are selected each iteration in priority order — **exits**
|
||||||
/// the inbox**, so a timeout fire is never starved by inbox traffic: `sys_rx`
|
/// (only when trapping) above **timers** above the **inbox** — so a shutdown
|
||||||
/// carries timer fires armed through `cx`, `rx` is the user inbox. A fire is
|
/// request or a timeout fire is never starved by inbox traffic. `sys_rx`
|
||||||
/// turned into the matching internal event (`state_timeout` / `timeout(name)`)
|
/// carries timer fires armed through `cx`; a fire is turned into the matching
|
||||||
/// and run through the same `handle` dispatch as an inbox event — the
|
/// internal event (`state_timeout` / `timeout(name)`) and run through the same
|
||||||
/// gen_statem model, where timeouts surface as ordinary events.
|
/// `handle` dispatch as an inbox event — the gen_statem model, where timeouts
|
||||||
|
/// (and, when trapping, shutdown and exits) surface as ordinary events.
|
||||||
///
|
///
|
||||||
/// The loop owns the **postpone queue**: a `handle` that defers its event hands
|
/// The loop owns the **postpone queue**: a `handle` that defers its event hands
|
||||||
/// it back ([`Step::Postponed`]) for the queue; a `handle` that transitions
|
/// it back ([`Step::Postponed`]) for the queue; a `handle` that transitions
|
||||||
/// ([`Step::Transitioned`]) triggers a [`replay`] of the queue in the new
|
/// ([`Step::Transitioned`]) triggers a [`replay`] of the queue in the new
|
||||||
/// state, ahead of the next intake.
|
/// state, ahead of the next intake.
|
||||||
fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
fn statem_loop<M: Machine>(keep: Sender<M::Ev>, rx: Receiver<M::Ev>, machine: M) {
|
||||||
|
// One inbox sender lives with the loop: the inbox never closes, refs are
|
||||||
|
// addresses, the machine's lifetime is the actor's (stop row, shutdown,
|
||||||
|
// stop, panic). The `Disconnected` inbox arm below is defensive only.
|
||||||
|
let _keep = keep;
|
||||||
|
// Drop guard — owns the machine and the timer registry, so `terminate`
|
||||||
|
// fires on every exit path (clean close, `stop`, a handler panic, a hard
|
||||||
|
// stop) and the timer drain is sequenced before it. Same shape as
|
||||||
|
// gen_server's guard; see the rationale there.
|
||||||
|
struct Terminate<M: Machine>(M, Arc<Mutex<Timers>>);
|
||||||
|
impl<M: Machine> Drop for Terminate<M> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
{
|
||||||
|
let mut reg = match self.1.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
||||||
|
};
|
||||||
|
if let Some((_, sub)) = reg.state.take() {
|
||||||
|
cancel_timer(sub);
|
||||||
|
}
|
||||||
|
for (_, (_, sub)) in reg.named.drain() {
|
||||||
|
cancel_timer(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.0.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (sys_tx, sys_rx) = channel::<Sys>();
|
let (sys_tx, sys_rx) = channel::<Sys>();
|
||||||
let reg = Arc::new(Mutex::new(Timers::new()));
|
let reg = Arc::new(Mutex::new(Timers::new()));
|
||||||
|
let mut guard = Terminate(machine, reg.clone());
|
||||||
// The loop owns `cx` (and through it a `sys_tx` clone) for its whole life,
|
// The loop owns `cx` (and through it a `sys_tx` clone) for its whole life,
|
||||||
// so the sys arm never closes from under us — no auto-close dance needed.
|
// so the sys arm never closes from under us — no auto-close dance needed.
|
||||||
let mut cx = Cx::new(sys_tx, reg.clone());
|
let mut cx = Cx::new(sys_tx, reg.clone());
|
||||||
// Events deferred by `postpone` rows, replayed FIFO on the next transition.
|
// Events deferred by `postpone` rows, replayed FIFO on the next transition.
|
||||||
let mut postpone: VecDeque<M::Ev> = VecDeque::new();
|
let mut postpone: VecDeque<M::Ev> = VecDeque::new();
|
||||||
machine.on_start(&mut cx);
|
guard.0.on_start(&mut cx);
|
||||||
|
// Trapping is opted into during on_start and fixed for the loop's life.
|
||||||
|
// The inbox is armed only when set: an untrapped machine keeps the
|
||||||
|
// two-arm select, and a shutdown request simply stops it as
|
||||||
|
// `request_stop` would.
|
||||||
|
let exits: Option<Receiver<ExitSignal>> = cx.trap.then(crate::link::trap_exit);
|
||||||
loop {
|
loop {
|
||||||
// Timer arm first: a ready fire is taken in preference to the inbox.
|
// Arm order encodes priority: exits → timers → inbox.
|
||||||
let i = select(&[&sys_rx, &rx]);
|
let ne = exits.is_some() as usize;
|
||||||
if i == 0 {
|
let i = {
|
||||||
|
let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(3);
|
||||||
|
if let Some(e) = &exits {
|
||||||
|
arms.push(e);
|
||||||
|
}
|
||||||
|
arms.push(&sys_rx);
|
||||||
|
arms.push(&rx);
|
||||||
|
select(&arms)
|
||||||
|
};
|
||||||
|
if i < ne {
|
||||||
|
// Exit arm: a shutdown request or a linked peer's death. The trap
|
||||||
|
// inbox lives for the loop's life, so it never closes.
|
||||||
|
let sig = exits.as_ref().and_then(|e| e.try_recv().ok().flatten());
|
||||||
|
match sig {
|
||||||
|
Some(sig) if sig.reason == DownReason::Shutdown => match M::shutdown_ev() {
|
||||||
|
Some(ev) => dispatch(&mut guard.0, &mut cx, &mut postpone, ev),
|
||||||
|
None => cx.stop(),
|
||||||
|
},
|
||||||
|
Some(sig) => {
|
||||||
|
if let Some(ev) = M::exit_ev(sig) {
|
||||||
|
dispatch(&mut guard.0, &mut cx, &mut postpone, ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
} else if i == ne {
|
||||||
match sys_rx.try_recv() {
|
match sys_rx.try_recv() {
|
||||||
Ok(Some(fire)) => {
|
Ok(Some(fire)) => {
|
||||||
// Confirm the fire is still the live one before dispatching:
|
// Confirm the fire is still the live one before dispatching:
|
||||||
@@ -488,7 +740,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
Sys::StateTimeout(local) => {
|
Sys::StateTimeout(local) => {
|
||||||
let mut t = match reg.lock() {
|
let mut t = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
match t.state {
|
match t.state {
|
||||||
Some((live, _)) if live == local => {
|
Some((live, _)) if live == local => {
|
||||||
@@ -501,7 +755,9 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
Sys::Timeout(name, local) => {
|
Sys::Timeout(name, local) => {
|
||||||
let mut t = match reg.lock() {
|
let mut t = match reg.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => panic!("smarm: gen_statem reg lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!(
|
||||||
|
"smarm: gen_statem reg lock poisoned (core corrupt): {e}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
match t.named.get(name) {
|
match t.named.get(name) {
|
||||||
Some(&(live, _)) if live == local => {
|
Some(&(live, _)) if live == local => {
|
||||||
@@ -513,7 +769,7 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(ev) = ev {
|
if let Some(ev) = ev {
|
||||||
dispatch(&mut machine, &mut cx, &mut postpone, ev);
|
dispatch(&mut guard.0, &mut cx, &mut postpone, ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Single-receiver: nothing can drain the arm between select's
|
// Single-receiver: nothing can drain the arm between select's
|
||||||
@@ -525,12 +781,16 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match rx.try_recv() {
|
match rx.try_recv() {
|
||||||
Ok(Some(ev)) => dispatch(&mut machine, &mut cx, &mut postpone, ev),
|
Ok(Some(ev)) => dispatch(&mut guard.0, &mut cx, &mut postpone, ev),
|
||||||
Ok(None) => debug_assert!(false, "ready inbox was empty"),
|
Ok(None) => debug_assert!(false, "ready inbox was empty"),
|
||||||
// All GenStatemRefs dropped → inbox closed → shutdown.
|
// Defensive: the loop holds a sender, so unreachable.
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A handler (or a replay) asked to stop: a normal exit.
|
||||||
|
if cx.stop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Observation point so a machine fed a hot inbox stays preemptible and
|
// Observation point so a machine fed a hot inbox stays preemptible and
|
||||||
// cancellable.
|
// cancellable.
|
||||||
crate::check!();
|
crate::check!();
|
||||||
@@ -539,7 +799,8 @@ fn statem_loop<M: Machine>(rx: Receiver<M::Ev>, mut machine: M) {
|
|||||||
|
|
||||||
/// Run one event through `handle` and act on its [`Step`]: stash a deferred
|
/// Run one event through `handle` and act on its [`Step`]: stash a deferred
|
||||||
/// event on the postpone queue, or — on a real transition — [`replay`] the
|
/// event on the postpone queue, or — on a real transition — [`replay`] the
|
||||||
/// queue in the new state. A stay/unmatched event needs nothing further.
|
/// queue in the new state. A stay/unmatched event needs nothing further. A
|
||||||
|
/// [`Cx::stop`] raised by the handler skips the replay; the loop breaks next.
|
||||||
fn dispatch<M: Machine>(
|
fn dispatch<M: Machine>(
|
||||||
machine: &mut M,
|
machine: &mut M,
|
||||||
cx: &mut Cx<M::Ev>,
|
cx: &mut Cx<M::Ev>,
|
||||||
@@ -549,14 +810,19 @@ fn dispatch<M: Machine>(
|
|||||||
match machine.handle(ev, cx) {
|
match machine.handle(ev, cx) {
|
||||||
Step::Postponed(ev) => postpone.push_back(ev),
|
Step::Postponed(ev) => postpone.push_back(ev),
|
||||||
Step::Stayed => {}
|
Step::Stayed => {}
|
||||||
Step::Transitioned => replay(machine, cx, postpone),
|
Step::Transitioned => {
|
||||||
|
if !cx.stop {
|
||||||
|
replay(machine, cx, postpone)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replay deferred events after a real transition: each goes back through
|
/// Replay deferred events after a real transition: each goes back through
|
||||||
/// `handle` in FIFO order, in the now-current state. An event that postpones
|
/// `handle` in FIFO order, in the now-current state. An event that postpones
|
||||||
/// again re-queues (to wait for the *next* transition); one that transitions
|
/// again re-queues (to wait for the *next* transition); one that transitions
|
||||||
/// re-arms the replay, so a later state can in turn drain what is still pending.
|
/// re-arms the replay, so a later state can in turn drain what is still pending;
|
||||||
|
/// one that raises [`Cx::stop`] ends the replay (and the machine) at once.
|
||||||
/// Subsequent events in a batch already see the post-transition state, since
|
/// Subsequent events in a batch already see the post-transition state, since
|
||||||
/// `handle` reads the live state cell — the outer loop only re-runs to give
|
/// `handle` reads the live state cell — the outer loop only re-runs to give
|
||||||
/// re-queued events another pass once a transition has occurred within a batch.
|
/// re-queued events another pass once a transition has occurred within a batch.
|
||||||
@@ -575,6 +841,9 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
Step::Stayed => {}
|
Step::Stayed => {}
|
||||||
Step::Transitioned => transitioned = true,
|
Step::Transitioned => transitioned = true,
|
||||||
}
|
}
|
||||||
|
if cx.stop {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !transitioned {
|
if !transitioned {
|
||||||
return;
|
return;
|
||||||
@@ -676,8 +945,9 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
/// // The transition table. Group rows by current state with `on <pat>`.
|
/// // The transition table. Group rows by current state with `on <pat>`.
|
||||||
/// // A row is: <kind> <event-pattern> [if <guard>] => <tail> ,
|
/// // A row is: <kind> <event-pattern> [if <guard>] => <tail> ,
|
||||||
/// // where <kind> is one of `cast`, `call`, `info`, `state_timeout`
|
/// // where <kind> is one of `cast`, `call`, `info`, `state_timeout`
|
||||||
/// // (no pattern — it is a unit event), or `timeout <name-pattern>`,
|
/// // (no pattern — it is a unit event), `timeout <name-pattern>`,
|
||||||
/// // and the tail is one of:
|
/// // `shutdown` (unit; trapping machines only) or `exit <sig-pattern>`
|
||||||
|
/// // (trapping only), and the tail is one of:
|
||||||
/// // * a state tag `Door::Closed` (transition, or "stay"
|
/// // * a state tag `Door::Closed` (transition, or "stay"
|
||||||
/// // if it equals current)
|
/// // if it equals current)
|
||||||
/// // * a block ending in one `{ data.enters += 1; Door::Closed }`
|
/// // * a block ending in one `{ data.enters += 1; Door::Closed }`
|
||||||
@@ -686,6 +956,9 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
/// // * the keyword `postpone` (defer until next
|
/// // * the keyword `postpone` (defer until next
|
||||||
/// // transition; cast/call/
|
/// // transition; cast/call/
|
||||||
/// // info only)
|
/// // info only)
|
||||||
|
/// // * the keyword `stop` (end the machine
|
||||||
|
/// // normally; sugar for
|
||||||
|
/// // `{ cx.stop(); prev }`)
|
||||||
/// on Door::Open => {
|
/// on Door::Open => {
|
||||||
/// cast DoorCast::Push => Door::Closed,
|
/// cast DoorCast::Push => Door::Closed,
|
||||||
/// // An armed state-timeout surfaces as an ordinary event:
|
/// // An armed state-timeout surfaces as an ordinary event:
|
||||||
@@ -709,6 +982,9 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
/// on _ => {
|
/// on _ => {
|
||||||
/// call DoorCall::GetState(r) => { r.reply(prev); prev },
|
/// call DoorCall::GetState(r) => { r.reply(prev); prev },
|
||||||
/// }
|
/// }
|
||||||
|
///
|
||||||
|
/// // Optional: runs as the machine exits, on every exit path.
|
||||||
|
/// terminate { data.enters = 0; }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
@@ -733,6 +1009,12 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
/// ignores info writes no `info` rows at all. State-timeouts and named
|
/// ignores info writes no `info` rows at all. State-timeouts and named
|
||||||
/// timeouts have **no** such default: a state that can see one must handle it
|
/// timeouts have **no** such default: a state that can see one must handle it
|
||||||
/// (or `unhandled` it) or the match is non-exhaustive.
|
/// (or `unhandled` it) or the match is non-exhaustive.
|
||||||
|
/// * **`shutdown` defaults to `stop`, `exit` to a silent drop.** Both reach a
|
||||||
|
/// machine only if its initial `enter` called `cx.trap_exit()`; a
|
||||||
|
/// non-trapping machine is stopped outright by a shutdown request. Write
|
||||||
|
/// `shutdown => …` rows only in the states that want to wind down first
|
||||||
|
/// (transition into a draining state, `stop` later); write `exit sig => …`
|
||||||
|
/// rows to react to linked-peer deaths. Neither is postponable.
|
||||||
/// * **Stay** = return the current tag. The `prev` you named in `context` is
|
/// * **Stay** = return the current tag. The `prev` you named in `context` is
|
||||||
/// bound to the pre-handler state for exactly this — handy in any-state
|
/// bound to the pre-handler state for exactly this — handy in any-state
|
||||||
/// (`on _`) rows where there is no single literal tag to write.
|
/// (`on _`) rows where there is no single literal tag to write.
|
||||||
@@ -758,12 +1040,12 @@ fn replay<M: Machine>(machine: &mut M, cx: &mut Cx<M::Ev>, postpone: &mut VecDeq
|
|||||||
/// # What it emits
|
/// # What it emits
|
||||||
///
|
///
|
||||||
/// The unified `enum $Ev` (the `Cast`/`Call`/`Info` wrappers plus the internal
|
/// The unified `enum $Ev` (the `Cast`/`Call`/`Info` wrappers plus the internal
|
||||||
/// `StateTimeout` / `Timeout` events), `struct $Sm { state, data }`,
|
/// `StateTimeout` / `Timeout` / `Shutdown` / `Exit` events), `struct $Sm {
|
||||||
/// `$Sm::start(init, data) -> GenStatemRef<$Sm>`, and the `Machine` impl:
|
/// state, data }`, `$Sm::start(init, data) -> GenStatemRef<$Sm>`, and the
|
||||||
/// `on_start` runs the initial `enter`; `handle` is the dispatch match plus the
|
/// `Machine` impl: `on_start` runs the initial `enter`; `handle` is the
|
||||||
/// stay/transition/unhandled apply-tail (the cell's sole writer, which also
|
/// dispatch match plus the stay/transition/unhandled apply-tail (the cell's
|
||||||
/// auto-resets the state-timeout on every real transition); and the `enter`
|
/// sole writer, which also auto-resets the state-timeout on every real
|
||||||
/// dispatch.
|
/// transition); the `enter` dispatch; and `terminate` when the block is given.
|
||||||
///
|
///
|
||||||
/// # Limitation
|
/// # Limitation
|
||||||
///
|
///
|
||||||
@@ -779,9 +1061,10 @@ macro_rules! gen_statem {
|
|||||||
context ( $data:ident , $cur:ident , $cx:ident ) ;
|
context ( $data:ident , $cur:ident , $cx:ident ) ;
|
||||||
enter { $( $est:pat => $ebody:expr ),+ $(,)? }
|
enter { $( $est:pat => $ebody:expr ),+ $(,)? }
|
||||||
$( on $st:pat => { $($rows:tt)* } )+
|
$( on $st:pat => { $($rows:tt)* } )+
|
||||||
|
$( terminate { $($tbody:tt)* } )?
|
||||||
) => {
|
) => {
|
||||||
/// Unified inbox payload: the user's `cast`/`call`/`info` enums folded
|
/// Unified inbox payload: the user's `cast`/`call`/`info` enums folded
|
||||||
/// together with the runtime's internal timeout events.
|
/// together with the runtime's internal events.
|
||||||
enum $Ev {
|
enum $Ev {
|
||||||
Cast($Cast),
|
Cast($Cast),
|
||||||
Call($Call),
|
Call($Call),
|
||||||
@@ -792,6 +1075,12 @@ macro_rules! gen_statem {
|
|||||||
StateTimeout,
|
StateTimeout,
|
||||||
/// A named timeout fired (matched in `timeout <pat>` rows).
|
/// A named timeout fired (matched in `timeout <pat>` rows).
|
||||||
Timeout(&'static str),
|
Timeout(&'static str),
|
||||||
|
/// A graceful shutdown request reached this (trapping) machine
|
||||||
|
/// (matched in `shutdown` rows). A state with no such row `stop`s.
|
||||||
|
Shutdown,
|
||||||
|
/// A linked peer died (trapping only; matched in `exit <pat>`
|
||||||
|
/// rows). An unmatched exit is silently dropped.
|
||||||
|
Exit($crate::ExitSignal),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct $sm {
|
struct $sm {
|
||||||
@@ -800,8 +1089,14 @@ macro_rules! gen_statem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $sm {
|
impl $sm {
|
||||||
|
/// The machine value, for [`gen_statem::run_named`]
|
||||||
|
/// (`$crate::gen_statem::run_named`) or `spawn`.
|
||||||
|
fn new(init: $State, data: $Data) -> $sm {
|
||||||
|
$sm { state: init, data }
|
||||||
|
}
|
||||||
|
|
||||||
fn start(init: $State, data: $Data) -> $crate::gen_statem::GenStatemRef<$sm> {
|
fn start(init: $State, data: $Data) -> $crate::gen_statem::GenStatemRef<$sm> {
|
||||||
$crate::gen_statem::spawn($sm { state: init, data })
|
$crate::gen_statem::spawn($sm::new(init, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -825,11 +1120,27 @@ macro_rules! gen_statem {
|
|||||||
$Ev::Timeout(name)
|
$Ev::Timeout(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shutdown_ev() -> Option<$Ev> {
|
||||||
|
Some($Ev::Shutdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit_ev(sig: $crate::ExitSignal) -> Option<$Ev> {
|
||||||
|
Some($Ev::Exit(sig))
|
||||||
|
}
|
||||||
|
|
||||||
fn on_start(&mut self, $cx: &mut $crate::gen_statem::Cx<$Ev>) {
|
fn on_start(&mut self, $cx: &mut $crate::gen_statem::Cx<$Ev>) {
|
||||||
let s = self.state;
|
let s = self.state;
|
||||||
self.enter(s, $cx);
|
self.enter(s, $cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn terminate(&mut self) {
|
||||||
|
let $data = &mut self.data;
|
||||||
|
$($tbody)*
|
||||||
|
}
|
||||||
|
)?
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[deny(unreachable_patterns)] // conflicting rows must fail even though
|
#[deny(unreachable_patterns)] // conflicting rows must fail even though
|
||||||
// this match is external-macro-expanded
|
// this match is external-macro-expanded
|
||||||
@@ -845,7 +1156,7 @@ macro_rules! gen_statem {
|
|||||||
// untouched), then the consuming `match (state, event)` whose
|
// untouched), then the consuming `match (state, event)` whose
|
||||||
// value is this `Resolution`.
|
// value is this `Resolution`.
|
||||||
let next: $crate::gen_statem::Resolution<$State> =
|
let next: $crate::gen_statem::Resolution<$State> =
|
||||||
$crate::gen_statem!(@arms ($Ev) ($cur, ev) [ ] [ ]
|
$crate::gen_statem!(@arms ($Ev) ($cur, ev, $cx) [ ] [ ]
|
||||||
$( on $st => { $($rows)* } )+);
|
$( on $st => { $($rows)* } )+);
|
||||||
match next {
|
match next {
|
||||||
$crate::gen_statem::Resolution::To(s) if s == $cur => {
|
$crate::gen_statem::Resolution::To(s) if s == $cur => {
|
||||||
@@ -881,7 +1192,7 @@ macro_rules! gen_statem {
|
|||||||
// is the `Info` silent-drop — last and broadest, so per-state `info` rows
|
// is the `Info` silent-drop — last and broadest, so per-state `info` rows
|
||||||
// stay reachable; cast/call/timeouts get no fallback, so a forgotten pair is
|
// stay reachable; cast/call/timeouts get no fallback, so a forgotten pair is
|
||||||
// still E0004).
|
// still E0004).
|
||||||
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ]) => {
|
(@arms ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ]) => {
|
||||||
{
|
{
|
||||||
// Phase 1 — postpone routing (borrow-only). A guard on a postpone
|
// Phase 1 — postpone routing (borrow-only). A guard on a postpone
|
||||||
// row runs here, against by-ref bindings, so it must not depend on
|
// row runs here, against by-ref bindings, so it must not depend on
|
||||||
@@ -898,140 +1209,247 @@ macro_rules! gen_statem {
|
|||||||
// returned for it.
|
// returned for it.
|
||||||
match ($ss, $se) {
|
match ($ss, $se) {
|
||||||
$($arms)*
|
$($arms)*
|
||||||
|
// Macro-injected defaults, last and broadest so per-state rows
|
||||||
|
// stay reachable; a user's own catch-all row may shadow them
|
||||||
|
// entirely, hence the allow.
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
(_, $Ev::Info(_)) => $crate::gen_statem::Resolution::Unhandled,
|
(_, $Ev::Info(_)) => $crate::gen_statem::Resolution::Unhandled,
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
(_, $Ev::Exit(_)) => $crate::gen_statem::Resolution::Unhandled,
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
(_, $Ev::Shutdown) => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Open an on-block: remember its state pat, drain its rows, then continue.
|
// Open an on-block: remember its state pat, drain its rows, then continue.
|
||||||
(@arms ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ]
|
(@arms ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ]
|
||||||
on $st:pat => { $($rows:tt)* } $($more:tt)*
|
on $st:pat => { $($rows:tt)* } $($more:tt)*
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se) [ $($arms)* ] [ $($post)* ] ($st)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx) [ $($arms)* ] [ $($post)* ] ($st)
|
||||||
{ $($rows)* } { $($more)* })
|
{ $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== @rows: drain one on-block's rows, threading both accs =============
|
// ===== @rows: drain one on-block's rows, threading both accs =============
|
||||||
// cast, explicit refusal
|
// cast, explicit refusal
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ cast $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
{ cast $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// cast, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ cast $ev:pat $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// cast, postpone (defer the event; the replay in a later state handles it)
|
// cast, postpone (defer the event; the replay in a later state handles it)
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ cast $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
{ cast $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
||||||
[ $($post)* ($st, $Ev::Cast($ev)) $(if $g)? => true, ]
|
[ $($post)* ($st, $Ev::Cast($ev)) $(if $g)? => true, ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// cast, transition / stay / branch
|
// cast, transition / stay / branch
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ cast $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
{ cast $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
[ $($arms)* ($st, $Ev::Cast($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// call, explicit refusal
|
// call, explicit refusal
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ call $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
{ call $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// call, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ call $ev:pat $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// call, postpone (the Reply rides inside the event onto the queue)
|
// call, postpone (the Reply rides inside the event onto the queue)
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ call $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
{ call $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
||||||
[ $($post)* ($st, $Ev::Call($ev)) $(if $g)? => true, ]
|
[ $($post)* ($st, $Ev::Call($ev)) $(if $g)? => true, ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// call, transition / stay / branch
|
// call, transition / stay / branch
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ call $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
{ call $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
[ $($arms)* ($st, $Ev::Call($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// info, explicit refusal
|
// info, explicit refusal
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ info $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
{ info $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// info, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ info $ev:pat $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// info, postpone
|
// info, postpone
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ info $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
{ info $ev:pat $(if $g:expr)? => postpone , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => unreachable!("postponed event is replayed, not dispatched here"), ]
|
||||||
[ $($post)* ($st, $Ev::Info($ev)) $(if $g)? => true, ]
|
[ $($post)* ($st, $Ev::Info($ev)) $(if $g)? => true, ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// info, transition / stay / branch
|
// info, transition / stay / branch
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ info $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
{ info $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
[ $($arms)* ($st, $Ev::Info($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// state_timeout, explicit refusal (unit event — no pattern; not postponable)
|
// state_timeout, explicit refusal (unit event — no pattern; not postponable)
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ state_timeout $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
{ state_timeout $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// state_timeout, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ state_timeout $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// state_timeout, transition / stay / branch
|
// state_timeout, transition / stay / branch
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ state_timeout $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
{ state_timeout $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
[ $($arms)* ($st, $Ev::StateTimeout) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
// timeout, explicit refusal (pattern matches the name; not postponable)
|
// timeout, explicit refusal (pattern matches the name; not postponable)
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ timeout $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
{ timeout $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// timeout, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ timeout $ev:pat $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// timeout, transition / stay / branch
|
// timeout, transition / stay / branch
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ timeout $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
{ timeout $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@rows ($Ev) ($ss, $se)
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
[ $($arms)* ($st, $Ev::Timeout($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
[ $($post)* ]
|
[ $($post)* ]
|
||||||
($st) { $($rows)* } { $($more)* })
|
($st) { $($rows)* } { $($more)* })
|
||||||
};
|
};
|
||||||
|
// shutdown, explicit refusal (unit event — no pattern; not postponable; a state with no shutdown row defaults to `stop`)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ shutdown $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Shutdown) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
|
// shutdown, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ shutdown $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Shutdown) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
|
// shutdown, transition / stay / branch
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ shutdown $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Shutdown) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
|
// exit, explicit refusal (pattern matches the ExitSignal; not postponable)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ exit $ev:pat $(if $g:expr)? => unhandled , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Exit($ev)) $(if $g)? => $crate::gen_statem::Resolution::Unhandled, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
|
// exit, stop (end the machine normally after this event)
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ exit $ev:pat $(if $g:expr)? => stop , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Exit($ev)) $(if $g)? => { $cx.stop(); $crate::gen_statem::Resolution::To($ss) }, ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
|
// exit, transition / stay / branch
|
||||||
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
|
{ exit $ev:pat $(if $g:expr)? => $tail:expr , $($rows:tt)* } { $($more:tt)* }
|
||||||
|
) => {
|
||||||
|
$crate::gen_statem!(@rows ($Ev) ($ss, $se, $cx)
|
||||||
|
[ $($arms)* ($st, $Ev::Exit($ev)) $(if $g)? => $crate::gen_statem::Resolution::To($tail.into()), ]
|
||||||
|
[ $($post)* ]
|
||||||
|
($st) { $($rows)* } { $($more)* })
|
||||||
|
};
|
||||||
// this block is drained: hand the remaining on-blocks back to @arms
|
// this block is drained: hand the remaining on-blocks back to @arms
|
||||||
(@rows ($Ev:ident) ($ss:expr, $se:expr) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
(@rows ($Ev:ident) ($ss:expr, $se:expr, $cx:ident) [ $($arms:tt)* ] [ $($post:tt)* ] ($st:pat)
|
||||||
{ } { $($more:tt)* }
|
{ } { $($more:tt)* }
|
||||||
) => {
|
) => {
|
||||||
$crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] [ $($post)* ] $($more)*)
|
$crate::gen_statem!(@arms ($Ev) ($ss, $se, $cx) [ $($arms)* ] [ $($post)* ] $($more)*)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-4
@@ -239,7 +239,10 @@ pub fn snapshot() -> RuntimeSnapshot {
|
|||||||
actors.push(info);
|
actors.push(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuntimeSnapshot { format_version: SNAPSHOT_FORMAT_VERSION, actors }
|
RuntimeSnapshot {
|
||||||
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||||
|
actors,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +390,10 @@ pub fn tree() -> RuntimeTree {
|
|||||||
/// want to inspect again) and want the tree view of it without re-reading
|
/// want to inspect again) and want the tree view of it without re-reading
|
||||||
/// the runtime.
|
/// the runtime.
|
||||||
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
||||||
let RuntimeSnapshot { format_version, actors } = snap;
|
let RuntimeSnapshot {
|
||||||
|
format_version,
|
||||||
|
actors,
|
||||||
|
} = snap;
|
||||||
|
|
||||||
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
||||||
for (i, a) in actors.iter().enumerate() {
|
for (i, a) in actors.iter().enumerate() {
|
||||||
@@ -418,7 +424,10 @@ pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
||||||
.collect();
|
.collect();
|
||||||
RuntimeTree { format_version, roots: root_nodes }
|
RuntimeTree {
|
||||||
|
format_version,
|
||||||
|
roots: root_nodes,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_node(
|
fn build_node(
|
||||||
@@ -436,5 +445,9 @@ fn build_node(
|
|||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Some(TreeNode { info, orphaned: orphaned[i], children })
|
Some(TreeNode {
|
||||||
|
info,
|
||||||
|
orphaned: orphaned[i],
|
||||||
|
children,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ pub struct IoThread {
|
|||||||
waiters: Waiters,
|
waiters: Waiters,
|
||||||
|
|
||||||
// ----- Epoll machinery -----
|
// ----- Epoll machinery -----
|
||||||
|
|
||||||
/// The epollfd, owned by `IoThread`. Callable cross-thread via
|
/// The epollfd, owned by `IoThread`. Callable cross-thread via
|
||||||
/// `epoll_ctl` per the man page.
|
/// `epoll_ctl` per the man page.
|
||||||
epollfd: RawFd,
|
epollfd: RawFd,
|
||||||
@@ -147,7 +146,6 @@ pub struct IoThread {
|
|||||||
shutdown_write: RawFd,
|
shutdown_write: RawFd,
|
||||||
|
|
||||||
// ----- Threads -----
|
// ----- Threads -----
|
||||||
|
|
||||||
pool_thread: Option<OsJoinHandle<()>>,
|
pool_thread: Option<OsJoinHandle<()>>,
|
||||||
epoll_thread: Option<OsJoinHandle<()>>,
|
epoll_thread: Option<OsJoinHandle<()>>,
|
||||||
}
|
}
|
||||||
@@ -284,9 +282,8 @@ impl IoThread {
|
|||||||
events,
|
events,
|
||||||
u64: fd as u64,
|
u64: fd as u64,
|
||||||
};
|
};
|
||||||
let r = unsafe {
|
let r =
|
||||||
libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _)
|
unsafe { libc::epoll_ctl(self.epollfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) };
|
||||||
};
|
|
||||||
if r < 0 {
|
if r < 0 {
|
||||||
return Err(io::Error::last_os_error());
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
@@ -398,12 +395,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let n = unsafe {
|
let n = unsafe {
|
||||||
libc::epoll_wait(
|
libc::epoll_wait(epollfd, events.as_mut_ptr(), MAX_EVENTS as libc::c_int, -1)
|
||||||
epollfd,
|
|
||||||
events.as_mut_ptr(),
|
|
||||||
MAX_EVENTS as libc::c_int,
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
@@ -438,12 +430,7 @@ fn epoll_loop(epollfd: RawFd, waiters: Waiters, rt: Weak<RuntimeInner>) {
|
|||||||
let entry = w.remove(&fd);
|
let entry = w.remove(&fd);
|
||||||
if entry.is_some() {
|
if entry.is_some() {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::epoll_ctl(
|
libc::epoll_ctl(epollfd, libc::EPOLL_CTL_DEL, fd, std::ptr::null_mut());
|
||||||
epollfd,
|
|
||||||
libc::EPOLL_CTL_DEL,
|
|
||||||
fd,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry
|
entry
|
||||||
|
|||||||
+41
-36
@@ -11,36 +11,36 @@
|
|||||||
//!
|
//!
|
||||||
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
||||||
|
|
||||||
pub mod stack;
|
|
||||||
pub(crate) mod signal;
|
|
||||||
pub mod context;
|
|
||||||
pub mod preempt;
|
|
||||||
pub mod pid;
|
|
||||||
pub mod actor;
|
pub mod actor;
|
||||||
|
pub mod causal;
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod scheduler;
|
pub mod context;
|
||||||
pub mod supervisor;
|
|
||||||
pub mod timer;
|
|
||||||
pub mod io;
|
|
||||||
pub mod mutex;
|
|
||||||
pub mod monitor;
|
|
||||||
pub mod registry;
|
|
||||||
pub mod pg;
|
|
||||||
pub mod link;
|
|
||||||
pub mod gen_server;
|
pub mod gen_server;
|
||||||
pub mod gen_statem;
|
pub mod gen_statem;
|
||||||
pub mod introspect;
|
pub mod introspect;
|
||||||
|
pub mod io;
|
||||||
|
pub mod link;
|
||||||
|
pub mod monitor;
|
||||||
|
pub mod mutex;
|
||||||
#[cfg(feature = "observer")]
|
#[cfg(feature = "observer")]
|
||||||
pub mod observer;
|
pub mod observer;
|
||||||
pub mod runtime;
|
|
||||||
pub(crate) mod park;
|
pub(crate) mod park;
|
||||||
|
pub mod pg;
|
||||||
|
pub mod pid;
|
||||||
|
pub mod preempt;
|
||||||
pub(crate) mod raw_mutex;
|
pub(crate) mod raw_mutex;
|
||||||
pub(crate) mod slot_state;
|
pub mod registry;
|
||||||
pub(crate) mod sync_shim;
|
|
||||||
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
|
#[doc(hidden)] // pub only so benches/rq_micro.rs can drive the raw structures
|
||||||
pub mod run_queue;
|
pub mod run_queue;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub(crate) mod signal;
|
||||||
|
pub(crate) mod slot_state;
|
||||||
|
pub mod stack;
|
||||||
|
pub mod supervisor;
|
||||||
|
pub(crate) mod sync_shim;
|
||||||
|
pub mod timer;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod causal;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Global allocator
|
// Global allocator
|
||||||
@@ -59,36 +59,41 @@ pub use channel::{
|
|||||||
};
|
};
|
||||||
pub use gen_server::{
|
pub use gen_server::{
|
||||||
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
|
||||||
NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher,
|
GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, NamedGenServerBuilder,
|
||||||
|
ShutdownAction, StopHandle, TimerHandle, Watcher,
|
||||||
};
|
};
|
||||||
pub use gen_statem::{
|
pub use gen_statem::{
|
||||||
CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError,
|
CallError as GenStatemCallError, Cx, GenStatemName, GenStatemRef, Machine, Reply, Resolution,
|
||||||
GenStatemRef,
|
SendError as GenStatemSendError,
|
||||||
};
|
};
|
||||||
pub use introspect::{StackInfo,
|
pub use introspect::{
|
||||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||||
TreeNode, SNAPSHOT_FORMAT_VERSION,
|
StackInfo, TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||||
};
|
};
|
||||||
|
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||||
|
pub use monitor::{
|
||||||
|
demonitor, mark_watchable, monitor, terminal_reason, Down, DownReason, Monitor, MonitorId,
|
||||||
|
};
|
||||||
|
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||||
#[cfg(feature = "observer")]
|
#[cfg(feature = "observer")]
|
||||||
pub use observer::{ObserverReply, ObserverRequest};
|
pub use observer::{ObserverReply, ObserverRequest};
|
||||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
pub use pg::{
|
||||||
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId,
|
||||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
};
|
||||||
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
|
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
|
||||||
pub use pg::{dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId};
|
|
||||||
pub use registry::{
|
pub use registry::{
|
||||||
install, lookup_as, register, send, send_dyn, send_to, unregister, whereis, RegisterError,
|
install, lookup_as, register, resolve_name, send, send_dyn, send_to, unregister, whereis,
|
||||||
SendError,
|
NameResolution, RegisterError, SendError,
|
||||||
};
|
};
|
||||||
pub use runtime::{init, Config, Runtime};
|
pub use runtime::{init, Config, Runtime, RuntimeHandle};
|
||||||
pub use scheduler::{
|
pub use scheduler::{
|
||||||
block_on_io, cancel_timer, request_stop, run, self_pid, send_after, send_after_named,
|
block_on_io, cancel_timer, request_shutdown, request_stop, run, self_pid, send_after,
|
||||||
send_after_named_wall, send_after_wall, sleep, sleep_wall,
|
send_after_named, send_after_named_wall, send_after_wall, sleep, sleep_wall, spawn, spawn_addr,
|
||||||
spawn, spawn_addr, spawn_addr_with, spawn_under, spawn_under_with, spawn_with,
|
spawn_addr_with, spawn_under, spawn_under_with, spawn_with, try_spawn, try_spawn_under_with,
|
||||||
wait_readable, wait_readable_timeout, wait_writable,
|
wait_readable, wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm,
|
||||||
wait_writable_timeout, yield_now, FdArm, JoinError, JoinHandle, SpawnOpts,
|
JoinError, JoinHandle, SpawnError, SpawnOpts,
|
||||||
};
|
};
|
||||||
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
pub use supervisor::{ChildSpec, OneForOne, Restart, Shutdown, Signal, Strategy};
|
||||||
pub use timer::TimerId;
|
pub use timer::TimerId;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+4
-1
@@ -157,7 +157,10 @@ pub fn link<A>(target: Pid<A>) {
|
|||||||
});
|
});
|
||||||
match my_trap {
|
match my_trap {
|
||||||
Some(tx) => {
|
Some(tx) => {
|
||||||
let _ = tx.send(ExitSignal { from: target, reason: DownReason::NoProc });
|
let _ = tx.send(ExitSignal {
|
||||||
|
from: target,
|
||||||
|
reason: DownReason::NoProc,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None => request_stop(me),
|
None => request_stop(me),
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-1
@@ -98,6 +98,11 @@ pub enum DownReason {
|
|||||||
Panic,
|
Panic,
|
||||||
/// The target was cooperatively cancelled via `request_stop`.
|
/// The target was cooperatively cancelled via `request_stop`.
|
||||||
Stopped,
|
Stopped,
|
||||||
|
/// A graceful shutdown was requested via `request_shutdown`. Only ever
|
||||||
|
/// appears in an [`ExitSignal`](crate::link::ExitSignal) delivered to a
|
||||||
|
/// trapping actor — never in a [`Down`]: a target that honours the request
|
||||||
|
/// exits *normally*, one that does not trap is `Stopped`.
|
||||||
|
Shutdown,
|
||||||
/// The target was already gone (finished and reclaimed, or never alive)
|
/// The target was already gone (finished and reclaimed, or never alive)
|
||||||
/// at the moment `monitor()` was called.
|
/// at the moment `monitor()` was called.
|
||||||
NoProc,
|
NoProc,
|
||||||
@@ -171,12 +176,73 @@ pub fn monitor<A>(target: Pid<A>) -> Monitor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !registered {
|
if !registered {
|
||||||
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc });
|
let _ = tx.send(Down {
|
||||||
|
pid: target,
|
||||||
|
reason: DownReason::NoProc,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor { id, target, rx }
|
Monitor { id, target, rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flag `target`'s tenancy as watchable: its death will stamp the slot's
|
||||||
|
/// terminal record (see [`terminal_reason`]), exactly as registering a name
|
||||||
|
/// does. The bridge calls this wherever a smarm pid is *encoded across the
|
||||||
|
/// boundary* — a contract reply, an introspection listing — because BEAM can
|
||||||
|
/// only watch pids it holds, and can only hold pids that crossed. Keeping the
|
||||||
|
/// bit rare is what keeps the record alive: anonymous never-exported churn
|
||||||
|
/// (holder threads, egress tasks) stays ineligible and cannot evict a
|
||||||
|
/// watchable tenancy's record from a LIFO-recycled slot.
|
||||||
|
///
|
||||||
|
/// Generation-checked and live-screened: marking a pid whose tenancy already
|
||||||
|
/// ended is a no-op — its record either exists (it was flagged before dying)
|
||||||
|
/// or is honestly unknowable. Same `Runtime::run()` context contract as
|
||||||
|
/// [`monitor`].
|
||||||
|
pub fn mark_watchable<A>(target: Pid<A>) {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
if let Some(slot) = inner.slot_at(target) {
|
||||||
|
// Cold lock FIRST: finalize publishes Done and checks the
|
||||||
|
// watchable bit under this same lock, so the mark either lands
|
||||||
|
// before finalize reads it (the death stamps) or observes the
|
||||||
|
// tenancy already dead (no-op). No lost-stamp window between an
|
||||||
|
// unlocked liveness read and the flag set.
|
||||||
|
let mut cold = slot.cold.lock();
|
||||||
|
if slot.is_live_for(target) {
|
||||||
|
cold.watchable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The terminal [`DownReason`] of the tenancy `target` names, if that tenancy
|
||||||
|
/// ever registered a name and is the *most recent named* death of its slot:
|
||||||
|
/// finalize stamps the slot with `(generation, reason)` for once-registered
|
||||||
|
/// tenancies (anonymous green-thread churn does not stamp — nor evict), and
|
||||||
|
/// the record survives reclaim and the next tenant's install, until the next
|
||||||
|
/// *named* tenant of the slot itself dies. `None` means the pid never lived,
|
||||||
|
/// is still alive, never held a name, or its record was overwritten by a
|
||||||
|
/// later named tenancy's death — callers fall back to `NoProc` semantics.
|
||||||
|
///
|
||||||
|
/// This exists for watch-installers that raced their target's death (bridge
|
||||||
|
/// soak signature 4): a `NoProc` observed at install time can be upgraded to
|
||||||
|
/// the real reason while the record still matches, which is exactly what an
|
||||||
|
/// install that had won the race would have delivered. It does NOT change
|
||||||
|
/// [`monitor`]'s own semantics — monitoring a stale pid still queues `NoProc`,
|
||||||
|
/// the same shape Erlang gives — the upgrade is the caller's deliberate act.
|
||||||
|
/// Same context contract as [`monitor`]: must run inside `Runtime::run()`.
|
||||||
|
pub fn terminal_reason<A>(target: Pid<A>) -> Option<DownReason> {
|
||||||
|
let target = target.erase();
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let slot = inner.slot_at(target)?;
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
match cold.terminal {
|
||||||
|
Some((generation, reason)) if generation == target.generation() => Some(reason),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
|
||||||
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
|
||||||
/// `None` if there was nothing left to remove: the target had already gone
|
/// `None` if there was nothing left to remove: the target had already gone
|
||||||
|
|||||||
+29
-10
@@ -158,7 +158,11 @@ impl TimerTarget for MutexCore {
|
|||||||
if st.holder == Some(pid) {
|
if st.holder == Some(pid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match st.waiters.iter().position(|w| w.pid == pid && w.epoch == epoch) {
|
match st
|
||||||
|
.waiters
|
||||||
|
.iter()
|
||||||
|
.position(|w| w.pid == pid && w.epoch == epoch)
|
||||||
|
{
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
st.waiters.remove(pos);
|
st.waiters.remove(pos);
|
||||||
true
|
true
|
||||||
@@ -246,7 +250,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"),
|
None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"),
|
||||||
};
|
};
|
||||||
return Ok(MutexGuard { mutex: self, value: Some(value) });
|
return Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +294,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing after grant (core corrupt)"),
|
None => panic!("smarm: Mutex value missing after grant (core corrupt)"),
|
||||||
};
|
};
|
||||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(LockTimeout)
|
Err(LockTimeout)
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,10 @@ impl<T> Mutex<T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"),
|
None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"),
|
||||||
};
|
};
|
||||||
Some(MutexGuard { mutex: self, value: Some(value) })
|
Some(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blocking fallback used when called outside the smarm runtime.
|
/// Blocking fallback used when called outside the smarm runtime.
|
||||||
@@ -329,10 +342,15 @@ impl<T> Mutex<T> {
|
|||||||
Ok(mut g) => g.take(),
|
Ok(mut g) => g.take(),
|
||||||
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"),
|
Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"),
|
||||||
};
|
};
|
||||||
if let Some(v) = v { break v; }
|
if let Some(v) = v {
|
||||||
|
break v;
|
||||||
|
}
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
};
|
};
|
||||||
Ok(MutexGuard { mutex: self, value: Some(value) })
|
Ok(MutexGuard {
|
||||||
|
mutex: self,
|
||||||
|
value: Some(value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +360,10 @@ impl<T> Clone for Mutex<T> {
|
|||||||
/// lock and one protected value; locking through any clone excludes
|
/// lock and one protected value; locking through any clone excludes
|
||||||
/// every other clone.
|
/// every other clone.
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self { core: self.core.clone(), value: self.value.clone() }
|
Self {
|
||||||
|
core: self.core.clone(),
|
||||||
|
value: self.value.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,9 +409,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for MutexGuard<'_, T> {
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("smarm: MutexGuard value missing (core corrupt)"),
|
None => panic!("smarm: MutexGuard value missing (core corrupt)"),
|
||||||
};
|
};
|
||||||
f.debug_tuple("MutexGuard")
|
f.debug_tuple("MutexGuard").field(value).finish()
|
||||||
.field(value)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-15
@@ -105,7 +105,9 @@ mod parker {
|
|||||||
|
|
||||||
impl Parker {
|
impl Parker {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self { state: AtomicU32::new(EMPTY) }
|
Self {
|
||||||
|
state: AtomicU32::new(EMPTY),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||||
@@ -211,7 +213,10 @@ mod parker {
|
|||||||
|
|
||||||
impl Parker {
|
impl Parker {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self { permit: Mutex::new(false), cv: Condvar::new() }
|
Self {
|
||||||
|
permit: Mutex::new(false),
|
||||||
|
cv: Condvar::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
/// Returns `true` = woken (permit consumed), `false` = timed out.
|
||||||
@@ -395,12 +400,10 @@ impl Coordinator {
|
|||||||
// The CAS is the exactly-one guarantee: whoever clears the bit
|
// The CAS is the exactly-one guarantee: whoever clears the bit
|
||||||
// owns the wake; a racing wake_one retries on the observed value
|
// owns the wake; a racing wake_one retries on the observed value
|
||||||
// (coherence: a failed CAS can never read older than `mask`).
|
// (coherence: a failed CAS can never read older than `mask`).
|
||||||
match self.idle.compare_exchange(
|
match self
|
||||||
mask,
|
.idle
|
||||||
mask & !bit,
|
.compare_exchange(mask, mask & !bit, Ordering::AcqRel, Ordering::Acquire)
|
||||||
Ordering::AcqRel,
|
{
|
||||||
Ordering::Acquire,
|
|
||||||
) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.parkers[id].unpark();
|
self.parkers[id].unpark();
|
||||||
return true;
|
return true;
|
||||||
@@ -467,7 +470,8 @@ impl Coordinator {
|
|||||||
// with the deadline still NO_DEADLINE compares `new < MAX` = true
|
// with the deadline still NO_DEADLINE compares `new < MAX` = true
|
||||||
// and over-wakes — the benign direction. (Under the mandated timer
|
// and over-wakes — the benign direction. (Under the mandated timer
|
||||||
// serialization this interleaving cannot occur anyway.)
|
// serialization this interleaving cannot occur anyway.)
|
||||||
self.tk_armed.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
self.tk_armed
|
||||||
|
.store(self.deadline_nanos(deadline), Ordering::SeqCst);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +582,10 @@ mod tests {
|
|||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let r = c.park(0, None, || false);
|
let r = c.park(0, None, || false);
|
||||||
assert_eq!(r, ParkResult::Woken);
|
assert_eq!(r, ParkResult::Woken);
|
||||||
assert!(t0.elapsed() < Duration::from_millis(100), "park blocked despite permit");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_millis(100),
|
||||||
|
"park blocked despite permit"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -632,14 +639,20 @@ mod tests {
|
|||||||
// Wait until all four are published idle.
|
// Wait until all four are published idle.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
while c.idle_mask().count_ones() != N as u32 {
|
while c.idle_mask().count_ones() != N as u32 {
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "threads never parked");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"threads never parked"
|
||||||
|
);
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
assert!(c.wake_one());
|
assert!(c.wake_one());
|
||||||
// Exactly one wakes; give the others a beat to (incorrectly) wake.
|
// Exactly one wakes; give the others a beat to (incorrectly) wake.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
while woken.load(O::SeqCst) == 0 {
|
while woken.load(O::SeqCst) == 0 {
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "wake_one woke nobody");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"wake_one woke nobody"
|
||||||
|
);
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
@@ -706,7 +719,10 @@ mod tests {
|
|||||||
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
|
assert_eq!(c.armed_deadline_nanos(), c.deadline_nanos(d2));
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE);
|
assert_eq!(c.armed_deadline_nanos(), NO_DEADLINE);
|
||||||
assert!(c.try_arm_timer(1, d1), "role must be re-takeable after disarm");
|
assert!(
|
||||||
|
c.try_arm_timer(1, d1),
|
||||||
|
"role must be re-takeable after disarm"
|
||||||
|
);
|
||||||
c.disarm_timer(1);
|
c.disarm_timer(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +737,10 @@ mod tests {
|
|||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let r = c.park(0, Some(far), || false);
|
let r = c.park(0, Some(far), || false);
|
||||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost");
|
assert_eq!(r, ParkResult::Woken, "re-arm wake lost");
|
||||||
assert!(t0.elapsed() < Duration::from_secs(5), "slept toward the stale deadline");
|
assert!(
|
||||||
|
t0.elapsed() < Duration::from_secs(5),
|
||||||
|
"slept toward the stale deadline"
|
||||||
|
);
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,7 +815,11 @@ mod tests {
|
|||||||
assert!(c.try_arm_timer(0, far));
|
assert!(c.try_arm_timer(0, far));
|
||||||
c.note_deadline(near);
|
c.note_deadline(near);
|
||||||
let r = c.park(0, Some(far), || false);
|
let r = c.park(0, Some(far), || false);
|
||||||
assert_eq!(r, ParkResult::Woken, "re-arm wake lost through note_deadline");
|
assert_eq!(
|
||||||
|
r,
|
||||||
|
ParkResult::Woken,
|
||||||
|
"re-arm wake lost through note_deadline"
|
||||||
|
);
|
||||||
c.disarm_timer(0);
|
c.disarm_timer(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,7 +221,9 @@ pub(crate) struct ProcessGroups {
|
|||||||
|
|
||||||
impl ProcessGroups {
|
impl ProcessGroups {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self { groups: HashMap::new() }
|
Self {
|
||||||
|
groups: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert `ms` into `group`. Idempotent on the *member*: if the member is
|
/// Insert `ms` into `group`. Idempotent on the *member*: if the member is
|
||||||
@@ -323,20 +325,33 @@ impl ProcessGroups {
|
|||||||
fn members_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Vec<Pid> {
|
fn members_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Vec<Pid> {
|
||||||
self.groups
|
self.groups
|
||||||
.get(group)
|
.get(group)
|
||||||
.map(|v| v.iter().map(|e| e.member.pid).filter(|&p| is_live(p)).collect())
|
.map(|v| {
|
||||||
|
v.iter()
|
||||||
|
.map(|e| e.member.pid)
|
||||||
|
.filter(|&p| is_live(p))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The first live member of `group` in insertion order — stateless
|
/// The first live member of `group` in insertion order — stateless
|
||||||
/// first-live `pick`, with the same read-path backstop as `members_where`.
|
/// first-live `pick`, with the same read-path backstop as `members_where`.
|
||||||
fn first_member_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Option<Pid> {
|
fn first_member_where(&self, group: &str, mut is_live: impl FnMut(Pid) -> bool) -> Option<Pid> {
|
||||||
self.groups.get(group)?.iter().map(|e| e.member.pid).find(|&p| is_live(p))
|
self.groups
|
||||||
|
.get(group)?
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.member.pid)
|
||||||
|
.find(|&p| is_live(p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the full member identity for `pid` from runtime identity.
|
/// Build the full member identity for `pid` from runtime identity.
|
||||||
fn member_for(inner: &crate::runtime::RuntimeInner, pid: Pid) -> Member {
|
fn member_for(inner: &crate::runtime::RuntimeInner, pid: Pid) -> Member {
|
||||||
Member { node: inner.node_id, incarnation: inner.incarnation, pid }
|
Member {
|
||||||
|
node: inner.node_id,
|
||||||
|
incarnation: inner.incarnation,
|
||||||
|
pid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is `pid` a live actor right now? Generation-checked atomic slot-word read,
|
/// Is `pid` a live actor right now? Generation-checked atomic slot-word read,
|
||||||
@@ -367,7 +382,10 @@ pub fn join<A>(group: impl Into<String>, pid: Pid<A>) -> bool {
|
|||||||
let mon = monitor(pid);
|
let mon = monitor(pid);
|
||||||
|
|
||||||
let (rejected, reaped) = with_runtime(|inner| {
|
let (rejected, reaped) = with_runtime(|inner| {
|
||||||
let ms = Membership { member: member_for(inner, pid), monitor: mon };
|
let ms = Membership {
|
||||||
|
member: member_for(inner, pid),
|
||||||
|
monitor: mon,
|
||||||
|
};
|
||||||
let mut pg = inner.process_groups.lock();
|
let mut pg = inner.process_groups.lock();
|
||||||
let reaped = pg.reap_group(&group);
|
let reaped = pg.reap_group(&group);
|
||||||
let rejected = pg.join(&group, ms);
|
let rejected = pg.join(&group, ms);
|
||||||
@@ -507,7 +525,11 @@ mod tests {
|
|||||||
let (tx, rx) = channel::<Down>();
|
let (tx, rx) = channel::<Down>();
|
||||||
let ms = Membership {
|
let ms = Membership {
|
||||||
member: member(index, generation),
|
member: member(index, generation),
|
||||||
monitor: Monitor { id: MonitorId(0), target: pid, rx },
|
monitor: Monitor {
|
||||||
|
id: MonitorId(0),
|
||||||
|
target: pid,
|
||||||
|
rx,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
(ms, tx)
|
(ms, tx)
|
||||||
}
|
}
|
||||||
@@ -518,7 +540,10 @@ mod tests {
|
|||||||
let (a, _ta) = synth(1, 0);
|
let (a, _ta) = synth(1, 0);
|
||||||
let (b, _tb) = synth(1, 0);
|
let (b, _tb) = synth(1, 0);
|
||||||
assert!(pg.join("workers", a).is_none(), "first join inserts");
|
assert!(pg.join("workers", a).is_none(), "first join inserts");
|
||||||
assert!(pg.join("workers", b).is_some(), "second identical join is handed back");
|
assert!(
|
||||||
|
pg.join("workers", b).is_some(),
|
||||||
|
"second identical join is handed back"
|
||||||
|
);
|
||||||
assert_eq!(pg.members_of("workers"), vec![member(1, 0)]);
|
assert_eq!(pg.members_of("workers"), vec![member(1, 0)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,7 +567,10 @@ mod tests {
|
|||||||
let (a, _ta) = synth(1, 0);
|
let (a, _ta) = synth(1, 0);
|
||||||
let (b, _tb) = synth(1, 1);
|
let (b, _tb) = synth(1, 1);
|
||||||
assert!(pg.join("g", a).is_none());
|
assert!(pg.join("g", a).is_none());
|
||||||
assert!(pg.join("g", b).is_none(), "different generation is a distinct member");
|
assert!(
|
||||||
|
pg.join("g", b).is_none(),
|
||||||
|
"different generation is a distinct member"
|
||||||
|
);
|
||||||
assert_eq!(pg.members_of("g"), vec![member(1, 0), member(1, 1)]);
|
assert_eq!(pg.members_of("g"), vec![member(1, 0), member(1, 1)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,16 +583,27 @@ mod tests {
|
|||||||
pg.join("g", b);
|
pg.join("g", b);
|
||||||
assert!(pg.leave("g", member(1, 0)).is_some());
|
assert!(pg.leave("g", member(1, 0)).is_some());
|
||||||
assert_eq!(pg.members_of("g"), vec![member(2, 0)]);
|
assert_eq!(pg.members_of("g"), vec![member(2, 0)]);
|
||||||
assert!(pg.leave("g", member(1, 0)).is_none(), "second leave finds nothing");
|
assert!(
|
||||||
|
pg.leave("g", member(1, 0)).is_none(),
|
||||||
|
"second leave finds nothing"
|
||||||
|
);
|
||||||
assert!(pg.leave("g", member(2, 0)).is_some());
|
assert!(pg.leave("g", member(2, 0)).is_some());
|
||||||
assert!(pg.members_of("g").is_empty(), "group is now empty");
|
assert!(pg.members_of("g").is_empty(), "group is now empty");
|
||||||
assert!(pg.leave("never", member(9, 0)).is_none(), "leaving an unknown group is a no-op");
|
assert!(
|
||||||
|
pg.leave("never", member(9, 0)).is_none(),
|
||||||
|
"leaving an unknown group is a no-op"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remove_where_sweeps_every_group() {
|
fn remove_where_sweeps_every_group() {
|
||||||
let mut pg = ProcessGroups::new();
|
let mut pg = ProcessGroups::new();
|
||||||
for (g, (m, _t)) in [("a", synth(1, 0)), ("a", synth(2, 0)), ("b", synth(1, 0)), ("c", synth(3, 0))] {
|
for (g, (m, _t)) in [
|
||||||
|
("a", synth(1, 0)),
|
||||||
|
("a", synth(2, 0)),
|
||||||
|
("b", synth(1, 0)),
|
||||||
|
("c", synth(3, 0)),
|
||||||
|
] {
|
||||||
pg.join(g, m);
|
pg.join(g, m);
|
||||||
}
|
}
|
||||||
// Death of pid index 1 (any generation) evicts it everywhere.
|
// Death of pid index 1 (any generation) evicts it everywhere.
|
||||||
@@ -582,8 +621,16 @@ mod tests {
|
|||||||
let pid = Pid::new(1, 0);
|
let pid = Pid::new(1, 0);
|
||||||
let (tx, rx) = channel::<Down>();
|
let (tx, rx) = channel::<Down>();
|
||||||
let dead = Membership {
|
let dead = Membership {
|
||||||
member: Member { node: DEFAULT_NODE_ID, incarnation: Incarnation::new(7), pid },
|
member: Member {
|
||||||
monitor: Monitor { id: MonitorId(0), target: pid, rx },
|
node: DEFAULT_NODE_ID,
|
||||||
|
incarnation: Incarnation::new(7),
|
||||||
|
pid,
|
||||||
|
},
|
||||||
|
monitor: Monitor {
|
||||||
|
id: MonitorId(0),
|
||||||
|
target: pid,
|
||||||
|
rx,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let _keep = tx;
|
let _keep = tx;
|
||||||
let (live, _tl) = synth(2, 0);
|
let (live, _tl) = synth(2, 0);
|
||||||
@@ -614,9 +661,17 @@ mod tests {
|
|||||||
pg.join("b", b1);
|
pg.join("b", b1);
|
||||||
// pid 1 dies: its group-a monitor receives a Down. Its group-b monitor
|
// pid 1 dies: its group-a monitor receives a Down. Its group-b monitor
|
||||||
// has not — reap must still sweep pid 1 out of b by the pid predicate.
|
// has not — reap must still sweep pid 1 out of b by the pid predicate.
|
||||||
ta1.send(Down { pid: Pid::new(1, 0), reason: DownReason::Exit }).unwrap();
|
ta1.send(Down {
|
||||||
|
pid: Pid::new(1, 0),
|
||||||
|
reason: DownReason::Exit,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
let evicted = pg.reap_group("a");
|
let evicted = pg.reap_group("a");
|
||||||
assert_eq!(evicted.len(), 2, "pid 1's memberships in both a and b are evicted");
|
assert_eq!(
|
||||||
|
evicted.len(),
|
||||||
|
2,
|
||||||
|
"pid 1's memberships in both a and b are evicted"
|
||||||
|
);
|
||||||
assert_eq!(pg.members_of("a"), vec![member(2, 0)]);
|
assert_eq!(pg.members_of("a"), vec![member(2, 0)]);
|
||||||
assert!(pg.members_of("b").is_empty(), "swept from b too; pruned");
|
assert!(pg.members_of("b").is_empty(), "swept from b too; pruned");
|
||||||
}
|
}
|
||||||
@@ -646,8 +701,16 @@ mod tests {
|
|||||||
let dead = Pid::new(1, 0);
|
let dead = Pid::new(1, 0);
|
||||||
let oracle = |pid: Pid| pid != dead;
|
let oracle = |pid: Pid| pid != dead;
|
||||||
|
|
||||||
assert_eq!(pg.members_where("g", oracle), vec![Pid::new(2, 0)], "dead pid filtered from read");
|
assert_eq!(
|
||||||
assert_eq!(pg.first_member_where("g", oracle), Some(Pid::new(2, 0)), "pick skips the dead first member");
|
pg.members_where("g", oracle),
|
||||||
|
vec![Pid::new(2, 0)],
|
||||||
|
"dead pid filtered from read"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pg.first_member_where("g", oracle),
|
||||||
|
Some(Pid::new(2, 0)),
|
||||||
|
"pick skips the dead first member"
|
||||||
|
);
|
||||||
|
|
||||||
// Backstop does not evict — that stays the monitor's job; raw storage
|
// Backstop does not evict — that stays the monitor's job; raw storage
|
||||||
// still holds both until reap runs.
|
// still holds both until reap runs.
|
||||||
|
|||||||
+12
-3
@@ -79,7 +79,10 @@ impl Pid<Erased> {
|
|||||||
/// here; typing happens at typed-actor boundaries via [`Pid::from_raw`].
|
/// here; typing happens at typed-actor boundaries via [`Pid::from_raw`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(index: u32, generation: u32) -> Self {
|
pub const fn new(index: u32, generation: u32) -> Self {
|
||||||
Self { raw: RawPid::new(index, generation), _marker: PhantomData }
|
Self {
|
||||||
|
raw: RawPid::new(index, generation),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +93,10 @@ impl<A> Pid<A> {
|
|||||||
/// resolution paths.
|
/// resolution paths.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) const fn from_raw(raw: RawPid) -> Self {
|
pub(crate) const fn from_raw(raw: RawPid) -> Self {
|
||||||
Self { raw, _marker: PhantomData }
|
Self {
|
||||||
|
raw,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The raw identity, dropping the actor type — the key for identity-only
|
/// The raw identity, dropping the actor type — the key for identity-only
|
||||||
@@ -192,7 +198,10 @@ impl<M> Name<M> {
|
|||||||
/// associated constants at call sites.
|
/// associated constants at call sites.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(name: &'static str) -> Self {
|
pub const fn new(name: &'static str) -> Self {
|
||||||
Self { name, _marker: PhantomData }
|
Self {
|
||||||
|
name,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The underlying registry key.
|
/// The underlying registry key.
|
||||||
|
|||||||
+4
-1
@@ -166,7 +166,10 @@ impl<T> RawMutex<T> {
|
|||||||
{
|
{
|
||||||
self.lock_slow();
|
self.lock_slow();
|
||||||
}
|
}
|
||||||
RawMutexGuard { m: self, prev_preempt }
|
RawMutexGuard {
|
||||||
|
m: self,
|
||||||
|
prev_preempt,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
|
|||||||
+107
-15
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
//! Give an actor a name so other actors can find it and message it.
|
//! Give an actor a name so other actors can find it and message it.
|
||||||
//!
|
//!
|
||||||
//! Without the registry, the only way to reach an actor is to already be
|
//! Without the registry, the only way to reach an actor is to already be
|
||||||
@@ -196,7 +195,9 @@ impl<M> std::fmt::Display for SendError<M> {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
||||||
SendError::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"),
|
SendError::Dead(_) => {
|
||||||
|
write!(f, "the addressed actor is no longer the live incarnation")
|
||||||
|
}
|
||||||
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
||||||
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
||||||
SendError::NoMember(_) => write!(f, "no live member in the process group"),
|
SendError::NoMember(_) => write!(f, "no live member in the process group"),
|
||||||
@@ -243,7 +244,10 @@ struct Mailbox {
|
|||||||
|
|
||||||
impl Mailbox {
|
impl Mailbox {
|
||||||
fn new(pid: Pid) -> Self {
|
fn new(pid: Pid) -> Self {
|
||||||
Self { pid, channels: HashMap::new() }
|
Self {
|
||||||
|
pid,
|
||||||
|
channels: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
||||||
@@ -294,7 +298,10 @@ pub(crate) struct Registry {
|
|||||||
|
|
||||||
impl Registry {
|
impl Registry {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self { by_index: HashMap::new(), by_name: HashMap::new() }
|
Self {
|
||||||
|
by_index: HashMap::new(),
|
||||||
|
by_name: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop a dead holder's artifacts: every name bound to it, and its
|
/// Drop a dead holder's artifacts: every name bound to it, and its
|
||||||
@@ -303,7 +310,11 @@ impl Registry {
|
|||||||
/// wholesale on pid mismatch) and is left untouched.
|
/// wholesale on pid mismatch) and is left untouched.
|
||||||
fn prune_holder(&mut self, holder: Pid) {
|
fn prune_holder(&mut self, holder: Pid) {
|
||||||
self.by_name.retain(|_, p| *p != holder);
|
self.by_name.retain(|_, p| *p != holder);
|
||||||
if self.by_index.get(&holder.index()).is_some_and(|mb| mb.pid == holder) {
|
if self
|
||||||
|
.by_index
|
||||||
|
.get(&holder.index())
|
||||||
|
.is_some_and(|mb| mb.pid == holder)
|
||||||
|
{
|
||||||
self.by_index.remove(&holder.index());
|
self.by_index.remove(&holder.index());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +362,11 @@ impl Registry {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
|
.filter_map(|(&n, &p)| (p == mb.pid).then_some(n))
|
||||||
.collect();
|
.collect();
|
||||||
Some(MailboxInfo { pid: mb.pid, names, depth: depth.min(u32::MAX as usize) as u32 })
|
Some(MailboxInfo {
|
||||||
|
pid: mb.pid,
|
||||||
|
names,
|
||||||
|
depth: depth.min(u32::MAX as usize) as u32,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +403,16 @@ pub(crate) fn register_with<M: Send + 'static>(
|
|||||||
tx: Sender<M>,
|
tx: Sender<M>,
|
||||||
) -> Result<(), RegisterError> {
|
) -> Result<(), RegisterError> {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
|
// Stamp-eligibility for the terminal record (soak sig 4): flag the
|
||||||
|
// tenancy BEFORE the binding lands and outside the registry lock (no
|
||||||
|
// nesting), so no successfully-registered actor can die unflagged.
|
||||||
|
// A register that then fails leaves a harmless overshoot; a stale
|
||||||
|
// `me` is screened by the same live() the binding requires below.
|
||||||
|
if live(inner, me) {
|
||||||
|
if let Some(slot) = inner.slot_at(me) {
|
||||||
|
slot.cold.lock().watchable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
if !live(inner, me) {
|
if !live(inner, me) {
|
||||||
return Err(RegisterError::NoProc);
|
return Err(RegisterError::NoProc);
|
||||||
@@ -418,13 +443,19 @@ pub(crate) fn register_with<M: Send + 'static>(
|
|||||||
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
||||||
/// Caller holds the registry lock and has established that `me` is live.
|
/// Caller holds the registry lock and has established that `me` is live.
|
||||||
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
|
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
|
||||||
let mb = reg.by_index.entry(me.index()).or_insert_with(|| Mailbox::new(me));
|
let mb = reg
|
||||||
|
.by_index
|
||||||
|
.entry(me.index())
|
||||||
|
.or_insert_with(|| Mailbox::new(me));
|
||||||
if mb.pid != me {
|
if mb.pid != me {
|
||||||
*mb = Mailbox::new(me);
|
*mb = Mailbox::new(me);
|
||||||
}
|
}
|
||||||
mb.channels.insert(
|
mb.channels.insert(
|
||||||
TypeId::of::<M>(),
|
TypeId::of::<M>(),
|
||||||
Channel { sender: Box::new(tx), msg_type: type_name::<M>() },
|
Channel {
|
||||||
|
sender: Box::new(tx),
|
||||||
|
msg_type: type_name::<M>(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +494,10 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
|
|||||||
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
|
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
debug_assert!(live(inner, pid), "install_for: pid must be a freshly spawned, live actor");
|
debug_assert!(
|
||||||
|
live(inner, pid),
|
||||||
|
"install_for: pid must be a freshly spawned, live actor"
|
||||||
|
);
|
||||||
publish_channel::<M>(&mut reg, pid, tx);
|
publish_channel::<M>(&mut reg, pid, tx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -486,6 +520,47 @@ pub fn whereis(name: &str) -> Option<Pid> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a name is bound to, three-valued (bridge soak signature 4).
|
||||||
|
///
|
||||||
|
/// [`Live`](NameResolution::Live) is [`whereis`]'s `Some`.
|
||||||
|
/// [`Corpse`](NameResolution::Corpse) carries the *stored* holder pid of a
|
||||||
|
/// dead-but-unpruned binding — a state Erlang cannot represent (its name
|
||||||
|
/// death unregisters atomically; smarm's prune is lazy), captured here before
|
||||||
|
/// the prune that `whereis` performs discards it, so the caller can consult
|
||||||
|
/// [`terminal_reason`](crate::monitor::terminal_reason) for the tenancy's
|
||||||
|
/// real down reason. [`Unbound`](NameResolution::Unbound) matches Erlang's
|
||||||
|
/// unregistered name.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NameResolution {
|
||||||
|
/// The stored holder is live (generation-checked); the binding stands.
|
||||||
|
Live(Pid),
|
||||||
|
/// The stored holder is dead. The binding was pruned on the way out —
|
||||||
|
/// the name heals exactly as `whereis` heals it; only the evidence is
|
||||||
|
/// returned instead of discarded. A second resolve is `Unbound`.
|
||||||
|
Corpse(Pid),
|
||||||
|
/// No binding stored (never registered, or already pruned by any reader).
|
||||||
|
Unbound,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `name` like [`whereis`], but keep the corpse: the dead-holder arm
|
||||||
|
/// returns the stored pid it pruned instead of a bare `None`. Same lock
|
||||||
|
/// discipline and pruning behavior as `whereis`; same `Runtime::run()`
|
||||||
|
/// context contract.
|
||||||
|
pub fn resolve_name(name: &str) -> NameResolution {
|
||||||
|
with_runtime(|inner| {
|
||||||
|
let mut reg = inner.registry.lock();
|
||||||
|
let Some(&pid) = reg.by_name.get(name) else {
|
||||||
|
return NameResolution::Unbound;
|
||||||
|
};
|
||||||
|
if live(inner, pid) {
|
||||||
|
NameResolution::Live(pid)
|
||||||
|
} else {
|
||||||
|
reg.prune_holder(pid);
|
||||||
|
NameResolution::Corpse(pid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
||||||
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
||||||
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
||||||
@@ -522,7 +597,10 @@ pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid
|
|||||||
}
|
}
|
||||||
// A live holder's mailbox is its own (publish replaces wholesale on
|
// A live holder's mailbox is its own (publish replaces wholesale on
|
||||||
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
// pid mismatch, and one live actor per slot), so index lookup is safe.
|
||||||
let tx = reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>)?;
|
let tx = reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)?;
|
||||||
Some((pid, tx))
|
Some((pid, tx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -535,7 +613,11 @@ pub fn unregister(name: &str) -> Option<Pid> {
|
|||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let mut reg = inner.registry.lock();
|
let mut reg = inner.registry.lock();
|
||||||
let pid = reg.by_name.remove(name)?;
|
let pid = reg.by_name.remove(name)?;
|
||||||
if live(inner, pid) { Some(pid) } else { None }
|
if live(inner, pid) {
|
||||||
|
Some(pid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,12 +649,17 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
|
|||||||
reg.prune_holder(pid);
|
reg.prune_holder(pid);
|
||||||
return Err(SendError::Unresolved(msg));
|
return Err(SendError::Unresolved(msg));
|
||||||
}
|
}
|
||||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
match reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)
|
||||||
|
{
|
||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
None => return Err(SendError::NoChannel(msg)),
|
None => return Err(SendError::NoChannel(msg)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
tx.send(msg)
|
||||||
|
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +683,11 @@ fn send_to_pid<M: Send + 'static>(
|
|||||||
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
|
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
|
||||||
// Exact incarnation, still alive: its `M` channel, or NoChannel.
|
// Exact incarnation, still alive: its `M` channel, or NoChannel.
|
||||||
Some(stored) if stored == pid && live(inner, pid) => {
|
Some(stored) if stored == pid && live(inner, pid) => {
|
||||||
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
|
match reg
|
||||||
|
.by_index
|
||||||
|
.get(&pid.index())
|
||||||
|
.and_then(Mailbox::clone_sender::<M>)
|
||||||
|
{
|
||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
None => return Err(SendError::NoChannel(msg)),
|
None => return Err(SendError::NoChannel(msg)),
|
||||||
}
|
}
|
||||||
@@ -611,7 +702,8 @@ fn send_to_pid<M: Send + 'static>(
|
|||||||
_ => return Err(SendError::Dead(msg)),
|
_ => return Err(SendError::Dead(msg)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
tx.send(msg)
|
||||||
|
.map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
|
||||||
|
|||||||
+28
-5
@@ -222,7 +222,10 @@ impl MpmcRing {
|
|||||||
if diff == 0 {
|
if diff == 0 {
|
||||||
// Our turn: claim the position.
|
// Our turn: claim the position.
|
||||||
match self.enqueue_pos.0.compare_exchange_weak(
|
match self.enqueue_pos.0.compare_exchange_weak(
|
||||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
pos,
|
||||||
|
pos + 1,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
Ordering::Relaxed,
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// SAFETY: the claim gives us exclusive write access
|
// SAFETY: the claim gives us exclusive write access
|
||||||
@@ -250,7 +253,10 @@ impl MpmcRing {
|
|||||||
let diff = seq as isize - (pos + 1) as isize;
|
let diff = seq as isize - (pos + 1) as isize;
|
||||||
if diff == 0 {
|
if diff == 0 {
|
||||||
match self.dequeue_pos.0.compare_exchange_weak(
|
match self.dequeue_pos.0.compare_exchange_weak(
|
||||||
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
|
pos,
|
||||||
|
pos + 1,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
Ordering::Relaxed,
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// SAFETY: the claim gives us exclusive read access;
|
// SAFETY: the claim gives us exclusive read access;
|
||||||
@@ -464,19 +470,36 @@ mod tests {
|
|||||||
|
|
||||||
let popped = popped.lock().unwrap();
|
let popped = popped.lock().unwrap();
|
||||||
assert_eq!(popped.len(), total, "count mismatch");
|
assert_eq!(popped.len(), total, "count mismatch");
|
||||||
let set: HashSet<u64> = popped.iter().map(|p| ((p.index() as u64) << 32) | p.generation() as u64).collect();
|
let set: HashSet<u64> = popped
|
||||||
|
.iter()
|
||||||
|
.map(|p| ((p.index() as u64) << 32) | p.generation() as u64)
|
||||||
|
.collect();
|
||||||
assert_eq!(set.len(), total, "duplicate or lost element");
|
assert_eq!(set.len(), total, "duplicate or lost element");
|
||||||
assert_eq!(pop(&q), None);
|
assert_eq!(pop(&q), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mpmc_exactly_once_contended() {
|
fn mpmc_exactly_once_contended() {
|
||||||
exactly_once(MpmcRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
exactly_once(
|
||||||
|
MpmcRing::new(8, 4096),
|
||||||
|
|q, p| q.push(p),
|
||||||
|
|q| q.pop(),
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn striped_exactly_once_contended() {
|
fn striped_exactly_once_contended() {
|
||||||
exactly_once(StripedRing::new(8, 4096), |q, p| q.push(p), |q| q.pop(), 4, 4, 1000);
|
exactly_once(
|
||||||
|
StripedRing::new(8, 4096),
|
||||||
|
|q, p| q.push(p),
|
||||||
|
|q| q.pop(),
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+268
-86
@@ -112,10 +112,11 @@
|
|||||||
//! deadline, so an expiry wakes one scheduler, not a herd.
|
//! deadline, so an expiry wakes one scheduler, not a herd.
|
||||||
|
|
||||||
use crate::actor::{
|
use crate::actor::{
|
||||||
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box,
|
clear_current_pid, is_actor_done, reset_actor_done, set_current_actor_box, set_current_pid,
|
||||||
set_current_pid, take_last_outcome, Actor, Outcome,
|
take_last_outcome, Actor, Outcome,
|
||||||
};
|
};
|
||||||
use crate::channel::Sender;
|
use crate::channel::Sender;
|
||||||
|
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
||||||
use crate::io::IoThread;
|
use crate::io::IoThread;
|
||||||
use crate::monitor::{Down, DownReason, MonitorId};
|
use crate::monitor::{Down, DownReason, MonitorId};
|
||||||
use crate::pid::Pid;
|
use crate::pid::Pid;
|
||||||
@@ -124,12 +125,9 @@ use crate::raw_mutex::RawMutex;
|
|||||||
use crate::slot_state::{StateWord, Status, Unpark};
|
use crate::slot_state::{StateWord, Status, Unpark};
|
||||||
use crate::supervisor::Signal;
|
use crate::supervisor::Signal;
|
||||||
use crate::timer::Timers;
|
use crate::timer::Timers;
|
||||||
use crate::context::{get_actor_sp, set_actor_sp, switch_to_actor};
|
|
||||||
|
|
||||||
use std::sync::atomic::{
|
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||||
AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering,
|
use std::sync::{Arc, Mutex, Weak};
|
||||||
};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -175,7 +173,9 @@ impl Config {
|
|||||||
pub fn exact(n: usize) -> Self {
|
pub fn exact(n: usize) -> Self {
|
||||||
assert!(n >= 1, "scheduler thread count must be ≥ 1");
|
assert!(n >= 1, "scheduler thread count must be ≥ 1");
|
||||||
Self {
|
Self {
|
||||||
min: n, max: n, exact: Some(n),
|
min: n,
|
||||||
|
max: n,
|
||||||
|
exact: Some(n),
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: n * 4,
|
stack_pool_cap: n * 4,
|
||||||
@@ -196,7 +196,9 @@ impl Config {
|
|||||||
assert!(e >= 1, "exact must be ≥ 1");
|
assert!(e >= 1, "exact must be ≥ 1");
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
min, max, exact,
|
min,
|
||||||
|
max,
|
||||||
|
exact,
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: max * 4,
|
stack_pool_cap: max * 4,
|
||||||
@@ -322,7 +324,9 @@ impl Default for Config {
|
|||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
Self {
|
Self {
|
||||||
min: 1, max: avail, exact: None,
|
min: 1,
|
||||||
|
max: avail,
|
||||||
|
exact: None,
|
||||||
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
alloc_interval: crate::preempt::DEFAULT_ALLOC_INTERVAL,
|
||||||
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
timeslice_cycles: crate::preempt::DEFAULT_TIMESLICE_CYCLES,
|
||||||
stack_pool_cap: avail * 4,
|
stack_pool_cap: avail * 4,
|
||||||
@@ -376,7 +380,9 @@ pub struct RuntimeStats {
|
|||||||
impl RuntimeStats {
|
impl RuntimeStats {
|
||||||
/// Sum of run queue lengths across all scheduler threads.
|
/// Sum of run queue lengths across all scheduler threads.
|
||||||
pub fn total_run_queue_len(&self) -> u64 {
|
pub fn total_run_queue_len(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.run_queue_len.load(Ordering::Relaxed))
|
.map(|s| s.run_queue_len.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -400,7 +406,9 @@ impl RuntimeStats {
|
|||||||
/// scheduler threads. Counters are reset at the start of each `run()`,
|
/// scheduler threads. Counters are reset at the start of each `run()`,
|
||||||
/// so after a run this reads that run's total.
|
/// so after a run this reads that run's total.
|
||||||
pub fn slot_hits(&self) -> u64 {
|
pub fn slot_hits(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.slot_hits.load(Ordering::Relaxed))
|
.map(|s| s.slot_hits.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -408,7 +416,9 @@ impl RuntimeStats {
|
|||||||
/// RFC 005: total slot occupants displaced to the shared queue, summed
|
/// RFC 005: total slot occupants displaced to the shared queue, summed
|
||||||
/// across scheduler threads. Reset at the start of each `run()`.
|
/// across scheduler threads. Reset at the start of each `run()`.
|
||||||
pub fn slot_displacements(&self) -> u64 {
|
pub fn slot_displacements(&self) -> u64 {
|
||||||
self.inner.stats.iter()
|
self.inner
|
||||||
|
.stats
|
||||||
|
.iter()
|
||||||
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
.map(|s| s.slot_displacements.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
@@ -472,6 +482,28 @@ pub(crate) struct SlotCold {
|
|||||||
/// epoch-matched unpark.
|
/// epoch-matched unpark.
|
||||||
pub(crate) waiters: Vec<(Pid, u32)>,
|
pub(crate) waiters: Vec<(Pid, u32)>,
|
||||||
pub(crate) outcome: Option<Outcome>,
|
pub(crate) outcome: Option<Outcome>,
|
||||||
|
/// The slot's most recent *watchable-tenancy* death: `(generation,
|
||||||
|
/// reason)`, stamped by `finalize_actor` — but only for a tenancy whose
|
||||||
|
/// `watchable` bit was set — and deliberately never cleared: a new
|
||||||
|
/// tenant's install leaves it standing (it describes the previous
|
||||||
|
/// tenancy), and only the next *watchable* death overwrites it.
|
||||||
|
/// Anonymous green-thread churn must not evict it: the free list is
|
||||||
|
/// LIFO, so the just-freed slot is the first recycled, and an
|
||||||
|
/// unconditional stamp made a watchable tenancy's record the
|
||||||
|
/// shortest-lived data in the runtime. Read generation-matched via
|
||||||
|
/// [`terminal_reason`](crate::monitor::terminal_reason), so a watch that
|
||||||
|
/// raced its target's death can recover the real down reason instead of
|
||||||
|
/// a blanket `NoProc` (bridge soak signatures 4 and 5).
|
||||||
|
pub(crate) terminal: Option<(u32, DownReason)>,
|
||||||
|
/// Stamp eligibility for `terminal` above: someone could plausibly hold
|
||||||
|
/// a watch on this tenancy. Two set-sites, both while the tenancy is
|
||||||
|
/// live: `register_with` *before* the binding lands (no successfully
|
||||||
|
/// registered actor can die unflagged; a failed register's overshoot is
|
||||||
|
/// harmless), and [`mark_watchable`](crate::monitor::mark_watchable) —
|
||||||
|
/// the bridge calls it wherever a pid is encoded across the boundary,
|
||||||
|
/// because BEAM can only watch pids it holds and can only hold pids
|
||||||
|
/// that crossed. Reset at reclaim.
|
||||||
|
pub(crate) watchable: bool,
|
||||||
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
||||||
/// Watchers registered via `monitor()`, each tagged with its
|
/// Watchers registered via `monitor()`, each tagged with its
|
||||||
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
||||||
@@ -626,6 +658,8 @@ impl Slot {
|
|||||||
actor: None,
|
actor: None,
|
||||||
waiters: Vec::new(),
|
waiters: Vec::new(),
|
||||||
outcome: None,
|
outcome: None,
|
||||||
|
terminal: None,
|
||||||
|
watchable: false,
|
||||||
supervisor_channel: None,
|
supervisor_channel: None,
|
||||||
monitors: Vec::new(),
|
monitors: Vec::new(),
|
||||||
links: Vec::new(),
|
links: Vec::new(),
|
||||||
@@ -689,7 +723,8 @@ impl Slot {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn record_message(&self) {
|
pub(crate) fn record_message(&self) {
|
||||||
let v = self.messages_received.load(Ordering::Relaxed);
|
let v = self.messages_received.load(Ordering::Relaxed);
|
||||||
self.messages_received.store(v.wrapping_add(1), Ordering::Relaxed);
|
self.messages_received
|
||||||
|
.store(v.wrapping_add(1), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the received-message tally (Relaxed; cross-thread snapshot read).
|
/// Read the received-message tally (Relaxed; cross-thread snapshot read).
|
||||||
@@ -708,7 +743,8 @@ impl Slot {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn add_budget(&self, cycles: u64) {
|
pub(crate) fn add_budget(&self, cycles: u64) {
|
||||||
let v = self.budget_cycles.load(Ordering::Relaxed);
|
let v = self.budget_cycles.load(Ordering::Relaxed);
|
||||||
self.budget_cycles.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
self.budget_cycles
|
||||||
|
.store(v.wrapping_add(cycles), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the accumulated budget cycles (Relaxed). Always 0 unless the
|
/// Read the accumulated budget cycles (Relaxed). Always 0 unless the
|
||||||
@@ -847,7 +883,6 @@ impl Slot {
|
|||||||
Some(*unsafe { Box::from_raw(raw) })
|
Some(*unsafe { Box::from_raw(raw) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -869,17 +904,10 @@ pub(crate) struct RuntimeInner {
|
|||||||
pub(crate) live_actors: AtomicU32,
|
pub(crate) live_actors: AtomicU32,
|
||||||
/// Packed `(index << 32 | generation)` of the run's root (initial) actor,
|
/// Packed `(index << 32 | generation)` of the run's root (initial) actor,
|
||||||
/// or `u64::MAX` (the ROOT_PID sentinel) before one is set. When this actor
|
/// or `u64::MAX` (the ROOT_PID sentinel) before one is set. When this actor
|
||||||
/// finalizes it flags `root_exited`; the scheduler's idle verdict then
|
/// finalizes, `finalize_actor` runs the root-exit shutdown (see
|
||||||
/// stops the remaining (parked-forever) actors. Set once per `run()`, right
|
/// `shutdown_forest_roots`). Set once per `run()`, right after the initial
|
||||||
/// after the initial spawn.
|
/// spawn.
|
||||||
pub(crate) root_bits: AtomicU64,
|
pub(crate) root_bits: AtomicU64,
|
||||||
/// Set when the root actor finalizes; read by the scheduler's idle verdict
|
|
||||||
/// to trigger the one-shot teardown sweep. Reset per `run()`.
|
|
||||||
pub(crate) root_exited: AtomicBool,
|
|
||||||
/// Guards the teardown sweep to fire at most once per run (a parked-forever
|
|
||||||
/// remainder that survives the sweep falls through to the normal idle wait
|
|
||||||
/// rather than busy-spinning). Reset per `run()`.
|
|
||||||
pub(crate) root_swept: AtomicBool,
|
|
||||||
/// Timer heap. Independent lock: never nested with any other.
|
/// Timer heap. Independent lock: never nested with any other.
|
||||||
pub(crate) timers: Mutex<Timers>,
|
pub(crate) timers: Mutex<Timers>,
|
||||||
/// IO subsystem. `None` between runs. Lock order: io before everything.
|
/// IO subsystem. `None` between runs. Lock order: io before everything.
|
||||||
@@ -969,8 +997,6 @@ impl RuntimeInner {
|
|||||||
free: RawMutex::new(free),
|
free: RawMutex::new(free),
|
||||||
live_actors: AtomicU32::new(0),
|
live_actors: AtomicU32::new(0),
|
||||||
root_bits: AtomicU64::new(u64::MAX),
|
root_bits: AtomicU64::new(u64::MAX),
|
||||||
root_exited: AtomicBool::new(false),
|
|
||||||
root_swept: AtomicBool::new(false),
|
|
||||||
timers: Mutex::new(timers),
|
timers: Mutex::new(timers),
|
||||||
io: Mutex::new(None),
|
io: Mutex::new(None),
|
||||||
next_monitor_id: AtomicU64::new(0),
|
next_monitor_id: AtomicU64::new(0),
|
||||||
@@ -1153,10 +1179,29 @@ impl RuntimeInner {
|
|||||||
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1)
|
MonitorId(self.next_monitor_id.fetch_add(1, Ordering::Relaxed) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pop a vacant slot index, or `None` when the slab is full. The claim
|
||||||
|
/// is atomic — a single pop under the free-list lock — so callers get
|
||||||
|
/// claim-or-report semantics with no check-then-spawn TOCTOU: whoever
|
||||||
|
/// gets `Some` owns that slot, full stop.
|
||||||
|
pub(crate) fn try_allocate_slot(&self) -> Option<u32> {
|
||||||
|
self.free.lock().pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a slot claimed by [`try_allocate_slot`](Self::try_allocate_slot)
|
||||||
|
/// that never had an actor installed into it (e.g. stack allocation
|
||||||
|
/// panicked between claim and install). NOT for dead actors — their
|
||||||
|
/// slots go back through `reclaim_slot`, which handles generation bump,
|
||||||
|
/// waiter/monitor/link teardown, and stack recycling.
|
||||||
|
pub(crate) fn return_vacant_slot(&self, idx: u32) {
|
||||||
|
self.free.lock().push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
|
/// Pop a vacant slot index, or die loudly. The fixed slab is a deliberate
|
||||||
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix.
|
/// v0.5 simplification (ROADMAP: "Deferred"); the panic names the fix.
|
||||||
|
/// Callers that can shed load instead use [`try_allocate_slot`]
|
||||||
|
/// (Self::try_allocate_slot) via `scheduler::try_spawn`.
|
||||||
pub(crate) fn allocate_slot(&self) -> u32 {
|
pub(crate) fn allocate_slot(&self) -> u32 {
|
||||||
match self.free.lock().pop() {
|
match self.try_allocate_slot() {
|
||||||
Some(idx) => idx,
|
Some(idx) => idx,
|
||||||
None => panic!(
|
None => panic!(
|
||||||
"smarm: actor slot table exhausted — {} actors are live \
|
"smarm: actor slot table exhausted — {} actors are live \
|
||||||
@@ -1246,11 +1291,13 @@ impl Runtime {
|
|||||||
|
|
||||||
// Re-initialise shared state for this run.
|
// Re-initialise shared state for this run.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.inner.run_queue.len(), 0,
|
self.inner.run_queue.len(),
|
||||||
|
0,
|
||||||
"run() called while previous run still active"
|
"run() called while previous run still active"
|
||||||
);
|
);
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
self.inner.live_actors.load(Ordering::Acquire), 0,
|
self.inner.live_actors.load(Ordering::Acquire),
|
||||||
|
0,
|
||||||
"run() called while previous run still active"
|
"run() called while previous run still active"
|
||||||
);
|
);
|
||||||
// RFC 018: the IO producers reach the runtime (slot table + unpark)
|
// RFC 018: the IO producers reach the runtime (slot table + unpark)
|
||||||
@@ -1282,10 +1329,9 @@ impl Runtime {
|
|||||||
// requires a running runtime in the thread-local).
|
// requires a running runtime in the thread-local).
|
||||||
RUNTIME.with(|r| *r.borrow_mut() = Some(self.inner.clone()));
|
RUNTIME.with(|r| *r.borrow_mut() = Some(self.inner.clone()));
|
||||||
let initial_handle = crate::scheduler::spawn(f);
|
let initial_handle = crate::scheduler::spawn(f);
|
||||||
// The initial actor is the run's root: when it exits, remaining actors
|
// The initial actor is the run's root: its exit means "the program is
|
||||||
// are stopped so the run winds down (see finalize_actor / schedule_loop).
|
// done" — every remaining top-level actor is asked to shut down (see
|
||||||
self.inner.root_exited.store(false, Ordering::Relaxed);
|
// `finalize_actor` / `shutdown_forest_roots`).
|
||||||
self.inner.root_swept.store(false, Ordering::Relaxed);
|
|
||||||
self.inner.set_root(initial_handle.pid());
|
self.inner.set_root(initial_handle.pid());
|
||||||
|
|
||||||
// Launch N-1 extra scheduler threads, named `smarm-sched-{slot}` so
|
// Launch N-1 extra scheduler threads, named `smarm-sched-{slot}` so
|
||||||
@@ -1397,7 +1443,74 @@ impl Runtime {
|
|||||||
|
|
||||||
/// Snapshot of runtime statistics for introspection / tests.
|
/// Snapshot of runtime statistics for introspection / tests.
|
||||||
pub fn stats(&self) -> RuntimeStats {
|
pub fn stats(&self) -> RuntimeStats {
|
||||||
RuntimeStats { inner: self.inner.clone() }
|
RuntimeStats {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `Send + Sync` handle to this runtime, usable from any thread —
|
||||||
|
/// including threads that are not smarm schedulers (an OS-signal handler
|
||||||
|
/// thread, an external event source). Grab it *before* [`run`](Self::run)
|
||||||
|
/// and hand it to e.g. a signal thread; that thread can then
|
||||||
|
/// [`request_stop`](RuntimeHandle::request_stop) the runtime's top
|
||||||
|
/// supervisor to drive an ordered shutdown from outside the runtime.
|
||||||
|
///
|
||||||
|
/// The in-runtime primitives ([`scheduler::request_stop`](crate::request_stop)
|
||||||
|
/// and friends) reach the runtime through a thread-local that is unset on
|
||||||
|
/// any non-scheduler thread, so they are silent no-ops off-runtime; this
|
||||||
|
/// handle carries its own reference and closes that gap.
|
||||||
|
pub fn handle(&self) -> RuntimeHandle {
|
||||||
|
RuntimeHandle {
|
||||||
|
inner: Arc::downgrade(&self.inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RuntimeHandle — off-runtime wake/stop
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A `Send + Sync` handle to a [`Runtime`], obtained from
|
||||||
|
/// [`Runtime::handle`]. Lets a thread that is *not* a smarm scheduler thread
|
||||||
|
/// drive a cooperative stop into the runtime — the off-runtime counterpart to
|
||||||
|
/// [`scheduler::request_stop`](crate::request_stop).
|
||||||
|
///
|
||||||
|
/// Holds a [`Weak`] to the runtime, for the same reason the IO backend does
|
||||||
|
/// (RFC 018): a lingering handle can never keep the runtime's slot table alive
|
||||||
|
/// and can never block [`Runtime::run`] from finishing. Once the `Runtime` is
|
||||||
|
/// dropped every method is a harmless no-op — the same end state as calling
|
||||||
|
/// `request_stop` on an actor that has already exited.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RuntimeHandle {
|
||||||
|
inner: Weak<RuntimeInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeHandle {
|
||||||
|
/// Ask `pid` to stop cooperatively, from any thread. The off-runtime
|
||||||
|
/// equivalent of [`scheduler::request_stop`](crate::request_stop): it sets
|
||||||
|
/// the target's stop flag and wakes it, so a parked actor unwinds at its
|
||||||
|
/// next checkpoint exactly as it would for an in-runtime stop. A no-op if
|
||||||
|
/// the runtime has been dropped, or if the actor has already exited.
|
||||||
|
pub fn request_stop<A>(&self, pid: Pid<A>) {
|
||||||
|
let pid = pid.erase();
|
||||||
|
// Upgrade the Weak per call, like the IO backend does (io.rs): a live
|
||||||
|
// runtime yields the inner and we drive the same stop the in-runtime
|
||||||
|
// path would; a dropped runtime makes this a no-op.
|
||||||
|
if let Some(inner) = self.inner.upgrade() {
|
||||||
|
crate::scheduler::request_stop_inner(&inner, pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask `pid` to shut down gracefully, from any thread. The off-runtime
|
||||||
|
/// equivalent of [`scheduler::request_shutdown`](crate::request_shutdown);
|
||||||
|
/// the delivered [`ExitSignal`](crate::ExitSignal) carries `from ==
|
||||||
|
/// ROOT_PID`, since no actor made the request. A no-op if the runtime has
|
||||||
|
/// been dropped, or if the actor has already exited.
|
||||||
|
pub fn request_shutdown<A>(&self, pid: Pid<A>) {
|
||||||
|
let pid = pid.erase();
|
||||||
|
if let Some(inner) = self.inner.upgrade() {
|
||||||
|
crate::scheduler::request_shutdown_inner(&inner, pid, ROOT_PID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1432,7 +1545,10 @@ thread_local! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub(crate) enum YieldIntent { Yield, Park }
|
pub(crate) enum YieldIntent {
|
||||||
|
Yield,
|
||||||
|
Park,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_yield_intent(i: YieldIntent) {
|
pub(crate) fn set_yield_intent(i: YieldIntent) {
|
||||||
YIELD_INTENT.with(|c| c.set(i));
|
YIELD_INTENT.with(|c| c.set(i));
|
||||||
@@ -1467,7 +1583,10 @@ pub const ROOT_PID: Pid = Pid::new(u32::MAX, u32::MAX);
|
|||||||
/// for writes after reclaim — so an over-eager mark costs a cancel-write,
|
/// for writes after reclaim — so an over-eager mark costs a cancel-write,
|
||||||
/// never data.
|
/// never data.
|
||||||
fn maybe_shrink_stack(slot: &Slot) {
|
fn maybe_shrink_stack(slot: &Slot) {
|
||||||
let parks = slot.parks_since_shrink.load(Ordering::Relaxed).saturating_add(1);
|
let parks = slot
|
||||||
|
.parks_since_shrink
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
.saturating_add(1);
|
||||||
slot.parks_since_shrink.store(parks, Ordering::Relaxed);
|
slot.parks_since_shrink.store(parks, Ordering::Relaxed);
|
||||||
|
|
||||||
let sp = slot.sp.load(Ordering::Relaxed);
|
let sp = slot.sp.load(Ordering::Relaxed);
|
||||||
@@ -1566,12 +1685,19 @@ pub(crate) fn install_actor(
|
|||||||
// publish below.
|
// publish below.
|
||||||
let (diag_reserve, diag_guard) = stack.shape();
|
let (diag_reserve, diag_guard) = stack.shape();
|
||||||
let diag_top = stack.top() as usize;
|
let diag_top = stack.top() as usize;
|
||||||
slot.stop_ptr.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
slot.stop_ptr
|
||||||
|
.store(Arc::as_ptr(&stop) as *mut _, Ordering::Release);
|
||||||
{
|
{
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
debug_assert!(cold.actor.is_none(), "install over live actor");
|
debug_assert!(cold.actor.is_none(), "install over live actor");
|
||||||
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty());
|
debug_assert!(cold.waiters.is_empty() && cold.monitors.is_empty() && cold.links.is_empty());
|
||||||
cold.actor = Some(Actor { pid, stack, supervisor, stop, trap: None });
|
cold.actor = Some(Actor {
|
||||||
|
pid,
|
||||||
|
stack,
|
||||||
|
supervisor,
|
||||||
|
stop,
|
||||||
|
trap: None,
|
||||||
|
});
|
||||||
cold.outstanding_handles = 1;
|
cold.outstanding_handles = 1;
|
||||||
cold.outcome = None;
|
cold.outcome = None;
|
||||||
cold.pending_io_result = None;
|
cold.pending_io_result = None;
|
||||||
@@ -1583,9 +1709,11 @@ pub(crate) fn install_actor(
|
|||||||
slot.parks_since_shrink.store(0, Ordering::Relaxed);
|
slot.parks_since_shrink.store(0, Ordering::Relaxed);
|
||||||
slot.shrink_count.store(0, Ordering::Relaxed);
|
slot.shrink_count.store(0, Ordering::Relaxed);
|
||||||
slot.diag_stack_top.store(diag_top, Ordering::Relaxed);
|
slot.diag_stack_top.store(diag_top, Ordering::Relaxed);
|
||||||
slot.diag_stack_reserve.store(diag_reserve, Ordering::Relaxed);
|
slot.diag_stack_reserve
|
||||||
|
.store(diag_reserve, Ordering::Relaxed);
|
||||||
slot.diag_stack_guard.store(diag_guard, Ordering::Relaxed);
|
slot.diag_stack_guard.store(diag_guard, Ordering::Relaxed);
|
||||||
slot.diag_pid.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
slot.diag_pid
|
||||||
|
.store(((idx as u64) << 32) | gen as u64, Ordering::Relaxed);
|
||||||
slot.store_closure(closure);
|
slot.store_closure(closure);
|
||||||
slot.reset_counters();
|
slot.reset_counters();
|
||||||
inner.live_actors.fetch_add(1, Ordering::Relaxed);
|
inner.live_actors.fetch_add(1, Ordering::Relaxed);
|
||||||
@@ -1594,7 +1722,10 @@ pub(crate) fn install_actor(
|
|||||||
// Release store orders everything above before any Acquire reader.
|
// Release store orders everything above before any Acquire reader.
|
||||||
slot.word.publish_queued(gen);
|
slot.word.publish_queued(gen);
|
||||||
inner.enqueue(pid);
|
inner.enqueue(pid);
|
||||||
crate::te!(crate::trace::Event::Spawn { parent: supervisor, child: pid });
|
crate::te!(crate::trace::Event::Spawn {
|
||||||
|
parent: supervisor,
|
||||||
|
child: pid
|
||||||
|
});
|
||||||
pid
|
pid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1611,14 +1742,19 @@ pub(crate) fn install_actor(
|
|||||||
/// is released — a last-sender drop can unpark a receiver, which takes the
|
/// is released — a last-sender drop can unpark a receiver, which takes the
|
||||||
/// run-queue mutex; legal under a cold lock, but pointless to nest.
|
/// run-queue mutex; legal under a cold lock, but pointless to nest.
|
||||||
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
||||||
let Some(slot) = inner.slot_at(pid) else { return };
|
let Some(slot) = inner.slot_at(pid) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let dropped_outside;
|
let dropped_outside;
|
||||||
{
|
{
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
|
if slot.status_for(pid) != Status::Done || cold.outstanding_handles != 0 {
|
||||||
return; // already reclaimed, or not yet eligible
|
return; // already reclaimed, or not yet eligible
|
||||||
}
|
}
|
||||||
debug_assert!(cold.actor.is_none(), "reclaiming a slot that still owns an actor");
|
debug_assert!(
|
||||||
|
cold.actor.is_none(),
|
||||||
|
"reclaiming a slot that still owns an actor"
|
||||||
|
);
|
||||||
dropped_outside = (
|
dropped_outside = (
|
||||||
cold.outcome.take(),
|
cold.outcome.take(),
|
||||||
cold.supervisor_channel.take(),
|
cold.supervisor_channel.take(),
|
||||||
@@ -1628,6 +1764,7 @@ pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
|||||||
cold.waiters.clear();
|
cold.waiters.clear();
|
||||||
cold.monitors.clear();
|
cold.monitors.clear();
|
||||||
cold.links.clear();
|
cold.links.clear();
|
||||||
|
cold.watchable = false;
|
||||||
slot.reset_counters();
|
slot.reset_counters();
|
||||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||||
// The generation bump IS the reclaim: every stale pid is dead from
|
// The generation bump IS the reclaim: every stale pid is dead from
|
||||||
@@ -1667,6 +1804,18 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
None => panic!("finalize_actor: actor vanished"),
|
None => panic!("finalize_actor: actor vanished"),
|
||||||
};
|
};
|
||||||
cold.outcome = Some(joiner_outcome);
|
cold.outcome = Some(joiner_outcome);
|
||||||
|
// Terminal record (soak sig 4): stamped before the generation ever
|
||||||
|
// bumps, under the cold lock, so a reader that resolved this pid can
|
||||||
|
// recover the reason after the slot moves on — but only for a
|
||||||
|
// tenancy that ever held a name. The free list is LIFO, so the slot
|
||||||
|
// this death frees is the very next one recycled; if every green
|
||||||
|
// thread's exit stamped too, the churn behind any real workload
|
||||||
|
// would evict a watchable tenancy's record in well under the race
|
||||||
|
// window this exists to cover. Overwritten only by the slot's next
|
||||||
|
// *watchable* death.
|
||||||
|
if cold.watchable {
|
||||||
|
cold.terminal = Some((pid.generation(), down_reason));
|
||||||
|
}
|
||||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||||
// Done is published under the cold lock, so join's
|
// Done is published under the cold lock, so join's
|
||||||
// check-Done-or-register-waiter (also under it) can never miss: it
|
// check-Done-or-register-waiter (also under it) can never miss: it
|
||||||
@@ -1704,7 +1853,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
// Notify monitors. Sent outside any slot lock: `send` may unpark a parked
|
// Notify monitors. Sent outside any slot lock: `send` may unpark a parked
|
||||||
// receiver, which takes the run-queue mutex.
|
// receiver, which takes the run-queue mutex.
|
||||||
for (_, m) in monitors {
|
for (_, m) in monitors {
|
||||||
let _ = m.send(Down { pid, reason: down_reason });
|
let _ = m.send(Down {
|
||||||
|
pid,
|
||||||
|
reason: down_reason,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk linked peers ONE AT A TIME (cold locks are leaves). For every
|
// Walk linked peers ONE AT A TIME (cold locks are leaves). For every
|
||||||
@@ -1736,7 +1888,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
};
|
};
|
||||||
match trap {
|
match trap {
|
||||||
Some(Some(tx)) => {
|
Some(Some(tx)) => {
|
||||||
let _ = tx.send(crate::link::ExitSignal { from: pid, reason: down_reason });
|
let _ = tx.send(crate::link::ExitSignal {
|
||||||
|
from: pid,
|
||||||
|
reason: down_reason,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Some(None) => crate::scheduler::request_stop(peer),
|
Some(None) => crate::scheduler::request_stop(peer),
|
||||||
None => {}
|
None => {}
|
||||||
@@ -1751,14 +1906,12 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
// Reclaim if no outstanding handles (re-verified inside).
|
// Reclaim if no outstanding handles (re-verified inside).
|
||||||
reclaim_slot(inner, pid);
|
reclaim_slot(inner, pid);
|
||||||
|
|
||||||
// Root-exit teardown is DEFERRED to the scheduler's idle verdict, not done
|
// Root exit = the program is done. Ask every top-level survivor to shut
|
||||||
// here: stopping eagerly would cut off actors that still have queued work
|
// down, right here, before the live-count decrement below: any wake this
|
||||||
// (they'd unwind on the stop before draining their mailbox). Flagging it
|
// produces is then ordered before `live_actors` can be observed at its
|
||||||
// instead lets the run queue drain naturally first; only the parked-forever
|
// decremented value, same as every other wakeup finalize issues.
|
||||||
// remainder (e.g. a server pinned alive by a registered name) is then
|
|
||||||
// stopped, once nothing runnable is left. See `schedule_loop`.
|
|
||||||
if inner.is_root(pid) {
|
if inner.is_root(pid) {
|
||||||
inner.root_exited.store(true, Ordering::Release);
|
shutdown_forest_roots(inner, pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The decrement is LAST: every wakeup this finalize produced (joiners,
|
// The decrement is LAST: every wakeup this finalize produced (joiners,
|
||||||
@@ -1769,16 +1922,62 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
|||||||
debug_assert!(prev >= 1, "live_actors underflow — double finalize");
|
debug_assert!(prev >= 1, "live_actors underflow — double finalize");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cooperatively stop every live actor — the root-exit teardown sweep, run from
|
/// The root-exit shutdown. Delivers [`request_shutdown`](crate::request_shutdown)
|
||||||
/// `schedule_loop` once the run queue is empty after the root has exited. Each
|
/// to every **forest root**: each live actor whose recorded parent
|
||||||
/// [`request_stop_inner`](crate::scheduler::request_stop_inner) re-verifies the
|
/// (`Actor::supervisor` — the spawner for a plain `spawn`, the supervisor for
|
||||||
/// target under its cold lock, so the racy per-slot generation read is safe: a
|
/// `spawn_under`) is the run itself (`ROOT_PID`) or is no longer live. Actors
|
||||||
/// vacant, dead, or reused slot no-ops. The swept actors unpark, unwind at their
|
/// under a live parent are not addressed — that parent is responsible for
|
||||||
/// next observation point, and finalize, dropping `live_actors` to zero.
|
/// them: a supervisor traps and runs its ordered, policy-driven shutdown; a
|
||||||
fn stop_live_actors(inner: &Arc<RuntimeInner>) {
|
/// bare parent that dies takes non-trapping children with it via the next
|
||||||
|
/// pass of this same rule only if it dies *now*, so a parent that outlives
|
||||||
|
/// this scan and later dies leaves its subtree to itself (Erlang semantics: an
|
||||||
|
/// unlinked spawn is nobody's child).
|
||||||
|
///
|
||||||
|
/// Semantics per target follow `request_shutdown`: a trapping actor receives
|
||||||
|
/// `ExitSignal { from: root, reason: Shutdown }` and may finish work — drain,
|
||||||
|
/// keep its timers ticking, then stop itself; a non-trapping one is stopped
|
||||||
|
/// outright. There is no second, forcing sweep: an actor that traps and never
|
||||||
|
/// stops keeps the run alive by design (put it under a supervisor with a
|
||||||
|
/// `Shutdown::Timeout` policy if that is not wanted). Runs once, on the root's
|
||||||
|
/// finalize path, so it races only against actors that are still running —
|
||||||
|
/// each `request_shutdown_inner` re-verifies its target under the cold lock,
|
||||||
|
/// so a slot that dies or is reused mid-scan is a no-op.
|
||||||
|
fn shutdown_forest_roots(inner: &Arc<RuntimeInner>, root: Pid) {
|
||||||
for idx in 0..inner.slots.len() as u32 {
|
for idx in 0..inner.slots.len() as u32 {
|
||||||
let pid = Pid::new(idx, inner.slots[idx as usize].generation());
|
let slot = &inner.slots[idx as usize];
|
||||||
crate::scheduler::request_stop_inner(inner, pid);
|
let pid = Pid::new(idx, slot.generation());
|
||||||
|
if pid == root {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Read the parent under the cold lock (generation-verified); act
|
||||||
|
// outside it — `request_shutdown_inner` sends and may unpark.
|
||||||
|
let parent = {
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
if slot.generation() != pid.generation() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match cold.actor.as_ref() {
|
||||||
|
Some(a) => a.supervisor,
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let parent_live = inner
|
||||||
|
.slot_at(parent)
|
||||||
|
.is_some_and(|ps| ps.is_live_for(parent));
|
||||||
|
if !parent_live {
|
||||||
|
// `_probe` so the trace can say what each leftover was: under
|
||||||
|
// `smarm-trace` every swept actor is a `root_sweep` line — the
|
||||||
|
// visibility that makes "a forgotten actor costs only a slot, never
|
||||||
|
// a hung run" a checkable claim rather than a hope.
|
||||||
|
let found = crate::scheduler::request_shutdown_inner_probe(inner, pid, root);
|
||||||
|
#[cfg_attr(not(feature = "smarm-trace"), allow(unused_variables))]
|
||||||
|
if let Some(trapping) = found {
|
||||||
|
crate::te!(crate::trace::Event::RootSweep {
|
||||||
|
target: pid,
|
||||||
|
trapping
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1862,9 +2061,6 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
Got(Pid),
|
Got(Pid),
|
||||||
Idle,
|
Idle,
|
||||||
AllDone,
|
AllDone,
|
||||||
/// Root has exited and nothing is runnable: stop the parked-forever
|
|
||||||
/// remainder, then re-pop. Fires at most once per run.
|
|
||||||
RootDrain,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2a. RFC 005: drain this thread's wake slot before touching the
|
// 2a. RFC 005: drain this thread's wake slot before touching the
|
||||||
@@ -1892,7 +2088,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
let io_out = inner.io_outstanding.load(Ordering::Acquire)
|
let io_out = inner.io_outstanding.load(Ordering::Acquire)
|
||||||
+ inner.io_fd_waiters.load(Ordering::Acquire);
|
+ inner.io_fd_waiters.load(Ordering::Acquire);
|
||||||
|
|
||||||
stats.run_queue_len.store(inner.run_queue.len(), Ordering::Relaxed);
|
stats
|
||||||
|
.run_queue_len
|
||||||
|
.store(inner.run_queue.len(), Ordering::Relaxed);
|
||||||
let pop = match inner.run_queue.pop() {
|
let pop = match inner.run_queue.pop() {
|
||||||
Some(pid) => Pop::Got(pid),
|
Some(pid) => Pop::Got(pid),
|
||||||
None => {
|
None => {
|
||||||
@@ -1912,15 +2110,6 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
let live = inner.live_actors.load(Ordering::Acquire);
|
let live = inner.live_actors.load(Ordering::Acquire);
|
||||||
if live == 0 && io_out == 0 {
|
if live == 0 && io_out == 0 {
|
||||||
Pop::AllDone
|
Pop::AllDone
|
||||||
} else if inner.root_exited.load(Ordering::Acquire)
|
|
||||||
&& !inner.root_swept.swap(true, Ordering::AcqRel)
|
|
||||||
{
|
|
||||||
// Root gone and nothing runnable — the live remainder
|
|
||||||
// are parked-forever daemons (Queued actors with pending
|
|
||||||
// work drained before the queue emptied). Stop them so
|
|
||||||
// the run can end. One-shot: a survivor falls through to
|
|
||||||
// the idle wait below on the next pass.
|
|
||||||
Pop::RootDrain
|
|
||||||
} else {
|
} else {
|
||||||
Pop::Idle
|
Pop::Idle
|
||||||
}
|
}
|
||||||
@@ -1948,13 +2137,6 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
inner.coord.wake_all();
|
inner.coord.wake_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Pop::RootDrain => {
|
|
||||||
// Root has exited and nothing is runnable: stop the
|
|
||||||
// parked-forever remainder, then loop back to re-pop the
|
|
||||||
// now-runnable (stopping) actors.
|
|
||||||
stop_live_actors(inner);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Pop::Idle => {
|
Pop::Idle => {
|
||||||
// Something is still in flight. Park on our own futex
|
// Something is still in flight. Park on our own futex
|
||||||
// until a producer wakes us (enqueue tail), a deadline
|
// until a producer wakes us (enqueue tail), a deadline
|
||||||
@@ -1985,8 +2167,6 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
|| (inner.live_actors.load(Ordering::Acquire) == 0
|
|| (inner.live_actors.load(Ordering::Acquire) == 0
|
||||||
&& inner.io_outstanding.load(Ordering::Acquire) == 0
|
&& inner.io_outstanding.load(Ordering::Acquire) == 0
|
||||||
&& inner.io_fd_waiters.load(Ordering::Acquire) == 0)
|
&& inner.io_fd_waiters.load(Ordering::Acquire) == 0)
|
||||||
|| (inner.root_exited.load(Ordering::Acquire)
|
|
||||||
&& !inner.root_swept.load(Ordering::Acquire))
|
|
||||||
|| inner.coord.deadline_due()
|
|| inner.coord.deadline_due()
|
||||||
});
|
});
|
||||||
if tk_deadline.is_some() {
|
if tk_deadline.is_some() {
|
||||||
@@ -2044,7 +2224,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update per-thread stats: record who's on-CPU.
|
// Update per-thread stats: record who's on-CPU.
|
||||||
stats.current_pid_index.store(pid.index(), Ordering::Relaxed);
|
stats
|
||||||
|
.current_pid_index
|
||||||
|
.store(pid.index(), Ordering::Relaxed);
|
||||||
|
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
set_current_pid(pid);
|
set_current_pid(pid);
|
||||||
|
|||||||
+275
-84
@@ -68,12 +68,10 @@
|
|||||||
use crate::actor::current_pid;
|
use crate::actor::current_pid;
|
||||||
use crate::channel::Sender;
|
use crate::channel::Sender;
|
||||||
use crate::pid::{Name, Pid};
|
use crate::pid::{Name, Pid};
|
||||||
use crate::runtime::{
|
use crate::runtime::{self, RuntimeInner, YieldIntent, RUNTIME};
|
||||||
self, RuntimeInner, YieldIntent, RUNTIME,
|
|
||||||
};
|
|
||||||
use crate::supervisor::Signal;
|
use crate::supervisor::Signal;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// with_runtime / try_with_runtime
|
// with_runtime / try_with_runtime
|
||||||
@@ -152,7 +150,9 @@ pub struct JoinHandle {
|
|||||||
|
|
||||||
impl JoinHandle {
|
impl JoinHandle {
|
||||||
/// The identity of the actor this handle refers to.
|
/// The identity of the actor this handle refers to.
|
||||||
pub fn pid(&self) -> Pid { self.pid }
|
pub fn pid(&self) -> Pid {
|
||||||
|
self.pid
|
||||||
|
}
|
||||||
|
|
||||||
/// Block the calling actor until the spawned actor finishes, then
|
/// Block the calling actor until the spawned actor finishes, then
|
||||||
/// report how it finished: `Ok(())` if it returned normally or stopped
|
/// report how it finished: `Ok(())` if it returned normally or stopped
|
||||||
@@ -182,12 +182,10 @@ impl JoinHandle {
|
|||||||
crate::slot_state::Status::Stale => {
|
crate::slot_state::Status::Stale => {
|
||||||
panic!("join: target slot has been reused")
|
panic!("join: target slot has been reused")
|
||||||
}
|
}
|
||||||
crate::slot_state::Status::Done => {
|
crate::slot_state::Status::Done => Some(match cold.outcome.take() {
|
||||||
Some(match cold.outcome.take() {
|
Some(outcome) => outcome,
|
||||||
Some(outcome) => outcome,
|
None => panic!("Done slot must have outcome"),
|
||||||
None => panic!("Done slot must have outcome"),
|
}),
|
||||||
})
|
|
||||||
}
|
|
||||||
crate::slot_state::Status::Live => {
|
crate::slot_state::Status::Live => {
|
||||||
// begin_wait is lock-free, legal under the cold lock;
|
// begin_wait is lock-free, legal under the cold lock;
|
||||||
// registering under it makes the epoch atomic with
|
// registering under it makes the epoch atomic with
|
||||||
@@ -227,8 +225,7 @@ impl JoinHandle {
|
|||||||
match slot.status_for(self.pid) {
|
match slot.status_for(self.pid) {
|
||||||
crate::slot_state::Status::Stale => false,
|
crate::slot_state::Status::Stale => false,
|
||||||
status => {
|
status => {
|
||||||
cold.outstanding_handles =
|
cold.outstanding_handles = cold.outstanding_handles.saturating_sub(1);
|
||||||
cold.outstanding_handles.saturating_sub(1);
|
|
||||||
cold.outstanding_handles == 0
|
cold.outstanding_handles == 0
|
||||||
&& status == crate::slot_state::Status::Done
|
&& status == crate::slot_state::Status::Done
|
||||||
}
|
}
|
||||||
@@ -281,6 +278,37 @@ pub struct SpawnOpts {
|
|||||||
pub guard_size: Option<usize>,
|
pub guard_size: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why [`try_spawn`] could not start an actor.
|
||||||
|
///
|
||||||
|
/// Marked `non_exhaustive`: today the only refusal is a full slab, but a
|
||||||
|
/// future variant (say, a shutdown-in-progress refusal) must not be a
|
||||||
|
/// breaking change for shed-path `match`es.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum SpawnError {
|
||||||
|
/// The fixed actor slab ([`Config::max_actors`]
|
||||||
|
/// (crate::runtime::Config::max_actors)) is full: every slot is claimed
|
||||||
|
/// by a live actor. This is a routine overload condition, not an
|
||||||
|
/// invariant violation — shed the unit of work (close the socket,
|
||||||
|
/// return a 503) and try again once actors have died.
|
||||||
|
AtCapacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for SpawnError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SpawnError::AtCapacity => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"actor slab at capacity (`Config::max_actors` live actors)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SpawnError {}
|
||||||
|
|
||||||
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
/// Start a new actor running `f`, and return a [`JoinHandle`] for it.
|
||||||
///
|
///
|
||||||
/// The new actor runs concurrently with its caller and with every other
|
/// The new actor runs concurrently with its caller and with every other
|
||||||
@@ -305,9 +333,7 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
|||||||
|
|
||||||
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
/// [`spawn`] with per-actor stack shape overrides (RFC 019).
|
||||||
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
pub fn spawn_with(opts: SpawnOpts, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||||
let parent = current_pid().unwrap_or_else(|| {
|
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||||
with_runtime(|_| crate::runtime::ROOT_PID)
|
|
||||||
});
|
|
||||||
spawn_under_with(parent, opts, f)
|
spawn_under_with(parent, opts, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +365,77 @@ pub fn spawn_under_with<A>(
|
|||||||
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||||
});
|
});
|
||||||
|
|
||||||
JoinHandle { pid, consumed: false }
|
JoinHandle {
|
||||||
|
pid,
|
||||||
|
consumed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`spawn`] that reports a full actor slab instead of panicking.
|
||||||
|
///
|
||||||
|
/// Behaviour parity with [`spawn`] in every case except one: when the fixed
|
||||||
|
/// slab ([`Config::max_actors`](crate::runtime::Config::max_actors)) is
|
||||||
|
/// full, this returns [`Err(SpawnError::AtCapacity)`](SpawnError::AtCapacity)
|
||||||
|
/// where `spawn` panics the calling actor. Use it at load-shedding call
|
||||||
|
/// sites — an accept loop spawning one actor per connection, a request
|
||||||
|
/// admission point — where "at capacity" is a routine overload condition to
|
||||||
|
/// handle (reject the unit of work), not an invariant violation. Internal
|
||||||
|
/// and bounded spawn sites should keep [`spawn`]: there, the panic is a
|
||||||
|
/// correct loud invariant check.
|
||||||
|
///
|
||||||
|
/// The claim is atomic (claim-or-report): there is no
|
||||||
|
/// check-then-spawn race against other spawners for the last slot, so no
|
||||||
|
/// headroom margin is needed.
|
||||||
|
pub fn try_spawn(f: impl FnOnce() + Send + 'static) -> Result<JoinHandle, SpawnError> {
|
||||||
|
let parent = current_pid().unwrap_or_else(|| with_runtime(|_| crate::runtime::ROOT_PID));
|
||||||
|
try_spawn_under_with(parent, SpawnOpts::default(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`try_spawn`] with an explicit supervisor and per-actor stack shape
|
||||||
|
/// overrides — the full-control core the other `try_` surface is built on
|
||||||
|
/// (mirrors [`spawn_under_with`]).
|
||||||
|
pub fn try_spawn_under_with<A>(
|
||||||
|
supervisor: Pid<A>,
|
||||||
|
opts: SpawnOpts,
|
||||||
|
f: impl FnOnce() + Send + 'static,
|
||||||
|
) -> Result<JoinHandle, SpawnError> {
|
||||||
|
let supervisor = supervisor.erase();
|
||||||
|
// Slot FIRST — deliberately the reverse of `spawn`'s stack-first order:
|
||||||
|
// under overload the Err arm is the HOT path, and a rejection must cost
|
||||||
|
// one mutex pop, not an mmap/pool-pop + init + recycle per shed unit of
|
||||||
|
// work. The claim is a single atomic pop (no TOCTOU; see
|
||||||
|
// `try_allocate_slot`).
|
||||||
|
let idx = match with_runtime(|inner| inner.try_allocate_slot()) {
|
||||||
|
Some(idx) => idx,
|
||||||
|
None => return Err(SpawnError::AtCapacity),
|
||||||
|
};
|
||||||
|
// Between claim and install the slot is owned by this frame alone; if
|
||||||
|
// stack allocation panics in that window the slot must go back or it
|
||||||
|
// leaks for the life of the runtime (and would trip the run()-teardown
|
||||||
|
// slot-leak debug_assert).
|
||||||
|
struct ReturnOnUnwind(Option<u32>);
|
||||||
|
impl Drop for ReturnOnUnwind {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(idx) = self.0 {
|
||||||
|
with_runtime(|inner| inner.return_vacant_slot(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut claimed = ReturnOnUnwind(Some(idx));
|
||||||
|
|
||||||
|
let stack = with_runtime(|inner| crate::runtime::acquire_stack(inner, opts));
|
||||||
|
let sp = init_actor_stack(stack.top(), crate::actor::trampoline);
|
||||||
|
let closure: crate::runtime::Closure = Box::new(f);
|
||||||
|
|
||||||
|
claimed.0 = None; // install_actor takes ownership of the slot from here
|
||||||
|
let pid = with_runtime(|inner| {
|
||||||
|
crate::runtime::install_actor(inner, idx, sp, stack, supervisor, closure)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(JoinHandle {
|
||||||
|
pid,
|
||||||
|
consumed: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn an actor that other actors can message directly by its [`Pid<A>`],
|
/// Spawn an actor that other actors can message directly by its [`Pid<A>`],
|
||||||
@@ -448,6 +544,29 @@ pub(crate) fn unpark_at(pid: Pid, epoch: u32) {
|
|||||||
let _ = try_with_runtime(|inner| inner.unpark_at(pid, epoch));
|
let _ = try_with_runtime(|inner| inner.unpark_at(pid, epoch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The current actor's runtime as a `Weak`, for a waker that must reach the
|
||||||
|
// runtime from a foreign thread later. A channel captures this when its
|
||||||
|
// receiver parks, so a cross-thread `send` can wake without the `RUNTIME`
|
||||||
|
// thread-local (unset off a scheduler thread). Panics outside `Runtime::run()`,
|
||||||
|
// the same contract as `begin_wait`.
|
||||||
|
pub(crate) fn runtime_weak() -> Weak<RuntimeInner> {
|
||||||
|
with_runtime(Arc::downgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Epoch-matched wake of `pid` from a waker that may or may not be on a
|
||||||
|
// scheduler thread. On a scheduler thread we take the thread-local path
|
||||||
|
// (preemption-gated, slot-eligible); off one that path is a silent no-op, so
|
||||||
|
// we reach the runtime through `rt` — the `Weak` the waker captured while it
|
||||||
|
// was in-runtime. Mirrors the IO backend's cross-context wake (io.rs, RFC 018).
|
||||||
|
pub(crate) fn unpark_at_via(pid: Pid, epoch: u32, rt: &Weak<RuntimeInner>) {
|
||||||
|
if try_with_runtime(|inner| inner.unpark_at(pid, epoch)).is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(inner) = rt.upgrade() {
|
||||||
|
inner.unpark_at(pid, epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Open a new wait for the current actor and return its wait identity
|
// Open a new wait for the current actor and return its wait identity
|
||||||
// ("epoch"). Call once per wait, before registering with any waker. Lock-free,
|
// ("epoch"). Call once per wait, before registering with any waker. Lock-free,
|
||||||
// so it's legal to call while already holding another internal lock.
|
// so it's legal to call while already holding another internal lock.
|
||||||
@@ -481,10 +600,12 @@ pub(crate) fn retire_wait() {
|
|||||||
/// [`JoinHandle::join`] reports it as a normal, non-error exit: cooperative
|
/// [`JoinHandle::join`] reports it as a normal, non-error exit: cooperative
|
||||||
/// stop is a controlled shutdown, not a failure.
|
/// stop is a controlled shutdown, not a failure.
|
||||||
///
|
///
|
||||||
/// This is exactly the mechanism `gen_server` shutdown, supervisor restarts,
|
/// This is the *hard* stop — OTP's `exit(Pid, kill)`. It is what a supervisor
|
||||||
/// and structured teardown are built from: reach for [`GenServerRef::shutdown`](crate::GenServerRef::shutdown)
|
/// falls back to when a child overstays its [`Shutdown`](crate::supervisor::Shutdown)
|
||||||
/// or a [`supervisor`](crate::supervisor) instead of calling this directly
|
/// grace period. For a stop the target gets to prepare for, use
|
||||||
/// where those apply.
|
/// [`request_shutdown`]; for structured teardown, reach for
|
||||||
|
/// [`GenServerRef::shutdown`](crate::GenServerRef::shutdown) or a
|
||||||
|
/// [`supervisor`](crate::supervisor) instead of calling this directly.
|
||||||
///
|
///
|
||||||
/// Because it's cooperative, an actor stuck in a tight loop with no
|
/// Because it's cooperative, an actor stuck in a tight loop with no
|
||||||
/// blocking call, no [`check!`](crate::check), and no allocation cannot be
|
/// blocking call, no [`check!`](crate::check), and no allocation cannot be
|
||||||
@@ -496,7 +617,8 @@ pub fn request_stop<A>(pid: Pid<A>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The core of `request_stop`, taking the runtime directly so it can also be
|
// The core of `request_stop`, taking the runtime directly so it can also be
|
||||||
// driven from inside the runtime itself (the root-exit sweep) without
|
// driven from inside the runtime itself (the RuntimeHandle path, supervisor
|
||||||
|
// sweeps) without
|
||||||
// re-borrowing the thread-local. Sets the stop flag under the target's lock
|
// re-borrowing the thread-local. Sets the stop flag under the target's lock
|
||||||
// (a generation mismatch, or no live actor there, makes it a no-op) and
|
// (a generation mismatch, or no live actor there, makes it a no-op) and
|
||||||
// wakes the target.
|
// wakes the target.
|
||||||
@@ -516,6 +638,68 @@ pub(crate) fn request_stop_inner(inner: &RuntimeInner, pid: Pid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask an actor to shut down gracefully — OTP's `exit(Pid, shutdown)`, where
|
||||||
|
/// [`request_stop`] is `exit(Pid, kill)`.
|
||||||
|
///
|
||||||
|
/// If the target has called [`trap_exit`](crate::trap_exit), it receives an
|
||||||
|
/// [`ExitSignal`](crate::ExitSignal) with reason
|
||||||
|
/// [`DownReason::Shutdown`](crate::DownReason::Shutdown) on its trap inbox and
|
||||||
|
/// keeps running: the request is advisory, and the target is expected to wind
|
||||||
|
/// down and exit normally in its own time (a supervisor bounds that time with
|
||||||
|
/// its child's [`Shutdown`](crate::supervisor::Shutdown) policy and falls back
|
||||||
|
/// to `request_stop`). A target that is not trapping is stopped exactly as by
|
||||||
|
/// `request_stop`. A dead pid is a no-op.
|
||||||
|
///
|
||||||
|
/// The signal's `from` is the calling actor, or `ROOT_PID` when driven from
|
||||||
|
/// outside the runtime (see [`RuntimeHandle::request_shutdown`](crate::RuntimeHandle::request_shutdown)).
|
||||||
|
pub fn request_shutdown<A>(pid: Pid<A>) {
|
||||||
|
let pid = pid.erase();
|
||||||
|
let from = current_pid().unwrap_or(crate::runtime::ROOT_PID);
|
||||||
|
let _ = try_with_runtime(|inner| request_shutdown_inner(inner, pid, from));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The core of `request_shutdown`. Reads the target's trap sender under its
|
||||||
|
// cold lock (generation-verified), then acts outside the lock: a trap send
|
||||||
|
// may unpark the receiver, and `request_stop_inner` re-takes the lock.
|
||||||
|
pub(crate) fn request_shutdown_inner(inner: &RuntimeInner, pid: Pid, from: Pid) {
|
||||||
|
request_shutdown_inner_probe(inner, pid, from);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`request_shutdown_inner`], reporting what it found: `Some(true)` if the
|
||||||
|
/// target was trapping (got the signal), `Some(false)` if it was stopped
|
||||||
|
/// outright, `None` if there was nothing live at `pid`.
|
||||||
|
pub(crate) fn request_shutdown_inner_probe(
|
||||||
|
inner: &RuntimeInner,
|
||||||
|
pid: Pid,
|
||||||
|
from: Pid,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let trap = match inner.slot_at(pid) {
|
||||||
|
Some(slot) => {
|
||||||
|
let cold = slot.cold.lock();
|
||||||
|
if slot.generation() == pid.generation() {
|
||||||
|
cold.actor.as_ref().map(|a| a.trap.clone())
|
||||||
|
} else {
|
||||||
|
None // stale pid: nothing there to shut down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
match trap {
|
||||||
|
Some(Some(tx)) => {
|
||||||
|
let _ = tx.send(crate::link::ExitSignal {
|
||||||
|
from,
|
||||||
|
reason: crate::monitor::DownReason::Shutdown,
|
||||||
|
});
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
Some(None) => {
|
||||||
|
request_stop_inner(inner, pid);
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// NoPreempt
|
// NoPreempt
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -566,11 +750,9 @@ pub fn sleep(duration: std::time::Duration) {
|
|||||||
let _np = NoPreempt::enter();
|
let _np = NoPreempt::enter();
|
||||||
let epoch = begin_wait();
|
let epoch = begin_wait();
|
||||||
let deadline = crate::timer::deadline_from_now(duration);
|
let deadline = crate::timer::deadline_from_now(duration);
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
||||||
Ok(mut timers) => timers.insert_sleep(deadline, me, epoch),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
park_current();
|
park_current();
|
||||||
}
|
}
|
||||||
@@ -588,11 +770,9 @@ pub fn sleep_wall(duration: std::time::Duration) {
|
|||||||
let _np = NoPreempt::enter();
|
let _np = NoPreempt::enter();
|
||||||
let epoch = begin_wait();
|
let epoch = begin_wait();
|
||||||
let deadline = crate::timer::deadline_from_now(duration);
|
let deadline = crate::timer::deadline_from_now(duration);
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
||||||
Ok(mut timers) => timers.insert_sleep_wall(deadline, me, epoch),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
park_current();
|
park_current();
|
||||||
}
|
}
|
||||||
@@ -608,15 +788,13 @@ pub fn insert_wait_timer(
|
|||||||
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
|
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
|
||||||
epoch: u32,
|
epoch: u32,
|
||||||
) {
|
) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert(
|
||||||
Ok(mut timers) => timers.insert(
|
deadline,
|
||||||
deadline,
|
pid,
|
||||||
pid,
|
crate::timer::Reason::WaitTimeout { target, epoch },
|
||||||
crate::timer::Reason::WaitTimeout { target, epoch },
|
),
|
||||||
),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,11 +824,9 @@ pub fn send_after<A: crate::pid::Addressable>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send_to(dest, msg);
|
let _ = crate::registry::send_to(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, dest.erase(), fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,11 +846,9 @@ pub fn send_after_named<M: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send(dest, msg);
|
let _ = crate::registry::send(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,11 +867,9 @@ pub fn send_after_wall<A: crate::pid::Addressable>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send_to(dest, msg);
|
let _ = crate::registry::send_to(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
||||||
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,11 +886,9 @@ pub fn send_after_named_wall<M: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = crate::registry::send(dest, msg);
|
let _ = crate::registry::send(dest, msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,11 +908,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
|||||||
let fire = Box::new(move || {
|
let fire = Box::new(move || {
|
||||||
let _ = tx.send(msg);
|
let _ = tx.send(msg);
|
||||||
});
|
});
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
||||||
Ok(mut timers) => timers.insert_send(deadline, armed_by, fire),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,11 +918,9 @@ pub(crate) fn send_after_to<T: Send + 'static>(
|
|||||||
/// it fires. Returns `true` if the timer was still pending and delivery is
|
/// it fires. Returns `true` if the timer was still pending and delivery is
|
||||||
/// now prevented, `false` if it had already fired or was already cancelled.
|
/// now prevented, `false` if it had already fired or was already cancelled.
|
||||||
pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
|
pub fn cancel_timer(id: crate::timer::TimerId) -> bool {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| match inner.timers.lock() {
|
||||||
match inner.timers.lock() {
|
Ok(mut timers) => timers.cancel(id),
|
||||||
Ok(mut timers) => timers.cancel(id),
|
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
||||||
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,7 +986,8 @@ where
|
|||||||
};
|
};
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
slot.generation(), me.generation(),
|
slot.generation(),
|
||||||
|
me.generation(),
|
||||||
"block_on_io: own slot reused mid-park"
|
"block_on_io: own slot reused mid-park"
|
||||||
);
|
);
|
||||||
match cold.pending_io_result.take() {
|
match cold.pending_io_result.take() {
|
||||||
@@ -946,12 +1113,20 @@ pub struct FdArm {
|
|||||||
impl FdArm {
|
impl FdArm {
|
||||||
/// An arm that becomes ready when `fd` is readable.
|
/// An arm that becomes ready when `fd` is readable.
|
||||||
pub fn readable(fd: std::os::fd::RawFd) -> Self {
|
pub fn readable(fd: std::os::fd::RawFd) -> Self {
|
||||||
FdArm { fd, readable: true, writable: false }
|
FdArm {
|
||||||
|
fd,
|
||||||
|
readable: true,
|
||||||
|
writable: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An arm that becomes ready when `fd` is writable.
|
/// An arm that becomes ready when `fd` is writable.
|
||||||
pub fn writable(fd: std::os::fd::RawFd) -> Self {
|
pub fn writable(fd: std::os::fd::RawFd) -> Self {
|
||||||
FdArm { fd, readable: false, writable: true }
|
FdArm {
|
||||||
|
fd,
|
||||||
|
readable: false,
|
||||||
|
writable: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,9 +1151,7 @@ impl crate::channel::Selectable for FdArm {
|
|||||||
match io.as_mut() {
|
match io.as_mut() {
|
||||||
Some(io) => {
|
Some(io) => {
|
||||||
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
|
inner.io_fd_waiters.fetch_add(1, Ordering::AcqRel);
|
||||||
let r = io.epoll_register(
|
let r = io.epoll_register(self.fd, pid, epoch, self.readable, self.writable);
|
||||||
self.fd, pid, epoch, self.readable, self.writable,
|
|
||||||
);
|
|
||||||
if r.is_err() {
|
if r.is_err() {
|
||||||
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
inner.io_fd_waiters.fetch_sub(1, Ordering::AcqRel);
|
||||||
}
|
}
|
||||||
@@ -1033,7 +1206,11 @@ fn poll_events(fd: std::os::fd::RawFd, readable: bool, writable: bool) -> std::i
|
|||||||
if writable {
|
if writable {
|
||||||
events |= libc::POLLOUT;
|
events |= libc::POLLOUT;
|
||||||
}
|
}
|
||||||
let mut pfd = libc::pollfd { fd, events, revents: 0 };
|
let mut pfd = libc::pollfd {
|
||||||
|
fd,
|
||||||
|
events,
|
||||||
|
revents: 0,
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
let r = unsafe { libc::poll(&mut pfd, 1, 0) };
|
let r = unsafe { libc::poll(&mut pfd, 1, 0) };
|
||||||
if r < 0 {
|
if r < 0 {
|
||||||
@@ -1081,7 +1258,11 @@ pub fn wait_writable_timeout(
|
|||||||
pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
wait_readable(fd)?;
|
wait_readable(fd)?;
|
||||||
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
|
||||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper: park until `fd` is writable, then perform the
|
/// Convenience wrapper: park until `fd` is writable, then perform the
|
||||||
@@ -1090,7 +1271,11 @@ pub fn read(fd: std::os::fd::RawFd, buf: &mut [u8]) -> std::io::Result<usize> {
|
|||||||
pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
wait_writable(fd)?;
|
wait_writable(fd)?;
|
||||||
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
|
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
|
||||||
if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1099,12 +1284,15 @@ pub fn write(fd: std::os::fd::RawFd, buf: &[u8]) -> std::io::Result<usize> {
|
|||||||
|
|
||||||
pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
|
pub fn register_supervisor_channel(pid: Pid, sender: Sender<Signal>) {
|
||||||
with_runtime(|inner| {
|
with_runtime(|inner| {
|
||||||
let slot = inner.slot_at(pid)
|
let slot = inner
|
||||||
|
.slot_at(pid)
|
||||||
.unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
|
.unwrap_or_else(|| panic!("register_supervisor_channel: pid {:?} not found", pid));
|
||||||
let mut cold = slot.cold.lock();
|
let mut cold = slot.cold.lock();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
slot.generation(), pid.generation(),
|
slot.generation(),
|
||||||
"register_supervisor_channel: pid {:?} not found", pid
|
pid.generation(),
|
||||||
|
"register_supervisor_channel: pid {:?} not found",
|
||||||
|
pid
|
||||||
);
|
);
|
||||||
cold.supervisor_channel = Some(sender);
|
cold.supervisor_channel = Some(sender);
|
||||||
});
|
});
|
||||||
@@ -1177,6 +1365,9 @@ mod send_after_to_tests {
|
|||||||
crate::sleep(Duration::from_millis(30));
|
crate::sleep(Duration::from_millis(30));
|
||||||
r2.store(true, Ordering::SeqCst);
|
r2.store(true, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
assert!(reached.load(Ordering::SeqCst), "runtime survived the dead-channel fire");
|
assert!(
|
||||||
|
reached.load(Ordering::SeqCst),
|
||||||
|
"runtime survived the dead-channel fire"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -214,7 +214,10 @@ struct Buf {
|
|||||||
|
|
||||||
impl Buf {
|
impl Buf {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Buf { b: [0; 320], len: 0 }
|
Buf {
|
||||||
|
b: [0; 320],
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn s(&mut self, s: &str) {
|
fn s(&mut self, s: &str) {
|
||||||
for &c in s.as_bytes() {
|
for &c in s.as_bytes() {
|
||||||
@@ -274,8 +277,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn inside_guard_both_edges() {
|
fn inside_guard_both_edges() {
|
||||||
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard);
|
||||||
assert_eq!(classify(GUARD_HI - 1, TOP, RESERVE, GUARD), FaultClass::Guard);
|
assert_eq!(
|
||||||
assert_eq!(classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD), FaultClass::Guard);
|
classify(GUARD_HI - 1, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD),
|
||||||
|
FaultClass::Guard
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+9
-9
@@ -188,8 +188,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"yield return from invalid word {w:#x}"
|
"yield return from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
if self
|
if self
|
||||||
@@ -247,8 +246,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"begin_wait from invalid word {w:#x}"
|
"begin_wait from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
|
let next = word_epoch(w).wrapping_add(1) & EPOCH_MASK;
|
||||||
@@ -342,8 +340,7 @@ impl StateWord {
|
|||||||
loop {
|
loop {
|
||||||
let w = self.load();
|
let w = self.load();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(w), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(w) == gen,
|
||||||
&& word_gen(w) == gen,
|
|
||||||
"clear_notify from invalid word {w:#x}"
|
"clear_notify from invalid word {w:#x}"
|
||||||
);
|
);
|
||||||
if word_state(w) != ST_RUNNING_NOTIFIED {
|
if word_state(w) != ST_RUNNING_NOTIFIED {
|
||||||
@@ -372,8 +369,7 @@ impl StateWord {
|
|||||||
pub(crate) fn set_done(&self, gen: u32) {
|
pub(crate) fn set_done(&self, gen: u32) {
|
||||||
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
|
let prev = self.0.swap(pack(gen, 0, ST_DONE), Ordering::AcqRel);
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED)
|
matches!(word_state(prev), ST_RUNNING | ST_RUNNING_NOTIFIED) && word_gen(prev) == gen,
|
||||||
&& word_gen(prev) == gen,
|
|
||||||
"finalize from invalid word {prev:#x}"
|
"finalize from invalid word {prev:#x}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -538,7 +534,11 @@ mod loom_tests {
|
|||||||
// not a pending notification.
|
// not a pending notification.
|
||||||
assert!(word.try_claim(0));
|
assert!(word.try_claim(0));
|
||||||
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop);
|
assert_eq!(word.unpark(0, Some(epoch)), Unpark::Noop);
|
||||||
assert_eq!(word_state(word.load()), ST_RUNNING, "stale epoch notified a live run");
|
assert_eq!(
|
||||||
|
word_state(word.load()),
|
||||||
|
ST_RUNNING,
|
||||||
|
"stale epoch notified a live run"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -57,16 +57,19 @@ impl Stack {
|
|||||||
}
|
}
|
||||||
let base = base as *mut u8;
|
let base = base as *mut u8;
|
||||||
|
|
||||||
let ret = unsafe {
|
let ret = unsafe { libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE) };
|
||||||
libc::mprotect(base as *mut libc::c_void, guard_size, libc::PROT_NONE)
|
|
||||||
};
|
|
||||||
if ret != 0 {
|
if ret != 0 {
|
||||||
let err = io::Error::last_os_error();
|
let err = io::Error::last_os_error();
|
||||||
unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
|
unsafe { libc::munmap(base as *mut libc::c_void, total_size) };
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { base, total_size, stack_size, guard_size })
|
Ok(Self {
|
||||||
|
base,
|
||||||
|
total_size,
|
||||||
|
stack_size,
|
||||||
|
guard_size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 16-byte-aligned top of the usable region.
|
/// 16-byte-aligned top of the usable region.
|
||||||
@@ -177,7 +180,11 @@ pub(crate) fn shrink_range(hwm: usize, sp: usize, page: usize) -> Option<(usize,
|
|||||||
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
/// with `stack_size` page-rounded by `Stack::new` the result is always
|
||||||
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
/// page-aligned. Checked math: `retain ≥ stack_size` (notably the default
|
||||||
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
/// 64 KiB reserve with the 64 KiB RETAIN) and overflow collapse to `None`.
|
||||||
pub(crate) fn retain_range(stack_size: usize, retain: usize, page: usize) -> Option<(usize, usize)> {
|
pub(crate) fn retain_range(
|
||||||
|
stack_size: usize,
|
||||||
|
retain: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
debug_assert!(page.is_power_of_two());
|
debug_assert!(page.is_power_of_two());
|
||||||
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
let retain = retain.checked_add(page - 1)? & !(page - 1); // page_up(retain)
|
||||||
let len = stack_size.checked_sub(retain)?;
|
let len = stack_size.checked_sub(retain)?;
|
||||||
|
|||||||
+197
-74
@@ -123,9 +123,9 @@ pub enum Signal {
|
|||||||
impl std::fmt::Debug for Signal {
|
impl std::fmt::Debug for Signal {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
Signal::Exit(pid) => write!(f, "Signal::Exit({:?})", pid),
|
||||||
Signal::Panic(pid, _) => write!(f, "Signal::Panic({:?}, ..)", pid),
|
Signal::Panic(pid, _) => write!(f, "Signal::Panic({:?}, ..)", pid),
|
||||||
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
Signal::Stopped(pid) => write!(f, "Signal::Stopped({:?})", pid),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,14 +133,15 @@ impl std::fmt::Debug for Signal {
|
|||||||
impl Signal {
|
impl Signal {
|
||||||
pub fn pid(&self) -> Pid {
|
pub fn pid(&self) -> Pid {
|
||||||
match self {
|
match self {
|
||||||
Signal::Exit(p) => *p,
|
Signal::Exit(p) => *p,
|
||||||
Signal::Panic(p, _) => *p,
|
Signal::Panic(p, _) => *p,
|
||||||
Signal::Stopped(p) => *p,
|
Signal::Stopped(p) => *p,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::channel::channel;
|
use crate::channel::{channel, RecvTimeoutError};
|
||||||
|
use crate::monitor::DownReason;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -165,11 +166,52 @@ pub enum Restart {
|
|||||||
pub struct ChildSpec {
|
pub struct ChildSpec {
|
||||||
start: Arc<dyn Fn() + Send + Sync + 'static>,
|
start: Arc<dyn Fn() + Send + Sync + 'static>,
|
||||||
restart: Restart,
|
restart: Restart,
|
||||||
|
shutdown: Shutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChildSpec {
|
impl ChildSpec {
|
||||||
|
/// A child with the given restart policy and the default
|
||||||
|
/// [`Shutdown::Timeout`] of 5 seconds.
|
||||||
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self {
|
pub fn new(restart: Restart, start: impl Fn() + Send + Sync + 'static) -> Self {
|
||||||
Self { start: Arc::new(start), restart }
|
Self {
|
||||||
|
start: Arc::new(start),
|
||||||
|
restart,
|
||||||
|
shutdown: Shutdown::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set how the supervisor stops this child (see [`Shutdown`]). A child
|
||||||
|
/// that is itself a supervisor should use [`Shutdown::Infinity`] so its
|
||||||
|
/// own subtree gets its full grace periods.
|
||||||
|
pub fn shutdown(mut self, shutdown: Shutdown) -> Self {
|
||||||
|
self.shutdown = shutdown;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a supervisor stops a child it is taking down — the OTP child-spec
|
||||||
|
/// `shutdown` value. Applies to every supervisor-initiated stop: the ordered
|
||||||
|
/// shutdown of the whole set and the sibling cycling of
|
||||||
|
/// [`Strategy::OneForAll`] / [`Strategy::RestForOne`].
|
||||||
|
///
|
||||||
|
/// A graceful stop is a [`request_shutdown`](crate::request_shutdown): a child
|
||||||
|
/// that traps exits receives the request as a message and winds down in its
|
||||||
|
/// own time; one that does not is stopped outright.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Shutdown {
|
||||||
|
/// `request_stop` immediately; no request, no grace period.
|
||||||
|
BrutalKill,
|
||||||
|
/// `request_shutdown`, wait up to the duration for the child to exit, then
|
||||||
|
/// `request_stop` it. The default, at 5 seconds.
|
||||||
|
Timeout(Duration),
|
||||||
|
/// `request_shutdown` and wait however long the child takes. Use for a
|
||||||
|
/// child supervisor, whose subtree has its own timeouts.
|
||||||
|
Infinity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Shutdown {
|
||||||
|
fn default() -> Self {
|
||||||
|
Shutdown::Timeout(Duration::from_secs(5))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,23 +283,35 @@ impl OneForOne {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run the supervision loop on the current actor. Returns when every child
|
/// Run the supervision loop on the current actor. Returns when every child
|
||||||
/// has reached a terminal, non-restartable state, or when the restart
|
/// has reached a terminal, non-restartable state, when the restart
|
||||||
/// intensity cap is tripped.
|
/// intensity cap is tripped, or when the supervisor is asked to shut down
|
||||||
|
/// (a [`request_shutdown`](crate::request_shutdown) — from its own
|
||||||
|
/// supervisor, or from the app). On every one of those exits the survivors
|
||||||
|
/// are stopped in reverse start order, each per its
|
||||||
|
/// [`Shutdown`] policy, before this returns.
|
||||||
|
///
|
||||||
|
/// The supervisor traps exits for the length of the loop (that is how the
|
||||||
|
/// shutdown request reaches it as a message). Should the supervisor itself
|
||||||
|
/// be hard-stopped with [`request_stop`](crate::request_stop), it unwinds
|
||||||
|
/// without waiting for anything — but a drop guard hard-stops its live
|
||||||
|
/// children on the way out, so the subtree is not orphaned (a child
|
||||||
|
/// supervisor unwinds the same way, recursively).
|
||||||
pub fn run(self) {
|
pub fn run(self) {
|
||||||
let me = crate::scheduler::self_pid();
|
let me = crate::scheduler::self_pid();
|
||||||
let (tx, rx) = channel::<Signal>();
|
let (tx, rx) = channel::<Signal>();
|
||||||
crate::scheduler::register_supervisor_channel(me, tx);
|
crate::scheduler::register_supervisor_channel(me, tx);
|
||||||
|
let exits = crate::link::trap_exit();
|
||||||
|
|
||||||
// pid -> index into `self.children`, for the children currently alive.
|
// pid -> index into `self.children`, for the children currently alive.
|
||||||
let mut by_pid: HashMap<Pid, usize> = HashMap::new();
|
let mut live = Live::default();
|
||||||
let mut active: usize = 0;
|
let mut active: usize = 0;
|
||||||
// Sliding window of recent restart instants, for the intensity cap.
|
// Sliding window of recent restart instants, for the intensity cap.
|
||||||
let mut restarts: Vec<Instant> = Vec::new();
|
let mut restarts: Vec<Instant> = Vec::new();
|
||||||
|
|
||||||
let start_child = |idx: usize, by_pid: &mut HashMap<Pid, usize>| {
|
let start_child = |idx: usize, live: &mut Live| {
|
||||||
let start = self.children[idx].start.clone();
|
let start = self.children[idx].start.clone();
|
||||||
let h = crate::scheduler::spawn_under(me, move || (start)());
|
let h = crate::scheduler::spawn_under(me, move || (start)());
|
||||||
by_pid.insert(h.pid(), idx);
|
live.insert(h.pid(), idx);
|
||||||
// We supervise via the signal funnel, not by joining; drop the
|
// We supervise via the signal funnel, not by joining; drop the
|
||||||
// handle so the child's slot is reclaimed promptly on death (the
|
// handle so the child's slot is reclaimed promptly on death (the
|
||||||
// termination Signal is delivered before reclamation regardless).
|
// termination Signal is delivered before reclamation regardless).
|
||||||
@@ -265,28 +319,105 @@ impl OneForOne {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for idx in 0..self.children.len() {
|
for idx in 0..self.children.len() {
|
||||||
start_child(idx, &mut by_pid);
|
start_child(idx, &mut live);
|
||||||
active += 1;
|
active += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A signal that arrives while we are awaiting stop-confirmations (for a
|
// A signal that arrives while we are awaiting stop-confirmations (for a
|
||||||
// child we are *not* currently stopping) is stashed here and processed
|
// child we are *not* currently stopping) is stashed here and processed
|
||||||
// by the main loop before it blocks on `recv` again.
|
// by the main loop before it blocks again.
|
||||||
let mut pending: VecDeque<Signal> = VecDeque::new();
|
let mut pending: VecDeque<Signal> = VecDeque::new();
|
||||||
let next_signal = |pending: &mut VecDeque<Signal>| -> Option<Signal> {
|
|
||||||
if let Some(s) = pending.pop_front() {
|
// Stop one child per its policy and wait for its termination signal.
|
||||||
Some(s)
|
// Signals for other pids that arrive meanwhile are stashed. Bounded by
|
||||||
} else {
|
// construction: `request_stop` (used directly, or as the fallback once
|
||||||
rx.recv().ok()
|
// the grace period lapses) always produces a signal.
|
||||||
|
let stop_child = |pid: Pid, idx: usize, pending: &mut VecDeque<Signal>| {
|
||||||
|
let await_one = |deadline: Option<Instant>, pending: &mut VecDeque<Signal>| -> bool {
|
||||||
|
loop {
|
||||||
|
let sig = match pending.iter().position(|s| s.pid() == pid) {
|
||||||
|
Some(i) => pending.remove(i),
|
||||||
|
None => match deadline {
|
||||||
|
None => rx.recv().ok(),
|
||||||
|
Some(dl) => {
|
||||||
|
match rx.recv_timeout(dl.saturating_duration_since(Instant::now()))
|
||||||
|
{
|
||||||
|
Ok(s) => Some(s),
|
||||||
|
Err(RecvTimeoutError::Timeout) => return false,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match sig {
|
||||||
|
Some(s) if s.pid() == pid => return true,
|
||||||
|
Some(s) => pending.push_back(s),
|
||||||
|
None => return true, // funnel closed: nothing more can arrive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match self.children[idx].shutdown {
|
||||||
|
Shutdown::BrutalKill => {
|
||||||
|
crate::scheduler::request_stop(pid);
|
||||||
|
await_one(None, pending);
|
||||||
|
}
|
||||||
|
Shutdown::Timeout(grace) => {
|
||||||
|
crate::scheduler::request_shutdown(pid);
|
||||||
|
if !await_one(Some(Instant::now() + grace), pending) {
|
||||||
|
crate::scheduler::request_stop(pid);
|
||||||
|
await_one(None, pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Shutdown::Infinity => {
|
||||||
|
crate::scheduler::request_shutdown(pid);
|
||||||
|
await_one(None, pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stop a set of children in reverse start order, one at a time.
|
||||||
|
let stop_set =
|
||||||
|
|set: &mut Vec<(Pid, usize)>, live: &mut Live, pending: &mut VecDeque<Signal>| {
|
||||||
|
set.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
|
||||||
|
for (pid, idx) in set.iter() {
|
||||||
|
live.remove(pid);
|
||||||
|
stop_child(*pid, *idx, pending);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for the next event: a stashed signal, a child signal, or a
|
||||||
|
// shutdown request. `Ok(sig)`, or `Err(())` when we must wind down.
|
||||||
|
let next_event = |pending: &mut VecDeque<Signal>| -> Result<Signal, ()> {
|
||||||
|
loop {
|
||||||
|
if let Some(s) = pending.pop_front() {
|
||||||
|
return Ok(s);
|
||||||
|
}
|
||||||
|
// The trap inbox is arm 0: a shutdown request is noticed even
|
||||||
|
// under a flood of child signals.
|
||||||
|
match crate::channel::select(&[&exits, &rx]) {
|
||||||
|
0 => match exits.try_recv() {
|
||||||
|
Ok(Some(sig)) if sig.reason == DownReason::Shutdown => return Err(()),
|
||||||
|
// Any other exit signal (a linked peer's death — a
|
||||||
|
// supervisor links nothing itself, but may be linked
|
||||||
|
// to) is not ours to act on; a closed trap inbox is
|
||||||
|
// impossible while `exits` is held here.
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => match rx.try_recv() {
|
||||||
|
Ok(Some(s)) => return Ok(s),
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(_) => return Err(()), // funnel closed: nothing left to supervise
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
while active > 0 {
|
while active > 0 {
|
||||||
let sig = match next_signal(&mut pending) {
|
let sig = match next_event(&mut pending) {
|
||||||
Some(s) => s,
|
Ok(s) => s,
|
||||||
None => break, // mailbox closed: nothing left to supervise
|
Err(()) => break,
|
||||||
};
|
};
|
||||||
let idx = match by_pid.remove(&sig.pid()) {
|
let idx = match live.remove(&sig.pid()) {
|
||||||
Some(i) => i,
|
Some(i) => i,
|
||||||
None => continue, // stray/duplicate signal
|
None => continue, // stray/duplicate signal
|
||||||
};
|
};
|
||||||
@@ -318,78 +449,70 @@ impl OneForOne {
|
|||||||
restarts.push(now);
|
restarts.push(now);
|
||||||
|
|
||||||
// Which *live* siblings get cycled along with the failed child.
|
// Which *live* siblings get cycled along with the failed child.
|
||||||
// (The failed child is already gone — removed from `by_pid` above.)
|
// (The failed child is already gone — removed from `live` above.)
|
||||||
let mut to_stop: Vec<(Pid, usize)> = match self.strategy {
|
let mut to_stop: Vec<(Pid, usize)> = match self.strategy {
|
||||||
Strategy::OneForOne => Vec::new(),
|
Strategy::OneForOne => Vec::new(),
|
||||||
Strategy::OneForAll => by_pid.iter().map(|(p, i)| (*p, *i)).collect(),
|
Strategy::OneForAll => live.iter().map(|(p, i)| (*p, *i)).collect(),
|
||||||
Strategy::RestForOne => by_pid
|
Strategy::RestForOne => live
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, i)| **i > idx)
|
.filter(|(_, i)| **i > idx)
|
||||||
.map(|(p, i)| (*p, *i))
|
.map(|(p, i)| (*p, *i))
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
// Stop survivors in reverse start order (highest child index first).
|
|
||||||
to_stop.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
|
|
||||||
|
|
||||||
// The set we will restart: the failed child plus every sibling we
|
// The set we will restart: the failed child plus every sibling we
|
||||||
// are about to stop, restarted in start (ascending index) order.
|
// are about to stop, restarted in start (ascending index) order.
|
||||||
let mut restart_set: Vec<usize> = Vec::with_capacity(to_stop.len() + 1);
|
let mut restart_set: Vec<usize> = Vec::with_capacity(to_stop.len() + 1);
|
||||||
restart_set.push(idx);
|
restart_set.push(idx);
|
||||||
|
restart_set.extend(to_stop.iter().map(|(_, i)| *i));
|
||||||
|
|
||||||
// Request stops, then await each survivor's termination signal
|
// Stop the survivors (each per its policy, reverse start order),
|
||||||
// before restarting. `request_stop` on an already-dead pid is a
|
// then restart the whole set in start order. Net effect on
|
||||||
// no-op; in that case its (already-sent) Exit signal serves as the
|
// `active`: one child died (idx), `to_stop.len()` were stopped,
|
||||||
// confirmation. Any signal for a pid we are *not* awaiting is
|
// and `restart_set.len() == 1 + to_stop.len()` are started — so
|
||||||
// stashed for the main loop.
|
|
||||||
let mut awaiting: Vec<Pid> = Vec::with_capacity(to_stop.len());
|
|
||||||
for (pid, cidx) in &to_stop {
|
|
||||||
by_pid.remove(pid);
|
|
||||||
restart_set.push(*cidx);
|
|
||||||
crate::scheduler::request_stop(*pid);
|
|
||||||
awaiting.push(*pid);
|
|
||||||
}
|
|
||||||
while !awaiting.is_empty() {
|
|
||||||
let s = match next_signal(&mut pending) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => break, // mailbox closed mid-await; stop waiting
|
|
||||||
};
|
|
||||||
if let Some(pos) = awaiting.iter().position(|p| *p == s.pid()) {
|
|
||||||
awaiting.swap_remove(pos);
|
|
||||||
} else {
|
|
||||||
pending.push_back(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart the whole set in start order. Net effect on `active`:
|
|
||||||
// one child died (idx), `to_stop.len()` were stopped, and
|
|
||||||
// `restart_set.len() == 1 + to_stop.len()` are started — so
|
|
||||||
// `active` is unchanged and needs no adjustment here.
|
// `active` is unchanged and needs no adjustment here.
|
||||||
|
stop_set(&mut to_stop, &mut live, &mut pending);
|
||||||
restart_set.sort_unstable();
|
restart_set.sort_unstable();
|
||||||
for cidx in restart_set {
|
for cidx in restart_set {
|
||||||
start_child(cidx, &mut by_pid);
|
start_child(cidx, &mut live);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordered shutdown: stop any survivors in reverse start order and await
|
// Ordered shutdown: stop any survivors in reverse start order, each per
|
||||||
// their termination. On the normal `active == 0` exit `by_pid` is empty
|
// its policy. On the normal `active == 0` exit `live` is empty and this
|
||||||
// and this is a no-op; on a cap-trip or mailbox-closed break it tears
|
// is a no-op; on a shutdown request, a cap-trip, or a closed funnel it
|
||||||
// the remaining children down deterministically instead of leaking them.
|
// tears the remaining children down deterministically.
|
||||||
let mut survivors: Vec<(Pid, usize)> = by_pid.iter().map(|(p, i)| (*p, *i)).collect();
|
let mut survivors: Vec<(Pid, usize)> = live.iter().map(|(p, i)| (*p, *i)).collect();
|
||||||
survivors.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
|
stop_set(&mut survivors, &mut live, &mut pending);
|
||||||
let mut awaiting: Vec<Pid> = Vec::with_capacity(survivors.len());
|
}
|
||||||
for (pid, _) in &survivors {
|
}
|
||||||
crate::scheduler::request_stop(*pid);
|
|
||||||
awaiting.push(*pid);
|
/// The live children of a supervisor, with a drop guard: if the supervisor is
|
||||||
}
|
/// unwound (a hard `request_stop`, or a panic in the loop) its children are
|
||||||
while !awaiting.is_empty() {
|
/// hard-stopped rather than orphaned. Fire-and-forget by necessity — a guard
|
||||||
let s = match next_signal(&mut pending) {
|
/// running mid-unwind cannot park to await anything.
|
||||||
Some(s) => s,
|
#[derive(Default)]
|
||||||
None => break,
|
struct Live(HashMap<Pid, usize>);
|
||||||
};
|
|
||||||
if let Some(pos) = awaiting.iter().position(|p| *p == s.pid()) {
|
impl std::ops::Deref for Live {
|
||||||
awaiting.swap_remove(pos);
|
type Target = HashMap<Pid, usize>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for Live {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Live {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if std::thread::panicking() {
|
||||||
|
for pid in self.0.keys() {
|
||||||
|
crate::scheduler::request_stop(*pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -129,7 +129,9 @@ impl Ord for Entry {
|
|||||||
// Earlier deadline first; ties broken by insertion order so the
|
// Earlier deadline first; ties broken by insertion order so the
|
||||||
// ordering is total. `Reason` and `Pid` deliberately don't
|
// ordering is total. `Reason` and `Pid` deliberately don't
|
||||||
// participate.
|
// participate.
|
||||||
self.deadline.cmp(&other.deadline).then_with(|| self.seq.cmp(&other.seq))
|
self.deadline
|
||||||
|
.cmp(&other.deadline)
|
||||||
|
.then_with(|| self.seq.cmp(&other.seq))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-30
@@ -16,13 +16,17 @@
|
|||||||
#[cfg(feature = "smarm-trace")]
|
#[cfg(feature = "smarm-trace")]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! te {
|
macro_rules! te {
|
||||||
($kind:expr) => { $crate::trace::record($kind) };
|
($kind:expr) => {
|
||||||
|
$crate::trace::record($kind)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "smarm-trace"))]
|
#[cfg(not(feature = "smarm-trace"))]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! te {
|
macro_rules! te {
|
||||||
($kind:expr) => { () };
|
($kind:expr) => {
|
||||||
|
()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "smarm-trace")]
|
#[cfg(feature = "smarm-trace")]
|
||||||
@@ -42,17 +46,32 @@ mod inner {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
// Actor lifecycle
|
// Actor lifecycle
|
||||||
Spawn { parent: Pid, child: Pid },
|
Spawn {
|
||||||
|
parent: Pid,
|
||||||
|
child: Pid,
|
||||||
|
},
|
||||||
Resume(Pid),
|
Resume(Pid),
|
||||||
Yield(Pid),
|
Yield(Pid),
|
||||||
Park(Pid),
|
Park(Pid),
|
||||||
Done(Pid),
|
Done(Pid),
|
||||||
|
/// Root exit found a live forest root (an actor nobody supervises)
|
||||||
|
/// and delivered `request_shutdown` to it. `trapping` says whether it
|
||||||
|
/// got the chance to drain (true) or was stopped outright (false).
|
||||||
|
/// Every such line is an actor whose lifetime was nobody's business
|
||||||
|
/// but the runtime's — the way to *see* unsupervised leftovers.
|
||||||
|
RootSweep {
|
||||||
|
target: Pid,
|
||||||
|
trapping: bool,
|
||||||
|
},
|
||||||
// Wakeup paths
|
// Wakeup paths
|
||||||
UnparkDirect(Pid), // unpark() saw Parked -> re-queued immediately
|
UnparkDirect(Pid), // unpark() saw Parked -> re-queued immediately
|
||||||
UnparkDeferred(Pid), // unpark() saw Runnable -> set pending_unpark flag
|
UnparkDeferred(Pid), // unpark() saw Runnable -> set pending_unpark flag
|
||||||
UnparkFlagConsumed(Pid), // scheduler saw flag on Park -> re-queued instead
|
UnparkFlagConsumed(Pid), // scheduler saw flag on Park -> re-queued instead
|
||||||
// Channel
|
// Channel
|
||||||
Send { sender: Pid, receiver: Option<Pid> },
|
Send {
|
||||||
|
sender: Pid,
|
||||||
|
receiver: Option<Pid>,
|
||||||
|
},
|
||||||
RecvPark(Pid),
|
RecvPark(Pid),
|
||||||
RecvWake(Pid),
|
RecvWake(Pid),
|
||||||
// Queue
|
// Queue
|
||||||
@@ -68,8 +87,8 @@ mod inner {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
struct Record {
|
struct Record {
|
||||||
nanos: u64, // ns since open()
|
nanos: u64, // ns since open()
|
||||||
tid: u64, // OS thread id
|
tid: u64, // OS thread id
|
||||||
event: Event,
|
event: Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +103,8 @@ mod inner {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
struct Global {
|
struct Global {
|
||||||
sender: mpsc::Sender<Msg>,
|
sender: mpsc::Sender<Msg>,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
static GLOBAL: Mutex<Option<Global>> = Mutex::new(None);
|
static GLOBAL: Mutex<Option<Global>> = Mutex::new(None);
|
||||||
@@ -95,7 +114,7 @@ mod inner {
|
|||||||
// The start Instant is copied alongside it — also one mutex hit per thread.
|
// The start Instant is copied alongside it — also one mutex hit per thread.
|
||||||
// record() never touches GLOBAL after that.
|
// record() never touches GLOBAL after that.
|
||||||
struct LocalState {
|
struct LocalState {
|
||||||
tx: mpsc::Sender<Msg>,
|
tx: mpsc::Sender<Msg>,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +128,8 @@ mod inner {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
pub fn open() {
|
pub fn open() {
|
||||||
let path = std::env::var("SMARM_TRACE_FILE")
|
let path =
|
||||||
.unwrap_or_else(|_| "smarm_trace.json".to_owned());
|
std::env::var("SMARM_TRACE_FILE").unwrap_or_else(|_| "smarm_trace.json".to_owned());
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel::<Msg>();
|
let (tx, rx) = mpsc::channel::<Msg>();
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -164,8 +183,11 @@ mod inner {
|
|||||||
// which would try to re-acquire inner.shared (already held at many
|
// which would try to re-acquire inner.shared (already held at many
|
||||||
// te!() call sites) -> deadlock. Guard at the very top, before any
|
// te!() call sites) -> deadlock. Guard at the very top, before any
|
||||||
// allocation-capable call.
|
// allocation-capable call.
|
||||||
let was_enabled = crate::preempt::PREEMPTION_ENABLED
|
let was_enabled = crate::preempt::PREEMPTION_ENABLED.with(|e| {
|
||||||
.with(|e| { let v = e.get(); e.set(false); v });
|
let v = e.get();
|
||||||
|
e.set(false);
|
||||||
|
v
|
||||||
|
});
|
||||||
|
|
||||||
LOCAL_STATE.with(|cell| {
|
LOCAL_STATE.with(|cell| {
|
||||||
let mut opt = cell.borrow_mut();
|
let mut opt = cell.borrow_mut();
|
||||||
@@ -182,7 +204,7 @@ mod inner {
|
|||||||
}
|
}
|
||||||
if let Some(ls) = opt.as_ref() {
|
if let Some(ls) = opt.as_ref() {
|
||||||
let nanos = ls.start.elapsed().as_nanos() as u64;
|
let nanos = ls.start.elapsed().as_nanos() as u64;
|
||||||
let tid = os_tid();
|
let tid = os_tid();
|
||||||
let _ = ls.tx.send(Msg::Event(Record { nanos, tid, event }));
|
let _ = ls.tx.send(Msg::Event(Record { nanos, tid, event }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -197,7 +219,10 @@ mod inner {
|
|||||||
fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) {
|
fn drain_thread(rx: mpsc::Receiver<Msg>, path: &str) {
|
||||||
let f = match std::fs::File::create(path) {
|
let f = match std::fs::File::create(path) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(e) => { eprintln!("[smarm-trace] create failed: {}", e); return; }
|
Err(e) => {
|
||||||
|
eprintln!("[smarm-trace] create failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let mut w = std::io::BufWriter::new(f);
|
let mut w = std::io::BufWriter::new(f);
|
||||||
let _ = writeln!(w, "{{\"traceEvents\":[");
|
let _ = writeln!(w, "{{\"traceEvents\":[");
|
||||||
@@ -210,7 +235,9 @@ mod inner {
|
|||||||
Ok(Msg::Event(r)) => {
|
Ok(Msg::Event(r)) => {
|
||||||
let (name, actor_idx) = chrome_fields(&r.event);
|
let (name, actor_idx) = chrome_fields(&r.event);
|
||||||
let ts_us = r.nanos as f64 / 1000.0;
|
let ts_us = r.nanos as f64 / 1000.0;
|
||||||
if !first { let _ = w.write_all(b",\n"); }
|
if !first {
|
||||||
|
let _ = w.write_all(b",\n");
|
||||||
|
}
|
||||||
first = false;
|
first = false;
|
||||||
let _ = write!(w,
|
let _ = write!(w,
|
||||||
"{{\"ph\":\"i\",\"ts\":{:.3},\"pid\":{},\"tid\":{},\"name\":{:?},\"s\":\"g\"}}",
|
"{{\"ph\":\"i\",\"ts\":{:.3},\"pid\":{},\"tid\":{},\"name\":{:?},\"s\":\"g\"}}",
|
||||||
@@ -234,27 +261,38 @@ mod inner {
|
|||||||
|
|
||||||
fn chrome_fields(ev: &Event) -> (String, u32) {
|
fn chrome_fields(ev: &Event) -> (String, u32) {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Spawn { parent, child } =>
|
Event::Spawn { parent, child } => {
|
||||||
(format!("spawn c={}", child.index()), parent.index()),
|
(format!("spawn c={}", child.index()), parent.index())
|
||||||
Event::Resume(p) => ("resume".into(), p.index()),
|
}
|
||||||
Event::Yield(p) => ("yield".into(), p.index()),
|
Event::Resume(p) => ("resume".into(), p.index()),
|
||||||
Event::Park(p) => ("park".into(), p.index()),
|
Event::Yield(p) => ("yield".into(), p.index()),
|
||||||
Event::Done(p) => ("done".into(), p.index()),
|
Event::Park(p) => ("park".into(), p.index()),
|
||||||
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
Event::Done(p) => ("done".into(), p.index()),
|
||||||
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
Event::RootSweep { target, trapping } => (
|
||||||
|
format!(
|
||||||
|
"root_sweep {}",
|
||||||
|
if *trapping { "shutdown" } else { "stopped" }
|
||||||
|
),
|
||||||
|
target.index(),
|
||||||
|
),
|
||||||
|
Event::UnparkDirect(p) => ("unpark_direct".into(), p.index()),
|
||||||
|
Event::UnparkDeferred(p) => ("unpark_deferred".into(), p.index()),
|
||||||
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
|
Event::UnparkFlagConsumed(p) => ("unpark_flag_consumed".into(), p.index()),
|
||||||
Event::Send { sender, receiver } => (
|
Event::Send { sender, receiver } => (
|
||||||
format!("send rx={}", receiver
|
format!(
|
||||||
.map(|p| p.index().to_string())
|
"send rx={}",
|
||||||
.unwrap_or_else(|| "none".into())),
|
receiver
|
||||||
|
.map(|p| p.index().to_string())
|
||||||
|
.unwrap_or_else(|| "none".into())
|
||||||
|
),
|
||||||
sender.index(),
|
sender.index(),
|
||||||
),
|
),
|
||||||
Event::RecvPark(p) => ("recv_park".into(), p.index()),
|
Event::RecvPark(p) => ("recv_park".into(), p.index()),
|
||||||
Event::RecvWake(p) => ("recv_wake".into(), p.index()),
|
Event::RecvWake(p) => ("recv_wake".into(), p.index()),
|
||||||
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
Event::Enqueue(p) => ("enqueue".into(), p.index()),
|
||||||
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
Event::Dequeue(p) => ("dequeue".into(), p.index()),
|
||||||
Event::SlotPush(p) => ("slot_push".into(), p.index()),
|
Event::SlotPush(p) => ("slot_push".into(), p.index()),
|
||||||
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
Event::SlotPop(p) => ("slot_pop".into(), p.index()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-6
@@ -49,8 +49,14 @@ fn looping_actor_on_check_is_stopped() {
|
|||||||
}
|
}
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run during the cancellation unwind"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -79,8 +85,14 @@ fn parked_on_recv_actor_is_stopped() {
|
|||||||
}
|
}
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run on cancellation of a parked actor");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run on cancellation of a parked actor"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -185,6 +197,12 @@ fn stop_flagged_while_queued_lands_at_first_park() {
|
|||||||
.recv_timeout(Duration::from_secs(10))
|
.recv_timeout(Duration::from_secs(10))
|
||||||
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
|
.expect("runtime deadlocked: stop against a QUEUED actor was lost at its first park");
|
||||||
|
|
||||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
assert!(
|
||||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
saw_stopped.load(Ordering::SeqCst),
|
||||||
|
"expected DownReason::Stopped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dropped.load(Ordering::SeqCst),
|
||||||
|
"Drop guard must run during the cancellation unwind"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-26
@@ -24,7 +24,11 @@ fn progress_point_counts() {
|
|||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
let after = smarm::causal::progress_snapshot();
|
let after = smarm::causal::progress_snapshot();
|
||||||
let delta = |name: &str| {
|
let delta = |name: &str| {
|
||||||
after.iter().find(|(n, _)| n == name).map(|(_, c)| *c).unwrap()
|
after
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == name)
|
||||||
|
.map(|(_, c)| *c)
|
||||||
|
.unwrap()
|
||||||
- before
|
- before
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(n, _)| n == name)
|
.find(|(n, _)| n == name)
|
||||||
@@ -45,21 +49,12 @@ fn site_guard_nesting_restores() {
|
|||||||
assert_eq!(smarm::causal::current_site_name(), None);
|
assert_eq!(smarm::causal::current_site_name(), None);
|
||||||
{
|
{
|
||||||
let _outer = smarm::causal_site!("outer");
|
let _outer = smarm::causal_site!("outer");
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("outer")
|
|
||||||
);
|
|
||||||
{
|
{
|
||||||
let _inner = smarm::causal_site!("inner");
|
let _inner = smarm::causal_site!("inner");
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("inner"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("inner")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(smarm::causal::current_site_name().as_deref(), Some("outer"));
|
||||||
smarm::causal::current_site_name().as_deref(),
|
|
||||||
Some("outer")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert_eq!(smarm::causal::current_site_name(), None);
|
assert_eq!(smarm::causal::current_site_name(), None);
|
||||||
});
|
});
|
||||||
@@ -88,10 +83,7 @@ fn virtual_speedup_ledger() {
|
|||||||
let bystander = smarm::spawn(move || {
|
let bystander = smarm::spawn(move || {
|
||||||
while !stop2.load(Ordering::Relaxed) {
|
while !stop2.load(Ordering::Relaxed) {
|
||||||
smarm::check!();
|
smarm::check!();
|
||||||
out2.store(
|
out2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||||
smarm::causal::my_absorbed_delay_cycles(),
|
|
||||||
Ordering::Relaxed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,10 +158,7 @@ fn runnable_bystander_pays_delay() {
|
|||||||
while !stop_b.load(Ordering::Relaxed) {
|
while !stop_b.load(Ordering::Relaxed) {
|
||||||
iters2.fetch_add(1, Ordering::Relaxed);
|
iters2.fetch_add(1, Ordering::Relaxed);
|
||||||
smarm::check!();
|
smarm::check!();
|
||||||
absorbed2.store(
|
absorbed2.store(smarm::causal::my_absorbed_delay_cycles(), Ordering::Relaxed);
|
||||||
smarm::causal::my_absorbed_delay_cycles(),
|
|
||||||
Ordering::Relaxed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,8 +166,7 @@ fn runnable_bystander_pays_delay() {
|
|||||||
let i0 = iters.load(Ordering::Relaxed);
|
let i0 = iters.load(Ordering::Relaxed);
|
||||||
let t = std::time::Instant::now();
|
let t = std::time::Instant::now();
|
||||||
smarm::sleep(Duration::from_millis(150));
|
smarm::sleep(Duration::from_millis(150));
|
||||||
let rate =
|
let rate = (iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
||||||
(iters.load(Ordering::Relaxed) - i0) as f64 / t.elapsed().as_secs_f64();
|
|
||||||
out.store(rate as u64, Ordering::Relaxed);
|
out.store(rate as u64, Ordering::Relaxed);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -448,7 +436,10 @@ fn timer_deadline_shifts_with_injected_delay() {
|
|||||||
|
|
||||||
// Raw deadline passed, effective deadline not: nothing fires, entry kept.
|
// Raw deadline passed, effective deadline not: nothing fires, entry kept.
|
||||||
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty());
|
assert!(t.pop_due(now + Duration::from_millis(60)).is_empty());
|
||||||
assert!(!t.is_empty(), "shifted entry must be re-queued, not dropped");
|
assert!(
|
||||||
|
!t.is_empty(),
|
||||||
|
"shifted entry must be re-queued, not dropped"
|
||||||
|
);
|
||||||
|
|
||||||
// Past raw + injected (with margin) it must fire. Chase in case a
|
// Past raw + injected (with margin) it must fire. Chase in case a
|
||||||
// parallel test injected more debt meanwhile.
|
// parallel test injected more debt meanwhile.
|
||||||
@@ -503,7 +494,11 @@ fn wall_timer_ignores_injected_delay() {
|
|||||||
// Just past the raw deadline: the wall entry fires, the virtual one is
|
// Just past the raw deadline: the wall entry fires, the virtual one is
|
||||||
// re-queued at its shifted deadline.
|
// re-queued at its shifted deadline.
|
||||||
let due = t.pop_due(now + Duration::from_millis(60));
|
let due = t.pop_due(now + Duration::from_millis(60));
|
||||||
assert_eq!(due.len(), 1, "exactly the wall entry must fire at raw deadline");
|
assert_eq!(
|
||||||
|
due.len(),
|
||||||
|
1,
|
||||||
|
"exactly the wall entry must fire at raw deadline"
|
||||||
|
);
|
||||||
assert_eq!(due[0].pid, Pid::new(0, 0));
|
assert_eq!(due[0].pid, Pid::new(0, 0));
|
||||||
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
|
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
|
||||||
}
|
}
|
||||||
@@ -623,7 +618,11 @@ fn wall_send_after_ignores_injected_delay() {
|
|||||||
|
|
||||||
// Just past the raw deadline: only the wall send pops; run its thunk.
|
// Just past the raw deadline: only the wall send pops; run its thunk.
|
||||||
let due = t.pop_due(now + Duration::from_millis(60));
|
let due = t.pop_due(now + Duration::from_millis(60));
|
||||||
assert_eq!(due.len(), 1, "exactly the wall send must fire at raw deadline");
|
assert_eq!(
|
||||||
|
due.len(),
|
||||||
|
1,
|
||||||
|
"exactly the wall send must fire at raw deadline"
|
||||||
|
);
|
||||||
for e in due {
|
for e in due {
|
||||||
if let smarm::timer::Reason::Send { fire } = e.reason {
|
if let smarm::timer::Reason::Send { fire } = e.reason {
|
||||||
fire();
|
fire();
|
||||||
|
|||||||
+18
-10
@@ -154,7 +154,10 @@ fn channel_ops_interleaved_with_monitor_churn_multi_thread() {
|
|||||||
}
|
}
|
||||||
consumer.join().unwrap();
|
consumer.join().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), (0..32).sum::<i64>());
|
assert_eq!(
|
||||||
|
total.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
(0..32).sum::<i64>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -220,7 +223,10 @@ fn recv_timeout_reports_disconnected_on_close() {
|
|||||||
fn recv_timeout_zero_duration_is_a_bounded_poll() {
|
fn recv_timeout_zero_duration_is_a_bounded_poll() {
|
||||||
run(|| {
|
run(|| {
|
||||||
let (_tx, rx) = channel::<i64>();
|
let (_tx, rx) = channel::<i64>();
|
||||||
assert_eq!(rx.recv_timeout(Duration::ZERO), Err(RecvTimeoutError::Timeout));
|
assert_eq!(
|
||||||
|
rx.recv_timeout(Duration::ZERO),
|
||||||
|
Err(RecvTimeoutError::Timeout)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,15 +268,17 @@ fn recv_timeout_many_waiters_multi_thread() {
|
|||||||
let (tx, rx) = channel::<i64>();
|
let (tx, rx) = channel::<i64>();
|
||||||
let got = got2.clone();
|
let got = got2.clone();
|
||||||
let timed_out = timed_out2.clone();
|
let timed_out = timed_out2.clone();
|
||||||
handles.push(spawn(move || match rx.recv_timeout(Duration::from_millis(100)) {
|
handles.push(spawn(move || {
|
||||||
Ok(v) => {
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
assert_eq!(v, i);
|
Ok(v) => {
|
||||||
got.fetch_add(1, Ordering::Relaxed);
|
assert_eq!(v, i);
|
||||||
|
got.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
|
timed_out.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(e) => panic!("unexpected: {e}"),
|
||||||
}
|
}
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
|
||||||
timed_out.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
Err(e) => panic!("unexpected: {e}"),
|
|
||||||
}));
|
}));
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
handles.push(spawn(move || {
|
handles.push(spawn(move || {
|
||||||
|
|||||||
+30
-11
@@ -11,9 +11,15 @@ thread_local! {
|
|||||||
static LOG: Cell<u64> = const { Cell::new(0) };
|
static LOG: Cell<u64> = const { Cell::new(0) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(v: u64) { LOG.with(|c| c.set(c.get() | v)); }
|
fn log(v: u64) {
|
||||||
fn get_log() -> u64 { LOG.with(|c| c.get()) }
|
LOG.with(|c| c.set(c.get() | v));
|
||||||
fn reset_log() { LOG.with(|c| c.set(0)); }
|
}
|
||||||
|
fn get_log() -> u64 {
|
||||||
|
LOG.with(|c| c.get())
|
||||||
|
}
|
||||||
|
fn reset_log() {
|
||||||
|
LOG.with(|c| c.set(0));
|
||||||
|
}
|
||||||
|
|
||||||
extern "C-unwind" fn actor_simple() {
|
extern "C-unwind" fn actor_simple() {
|
||||||
log(0x1);
|
log(0x1);
|
||||||
@@ -56,7 +62,7 @@ fn actor_yields_and_resumes() {
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
||||||
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
||||||
|
|
||||||
extern "C-unwind" fn actor_reg_check() {
|
extern "C-unwind" fn actor_reg_check() {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -73,7 +79,10 @@ extern "C-unwind" fn actor_reg_check() {
|
|||||||
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
||||||
switch_to_scheduler();
|
switch_to_scheduler();
|
||||||
|
|
||||||
let a0: u64; let a1: u64; let a2: u64; let a3: u64;
|
let a0: u64;
|
||||||
|
let a1: u64;
|
||||||
|
let a2: u64;
|
||||||
|
let a3: u64;
|
||||||
core::arch::asm!(
|
core::arch::asm!(
|
||||||
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
||||||
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
||||||
@@ -88,8 +97,14 @@ fn callee_saved_registers_survive_yield() {
|
|||||||
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
let stack = Stack::new(64 * 1024, 4096).unwrap();
|
||||||
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
||||||
set_actor_sp(sp);
|
set_actor_sp(sp);
|
||||||
unsafe { switch_to_actor(); switch_to_actor(); }
|
unsafe {
|
||||||
assert_eq!(REG_BEFORE.get().copied().unwrap(), REG_AFTER.get().copied().unwrap());
|
switch_to_actor();
|
||||||
|
switch_to_actor();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
REG_BEFORE.get().copied().unwrap(),
|
||||||
|
REG_AFTER.get().copied().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two actors, independent stacks.
|
// Two actors, independent stacks.
|
||||||
@@ -123,14 +138,18 @@ fn two_actors_dont_corrupt_each_other() {
|
|||||||
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
||||||
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
||||||
|
|
||||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
set_actor_sp(sp_a);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
let sp_a = get_actor_sp();
|
let sp_a = get_actor_sp();
|
||||||
|
|
||||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
set_actor_sp(sp_b);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
let sp_b = get_actor_sp();
|
let sp_b = get_actor_sp();
|
||||||
|
|
||||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
set_actor_sp(sp_a);
|
||||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
unsafe { switch_to_actor() };
|
||||||
|
set_actor_sp(sp_b);
|
||||||
|
unsafe { switch_to_actor() };
|
||||||
|
|
||||||
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
||||||
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
//! Cross-thread wake: a thread that is *not* a smarm scheduler thread must be
|
||||||
|
//! able to wake (and stop) a parked actor.
|
||||||
|
//!
|
||||||
|
//! The gap this pins down: every off-runtime wake primitive (`unpark`,
|
||||||
|
//! `unpark_at`, `request_stop`) reaches the runtime through the `RUNTIME`
|
||||||
|
//! thread-local, which is `None` on any non-scheduler thread — so a wake
|
||||||
|
//! issued from a foreign OS thread is a silent no-op and the parked actor
|
||||||
|
//! sleeps forever. Both failure modes below manifest as `Runtime::run` never
|
||||||
|
//! returning, so each test is wrapped in a watchdog: a timeout is the failure.
|
||||||
|
//!
|
||||||
|
//! The fix mirrors RFC 018's IO backend — the waker reaches the runtime
|
||||||
|
//! through a `Weak<RuntimeInner>` it already holds (the receiver captures one
|
||||||
|
//! when it parks; `Runtime::handle()` hands one to an app thread).
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const WATCHDOG: Duration = Duration::from_secs(10);
|
||||||
|
/// Give the target actor time to actually park before the foreign thread pokes
|
||||||
|
/// it, so we exercise the *wake* of a parked actor rather than the entry-side
|
||||||
|
/// stop check.
|
||||||
|
const SETTLE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
|
||||||
|
/// A cross-thread `send` from a plain OS thread must wake a receiver parked in
|
||||||
|
/// `recv`. Under the thread-local-only wake path the send enqueues the message
|
||||||
|
/// but never wakes the receiver, so `recv` — and therefore `run` — hangs.
|
||||||
|
#[test]
|
||||||
|
fn foreign_thread_send_wakes_parked_receiver() {
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let rt = smarm::init(smarm::Config::exact(2));
|
||||||
|
rt.run(|| {
|
||||||
|
let (tx, rx) = smarm::channel::<u32>();
|
||||||
|
// Receiver actor: parks on recv until the foreign thread sends.
|
||||||
|
let h = smarm::spawn(move || {
|
||||||
|
assert_eq!(rx.recv().expect("recv"), 42);
|
||||||
|
});
|
||||||
|
// Foreign (non-scheduler) OS thread owns the Sender and sends
|
||||||
|
// after the receiver has parked.
|
||||||
|
let sender = thread::spawn(move || {
|
||||||
|
thread::sleep(SETTLE);
|
||||||
|
tx.send(42).expect("send");
|
||||||
|
});
|
||||||
|
let _ = h.join();
|
||||||
|
sender.join().expect("sender thread");
|
||||||
|
});
|
||||||
|
let _ = done_tx.send(());
|
||||||
|
});
|
||||||
|
done_rx
|
||||||
|
.recv_timeout(WATCHDOG)
|
||||||
|
.expect("run did not return: a foreign-thread send never woke the parked receiver");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cross-thread `request_stop` through a `RuntimeHandle` must wake and stop a
|
||||||
|
/// parked actor. The actor parks on a long sleep (only a stop can end it); the
|
||||||
|
/// handle is grabbed before `run` and driven from a foreign thread.
|
||||||
|
#[test]
|
||||||
|
fn foreign_thread_request_stop_wakes_parked_actor() {
|
||||||
|
assert_send_sync::<smarm::RuntimeHandle>();
|
||||||
|
|
||||||
|
let rt = smarm::init(smarm::Config::exact(2));
|
||||||
|
let handle = rt.handle();
|
||||||
|
|
||||||
|
// Foreign thread: learn the target pid from inside the run, let it park,
|
||||||
|
// then stop it through the handle.
|
||||||
|
let (pid_tx, pid_rx) = mpsc::channel::<smarm::Pid>();
|
||||||
|
let stopper = thread::spawn(move || {
|
||||||
|
let pid = pid_rx.recv().expect("pid");
|
||||||
|
thread::sleep(SETTLE);
|
||||||
|
handle.request_stop(pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
rt.run(move || {
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
// Parks indefinitely; only a cooperative stop unwinds it.
|
||||||
|
smarm::sleep(Duration::from_secs(3600));
|
||||||
|
});
|
||||||
|
pid_tx.send(h.pid()).expect("send pid");
|
||||||
|
let _ = h.join();
|
||||||
|
});
|
||||||
|
let _ = done_tx.send(());
|
||||||
|
});
|
||||||
|
|
||||||
|
done_rx
|
||||||
|
.recv_timeout(WATCHDOG)
|
||||||
|
.expect("run did not return: a foreign-thread request_stop never woke the parked actor");
|
||||||
|
stopper.join().expect("stopper thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `RuntimeHandle` held across (and beyond) a run must not keep the runtime
|
||||||
|
/// alive or block all-done: `run` still returns, and once the `Runtime` is
|
||||||
|
/// dropped the handle degrades to a harmless no-op (Weak lifecycle) rather than
|
||||||
|
/// panicking or touching freed memory.
|
||||||
|
#[test]
|
||||||
|
fn lingering_handle_does_not_block_all_done() {
|
||||||
|
let rt = smarm::init(smarm::Config::exact(1));
|
||||||
|
let handle = rt.handle(); // outlives the run below
|
||||||
|
|
||||||
|
let (pid_tx, pid_rx) = mpsc::channel::<smarm::Pid>();
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
let runner = thread::spawn(move || {
|
||||||
|
rt.run(move || {
|
||||||
|
let h = smarm::spawn(|| {});
|
||||||
|
pid_tx.send(h.pid()).expect("send pid");
|
||||||
|
let _ = h.join();
|
||||||
|
});
|
||||||
|
// `rt` is dropped here, at the end of this thread.
|
||||||
|
let _ = done_tx.send(());
|
||||||
|
});
|
||||||
|
|
||||||
|
done_rx
|
||||||
|
.recv_timeout(WATCHDOG)
|
||||||
|
.expect("run did not return while a RuntimeHandle was held live");
|
||||||
|
runner.join().expect("runner thread");
|
||||||
|
|
||||||
|
// Runtime is now dropped. A stop through the lingering handle must be a
|
||||||
|
// silent no-op, not a panic or use-after-free.
|
||||||
|
let dead_pid = pid_rx.recv().expect("pid");
|
||||||
|
handle.request_stop(dead_pid);
|
||||||
|
}
|
||||||
+22
-7
@@ -11,8 +11,8 @@
|
|||||||
//! OUTSIDE `run` — an in-actor assertion alone passes vacuously.
|
//! OUTSIDE `run` — an in-actor assertion alone passes vacuously.
|
||||||
|
|
||||||
use smarm::{
|
use smarm::{
|
||||||
channel, run, select, select_timeout, spawn, try_select, wait_readable,
|
channel, run, select, select_timeout, spawn, try_select, wait_readable, wait_readable_timeout,
|
||||||
wait_readable_timeout, wait_writable_timeout, yield_now, FdArm,
|
wait_writable_timeout, yield_now, FdArm,
|
||||||
};
|
};
|
||||||
use std::os::fd::RawFd;
|
use std::os::fd::RawFd;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
@@ -33,7 +33,10 @@ impl Pipe {
|
|||||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||||
assert_eq!(r, 0, "pipe2 failed");
|
assert_eq!(r, 0, "pipe2 failed");
|
||||||
Pipe { read: fds[0], write: fds[1] }
|
Pipe {
|
||||||
|
read: fds[0],
|
||||||
|
write: fds[1],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,12 +256,18 @@ fn wait_readable_timeout_times_out_then_succeeds_with_data() {
|
|||||||
let (rfd, wfd) = (p.read, p.write);
|
let (rfd, wfd) = (p.read, p.write);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_millis(30)).unwrap(), false);
|
assert_eq!(
|
||||||
|
wait_readable_timeout(rfd, Duration::from_millis(30)).unwrap(),
|
||||||
|
false
|
||||||
|
);
|
||||||
assert!(start.elapsed() >= Duration::from_millis(30));
|
assert!(start.elapsed() >= Duration::from_millis(30));
|
||||||
|
|
||||||
// Timed-out wait must leave the fd clean; ready path returns true.
|
// Timed-out wait must leave the fd clean; ready path returns true.
|
||||||
assert_eq!(raw_write(wfd, b"d"), 1);
|
assert_eq!(raw_write(wfd, b"d"), 1);
|
||||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(), true);
|
assert_eq!(
|
||||||
|
wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(),
|
||||||
|
true
|
||||||
|
);
|
||||||
let mut buf = [0u8; 1];
|
let mut buf = [0u8; 1];
|
||||||
assert_eq!(raw_read(rfd, &mut buf), 1);
|
assert_eq!(raw_read(rfd, &mut buf), 1);
|
||||||
ok2.store(true, Ordering::SeqCst);
|
ok2.store(true, Ordering::SeqCst);
|
||||||
@@ -274,7 +283,10 @@ fn wait_readable_timeout_wakes_on_late_data() {
|
|||||||
let p = Pipe::new();
|
let p = Pipe::new();
|
||||||
let (rfd, wfd) = (p.read, p.write);
|
let (rfd, wfd) = (p.read, p.write);
|
||||||
let h = spawn(move || {
|
let h = spawn(move || {
|
||||||
assert_eq!(wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(), true);
|
assert_eq!(
|
||||||
|
wait_readable_timeout(rfd, Duration::from_secs(5)).unwrap(),
|
||||||
|
true
|
||||||
|
);
|
||||||
let mut buf = [0u8; 1];
|
let mut buf = [0u8; 1];
|
||||||
assert_eq!(raw_read(rfd, &mut buf), 1);
|
assert_eq!(raw_read(rfd, &mut buf), 1);
|
||||||
got2.store(buf[0] as u32, Ordering::SeqCst);
|
got2.store(buf[0] as u32, Ordering::SeqCst);
|
||||||
@@ -292,7 +304,10 @@ fn wait_writable_timeout_ready_now_on_empty_pipe() {
|
|||||||
run(move || {
|
run(move || {
|
||||||
let p = Pipe::new();
|
let p = Pipe::new();
|
||||||
// An empty pipe's write end is writable: ready-now path, no park.
|
// An empty pipe's write end is writable: ready-now path, no park.
|
||||||
assert_eq!(wait_writable_timeout(p.write, Duration::from_secs(5)).unwrap(), true);
|
assert_eq!(
|
||||||
|
wait_writable_timeout(p.write, Duration::from_secs(5)).unwrap(),
|
||||||
|
true
|
||||||
|
);
|
||||||
ok2.store(true, Ordering::SeqCst);
|
ok2.store(true, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
assert!(ok.load(Ordering::SeqCst));
|
assert!(ok.load(Ordering::SeqCst));
|
||||||
|
|||||||
+64
-20
@@ -88,7 +88,7 @@ impl GenServer for Lifecycle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// init -> handle_call -> (drop last ref closes inbox) -> terminate.
|
// init -> handle_call -> shutdown -> terminate.
|
||||||
#[test]
|
#[test]
|
||||||
fn init_and_terminate_run() {
|
fn init_and_terminate_run() {
|
||||||
let log = Arc::new(Mutex::new(Vec::new()));
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
@@ -96,9 +96,9 @@ fn init_and_terminate_run() {
|
|||||||
run(move || {
|
run(move || {
|
||||||
let server = start(Lifecycle { log: log2 });
|
let server = start(Lifecycle { log: log2 });
|
||||||
server.call(()).unwrap();
|
server.call(()).unwrap();
|
||||||
// Dropping the only ref closes the inbox; the server breaks out of its
|
// Refs are addresses: dropping one does not end the server. The
|
||||||
// recv loop and runs terminate. run() will not return until it has.
|
// explicit close does, and waits for terminate.
|
||||||
drop(server);
|
server.shutdown();
|
||||||
});
|
});
|
||||||
assert_eq!(*log.lock().unwrap(), vec!["init", "call", "terminate"]);
|
assert_eq!(*log.lock().unwrap(), vec!["init", "call", "terminate"]);
|
||||||
}
|
}
|
||||||
@@ -403,7 +403,10 @@ fn worker_pool_down_reaches_handle_down() {
|
|||||||
let got = Arc::new(Mutex::new(Vec::new()));
|
let got = Arc::new(Mutex::new(Vec::new()));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
let server = start(Pool {
|
||||||
|
watcher: None,
|
||||||
|
log: Vec::new(),
|
||||||
|
});
|
||||||
server.cast(PoolCast::SpawnDoomedWorker).unwrap();
|
server.cast(PoolCast::SpawnDoomedWorker).unwrap();
|
||||||
let _ = server.call(()).unwrap(); // sync point: cast handled, worker live
|
let _ = server.call(()).unwrap(); // sync point: cast handled, worker live
|
||||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||||
@@ -421,7 +424,10 @@ fn watch_dead_pid_is_noproc_down() {
|
|||||||
let h = spawn(|| {});
|
let h = spawn(|| {});
|
||||||
let dead = h.pid();
|
let dead = h.pid();
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
let server = start(Pool {
|
||||||
|
watcher: None,
|
||||||
|
log: Vec::new(),
|
||||||
|
});
|
||||||
server.cast(PoolCast::Watch(dead)).unwrap();
|
server.cast(PoolCast::Watch(dead)).unwrap();
|
||||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||||
});
|
});
|
||||||
@@ -497,7 +503,12 @@ impl GenServer for Timed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn timed(fired: Arc<Mutex<Vec<u32>>>, cancel_won: Arc<Mutex<Option<bool>>>) -> Timed {
|
fn timed(fired: Arc<Mutex<Vec<u32>>>, cancel_won: Arc<Mutex<Option<bool>>>) -> Timed {
|
||||||
Timed { timer: None, fired, cancel_won, last: None }
|
Timed {
|
||||||
|
timer: None,
|
||||||
|
fired,
|
||||||
|
cancel_won,
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A one-shot armed from a handler fires into handle_timer with its payload.
|
// A one-shot armed from a handler fires into handle_timer with its payload.
|
||||||
@@ -534,7 +545,11 @@ fn cancel_before_fire_suppresses_it() {
|
|||||||
let count = server.call(()).unwrap();
|
let count = server.call(()).unwrap();
|
||||||
assert_eq!(count, 0, "cancelled timer must not fire");
|
assert_eq!(count, 0, "cancelled timer must not fire");
|
||||||
});
|
});
|
||||||
assert_eq!(*cancel_won.lock().unwrap(), Some(true), "cancel beat the fire");
|
assert_eq!(
|
||||||
|
*cancel_won.lock().unwrap(),
|
||||||
|
Some(true),
|
||||||
|
"cancel beat the fire"
|
||||||
|
);
|
||||||
assert!(fired.lock().unwrap().is_empty());
|
assert!(fired.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,11 +564,16 @@ fn tick_every_rearms_repeatedly() {
|
|||||||
run(move || {
|
run(move || {
|
||||||
let cw = Arc::new(Mutex::new(None));
|
let cw = Arc::new(Mutex::new(None));
|
||||||
let server = start(timed(f2, cw));
|
let server = start(timed(f2, cw));
|
||||||
server.cast(TkCast::Tick(Duration::from_millis(20))).unwrap();
|
server
|
||||||
|
.cast(TkCast::Tick(Duration::from_millis(20)))
|
||||||
|
.unwrap();
|
||||||
let _ = server.call(()).unwrap(); // sync: periodic armed
|
let _ = server.call(()).unwrap(); // sync: periodic armed
|
||||||
smarm::sleep(Duration::from_millis(130)); // ~6 periods
|
smarm::sleep(Duration::from_millis(130)); // ~6 periods
|
||||||
let count = server.call(()).unwrap();
|
let count = server.call(()).unwrap();
|
||||||
assert!(count >= 3, "periodic should have re-armed several times, got {count}");
|
assert!(
|
||||||
|
count >= 3,
|
||||||
|
"periodic should have re-armed several times, got {count}"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
// Every tick delivered the same payload.
|
// Every tick delivered the same payload.
|
||||||
assert!(fired.lock().unwrap().iter().all(|&v| v == 9));
|
assert!(fired.lock().unwrap().iter().all(|&v| v == 9));
|
||||||
@@ -568,7 +588,9 @@ fn cancel_stops_a_periodic() {
|
|||||||
let c2 = cancel_won.clone();
|
let c2 = cancel_won.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let server = start(timed(f2, c2));
|
let server = start(timed(f2, c2));
|
||||||
server.cast(TkCast::Tick(Duration::from_millis(20))).unwrap();
|
server
|
||||||
|
.cast(TkCast::Tick(Duration::from_millis(20)))
|
||||||
|
.unwrap();
|
||||||
let _ = server.call(()).unwrap();
|
let _ = server.call(()).unwrap();
|
||||||
smarm::sleep(Duration::from_millis(70)); // a few ticks
|
smarm::sleep(Duration::from_millis(70)); // a few ticks
|
||||||
server.cast(TkCast::CancelLast).unwrap();
|
server.cast(TkCast::CancelLast).unwrap();
|
||||||
@@ -616,11 +638,17 @@ fn idle_fires_repeatedly_on_quiet() {
|
|||||||
let idles = Arc::new(Mutex::new(0));
|
let idles = Arc::new(Mutex::new(0));
|
||||||
let i2 = idles.clone();
|
let i2 = idles.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let server = start(Idler { window: Duration::from_millis(25), idles: i2 });
|
let server = start(Idler {
|
||||||
|
window: Duration::from_millis(25),
|
||||||
|
idles: i2,
|
||||||
|
});
|
||||||
smarm::sleep(Duration::from_millis(130)); // quiet ⇒ ~5 windows
|
smarm::sleep(Duration::from_millis(130)); // quiet ⇒ ~5 windows
|
||||||
drop(server); // keep the server alive across the quiet span
|
drop(server); // keep the server alive across the quiet span
|
||||||
});
|
});
|
||||||
assert!(*idles.lock().unwrap() >= 2, "idle should re-arm and fire several times");
|
assert!(
|
||||||
|
*idles.lock().unwrap() >= 2,
|
||||||
|
"idle should re-arm and fire several times"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traffic within the window keeps idle from firing; only once the inbox goes
|
// Traffic within the window keeps idle from firing; only once the inbox goes
|
||||||
@@ -632,7 +660,10 @@ fn traffic_resets_the_idle_window() {
|
|||||||
let before_quiet = Arc::new(Mutex::new(u32::MAX));
|
let before_quiet = Arc::new(Mutex::new(u32::MAX));
|
||||||
let bq = before_quiet.clone();
|
let bq = before_quiet.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let server = start(Idler { window: Duration::from_millis(60), idles: i2 });
|
let server = start(Idler {
|
||||||
|
window: Duration::from_millis(60),
|
||||||
|
idles: i2,
|
||||||
|
});
|
||||||
// Poke every 25ms (< 60ms window) for ~100ms: each cast resets the
|
// Poke every 25ms (< 60ms window) for ~100ms: each cast resets the
|
||||||
// window before it can elapse.
|
// window before it can elapse.
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
@@ -643,8 +674,15 @@ fn traffic_resets_the_idle_window() {
|
|||||||
smarm::sleep(Duration::from_millis(140)); // now genuinely quiet
|
smarm::sleep(Duration::from_millis(140)); // now genuinely quiet
|
||||||
drop(server);
|
drop(server);
|
||||||
});
|
});
|
||||||
assert_eq!(*before_quiet.lock().unwrap(), 0, "steady traffic must suppress idle");
|
assert_eq!(
|
||||||
assert!(*idles.lock().unwrap() >= 1, "idle fires once the inbox falls quiet");
|
*before_quiet.lock().unwrap(),
|
||||||
|
0,
|
||||||
|
"steady traffic must suppress idle"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
*idles.lock().unwrap() >= 1,
|
||||||
|
"idle fires once the inbox falls quiet"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 015 §4.7 — no armed timer survives loop exit. A server with a live
|
// RFC 015 §4.7 — no armed timer survives loop exit. A server with a live
|
||||||
@@ -658,15 +696,21 @@ fn no_timer_survives_exit() {
|
|||||||
let f_read = fired.clone();
|
let f_read = fired.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let server = start(timed(f_server, Arc::new(Mutex::new(None))));
|
let server = start(timed(f_server, Arc::new(Mutex::new(None))));
|
||||||
server.cast(TkCast::Tick(Duration::from_millis(15))).unwrap();
|
server
|
||||||
|
.cast(TkCast::Tick(Duration::from_millis(15)))
|
||||||
|
.unwrap();
|
||||||
let _ = server.call(()).unwrap(); // sync: periodic armed
|
let _ = server.call(()).unwrap(); // sync: periodic armed
|
||||||
smarm::sleep(Duration::from_millis(45)); // a couple of ticks
|
smarm::sleep(Duration::from_millis(45)); // a couple of ticks
|
||||||
let mon = smarm::monitor(server.pid());
|
let mon = smarm::monitor(server.pid());
|
||||||
drop(server); // inbox closes → loop exits → guard drains timers
|
server.shutdown(); // loop exits → guard drains timers
|
||||||
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
|
||||||
assert!(mon.rx.recv().is_ok());
|
assert!(mon.rx.recv().is_ok());
|
||||||
let at_exit = f_read.lock().unwrap().len();
|
let at_exit = f_read.lock().unwrap().len();
|
||||||
smarm::sleep(Duration::from_millis(90)); // would be several more ticks
|
smarm::sleep(Duration::from_millis(90)); // would be several more ticks
|
||||||
assert_eq!(f_read.lock().unwrap().len(), at_exit, "no tick may fire after exit");
|
assert_eq!(
|
||||||
|
f_read.lock().unwrap().len(),
|
||||||
|
at_exit,
|
||||||
|
"no tick may fire after exit"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
//! gen_server lifetime is the actor's, not its refs' (OTP: a pid is an
|
||||||
|
//! address, a process lives until it stops, is shut down, or is killed).
|
||||||
|
//!
|
||||||
|
//! - Dropping the last `GenServerRef` does NOT end the server. It ends via
|
||||||
|
//! `StopHandle::stop`, `request_shutdown` / `GenServerRef::shutdown`,
|
||||||
|
//! `request_stop`, or a handler panic.
|
||||||
|
//! - `GenServerBuilder::named(N).run()` runs the loop inline as the *current*
|
||||||
|
//! actor, so a server is a direct `ChildSpec` child: the supervisor's
|
||||||
|
//! shutdown reaches it as `handle_shutdown`, a restart re-binds the name,
|
||||||
|
//! and by-name `call`/`cast` reach whichever incarnation is live.
|
||||||
|
|
||||||
|
use smarm::gen_server::{
|
||||||
|
self, GenServer, GenServerBuilder, GenServerCtx, GenServerName, ShutdownAction, StopHandle,
|
||||||
|
};
|
||||||
|
use smarm::registry::RegisterError;
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
|
||||||
|
use smarm::{monitor, request_shutdown, run, sleep, spawn, DownReason};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Log(Arc<Mutex<Vec<String>>>);
|
||||||
|
impl Log {
|
||||||
|
fn push(&self, s: impl Into<String>) {
|
||||||
|
self.0.lock().unwrap().push(s.into());
|
||||||
|
}
|
||||||
|
fn get(&self) -> Vec<String> {
|
||||||
|
self.0.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Counter {
|
||||||
|
log: Log,
|
||||||
|
n: u64,
|
||||||
|
trap: bool,
|
||||||
|
stop: Option<StopHandle<Counter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Call {
|
||||||
|
Get,
|
||||||
|
}
|
||||||
|
enum Cast {
|
||||||
|
Inc,
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenServer for Counter {
|
||||||
|
type Call = Call;
|
||||||
|
type Reply = u64;
|
||||||
|
type Cast = Cast;
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
|
if self.trap {
|
||||||
|
ctx.trap_exit();
|
||||||
|
}
|
||||||
|
self.stop = Some(ctx.stop_handle());
|
||||||
|
self.log.push("init");
|
||||||
|
}
|
||||||
|
fn handle_call(&mut self, Call::Get: Call) -> u64 {
|
||||||
|
self.n
|
||||||
|
}
|
||||||
|
fn handle_cast(&mut self, c: Cast) {
|
||||||
|
match c {
|
||||||
|
Cast::Inc => self.n += 1,
|
||||||
|
Cast::Stop => self.stop.as_ref().unwrap().stop(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_shutdown(&mut self) -> ShutdownAction {
|
||||||
|
self.log.push("handle_shutdown");
|
||||||
|
ShutdownAction::Exit
|
||||||
|
}
|
||||||
|
fn terminate(&mut self) {
|
||||||
|
self.log.push("terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn counter(log: &Log, trap: bool) -> Counter {
|
||||||
|
Counter {
|
||||||
|
log: log.clone(),
|
||||||
|
n: 0,
|
||||||
|
trap,
|
||||||
|
stop: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Refs are addresses: dropping the last one does not end the server.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropping_last_ref_does_not_end_server() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let srv = gen_server::start(counter(&l, true));
|
||||||
|
let pid = srv.pid();
|
||||||
|
srv.cast(Cast::Inc).unwrap();
|
||||||
|
assert_eq!(srv.call(Call::Get).unwrap(), 1);
|
||||||
|
let mon = monitor(pid);
|
||||||
|
drop(srv);
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
assert!(
|
||||||
|
mon.rx.try_recv().unwrap().is_none(),
|
||||||
|
"server must outlive its last ref"
|
||||||
|
);
|
||||||
|
assert_eq!(l.get(), vec!["init"], "terminate must not have run");
|
||||||
|
// Explicit teardown still works, and is what ends it.
|
||||||
|
request_shutdown(pid);
|
||||||
|
let down = mon.rx.recv().unwrap();
|
||||||
|
assert_eq!(down.reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["init", "handle_shutdown", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ref_shutdown_is_the_explicit_close() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let srv = gen_server::start(counter(&l, true));
|
||||||
|
srv.call(Call::Get).unwrap(); // sync: init (and trap_exit) has run
|
||||||
|
srv.shutdown(); // graceful, waits
|
||||||
|
assert_eq!(l.get(), vec!["init", "handle_shutdown", "terminate"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forgotten_server_is_shut_down_at_root_exit() {
|
||||||
|
// A ref-less server is not a hung run: root exit shuts it down.
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let srv = gen_server::start(counter(&l, false));
|
||||||
|
drop(srv);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["init", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Inline run: a gen_server as a direct ChildSpec child.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const COUNTER: GenServerName<Counter> = GenServerName::new("lifetime-counter");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_run_is_a_direct_supervised_child_and_gets_shutdown() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let l2 = l.clone();
|
||||||
|
let sup = spawn(move || {
|
||||||
|
let l3 = l2.clone();
|
||||||
|
OneForOne::new()
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
GenServerBuilder::new(counter(&l3, true))
|
||||||
|
.named(COUNTER)
|
||||||
|
.run()
|
||||||
|
.expect("name free");
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Infinity),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
gen_server::cast(COUNTER, Cast::Inc).unwrap();
|
||||||
|
assert_eq!(gen_server::call(COUNTER, Call::Get).unwrap(), 1);
|
||||||
|
request_shutdown(sup.pid());
|
||||||
|
sup.join()
|
||||||
|
.expect("ordered shutdown, supervisor returns normally");
|
||||||
|
assert_eq!(l.get(), vec!["init", "handle_shutdown", "terminate"]);
|
||||||
|
assert!(gen_server::whereis_server(COUNTER).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_run_child_restarts_and_rebinds_name() {
|
||||||
|
let log = Log::default();
|
||||||
|
let inits = Arc::new(AtomicUsize::new(0));
|
||||||
|
let l = log.clone();
|
||||||
|
let i = inits.clone();
|
||||||
|
run(move || {
|
||||||
|
let l2 = l.clone();
|
||||||
|
let i2 = i.clone();
|
||||||
|
let sup = spawn(move || {
|
||||||
|
let l3 = l2.clone();
|
||||||
|
let i3 = i2.clone();
|
||||||
|
OneForOne::new()
|
||||||
|
.child(ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
i3.fetch_add(1, Ordering::SeqCst);
|
||||||
|
GenServerBuilder::new(counter(&l3, false))
|
||||||
|
.named(COUNTER)
|
||||||
|
.run()
|
||||||
|
.expect("name free on (re)start");
|
||||||
|
}))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
gen_server::cast(COUNTER, Cast::Inc).unwrap();
|
||||||
|
assert_eq!(gen_server::call(COUNTER, Call::Get).unwrap(), 1);
|
||||||
|
// Normal self-exit → Permanent restarts it, fresh state, same name.
|
||||||
|
gen_server::cast(COUNTER, Cast::Stop).unwrap();
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
assert_eq!(i.load(Ordering::SeqCst), 2, "restarted once");
|
||||||
|
assert_eq!(gen_server::call(COUNTER, Call::Get).unwrap(), 0);
|
||||||
|
request_shutdown(sup.pid());
|
||||||
|
sup.join().unwrap();
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["init", "terminate", "init", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_run_name_clash_fails_before_init() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let first = GenServerBuilder::new(counter(&l, false))
|
||||||
|
.named(COUNTER)
|
||||||
|
.start()
|
||||||
|
.unwrap();
|
||||||
|
let l2 = l.clone();
|
||||||
|
let res = Arc::new(Mutex::new(None));
|
||||||
|
let r2 = res.clone();
|
||||||
|
let first_pid = first.pid();
|
||||||
|
spawn(move || {
|
||||||
|
let r = GenServerBuilder::new(counter(&l2, false))
|
||||||
|
.named(COUNTER)
|
||||||
|
.run();
|
||||||
|
*r2.lock().unwrap() = Some(r);
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
*res.lock().unwrap(),
|
||||||
|
Some(Err(RegisterError::NameTaken { holder: first_pid }))
|
||||||
|
);
|
||||||
|
assert_eq!(l.get(), vec!["init"], "clashing server never ran init");
|
||||||
|
first.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
//! gen_server graceful shutdown.
|
||||||
|
//!
|
||||||
|
//! - A server that does not opt in (`ctx.trap_exit()` in `init`) is stopped
|
||||||
|
//! outright by `request_shutdown`, exactly as by `request_stop`.
|
||||||
|
//! - A trapping server receives the request as `handle_shutdown`. The default
|
||||||
|
//! returns `ShutdownAction::Exit`: the loop breaks and `terminate` runs on
|
||||||
|
//! the normal (non-unwind) path, so it may block. `Continue` keeps the loop
|
||||||
|
//! dispatching; the state later ends itself with a `StopHandle` — the only
|
||||||
|
//! way for a gen_server to exit *normally* on its own (`request_stop` on
|
||||||
|
//! self is an abnormal `Stopped`, which `Transient` restarts).
|
||||||
|
//! - Other exit signals (linked peers dying) reach a trapping server via
|
||||||
|
//! `handle_exit`.
|
||||||
|
|
||||||
|
use smarm::gen_server::{
|
||||||
|
start, GenServer, GenServerBuilder, GenServerCtx, GenServerRef, ShutdownAction, StopHandle,
|
||||||
|
};
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart};
|
||||||
|
use smarm::{link, monitor, request_shutdown, run, self_pid, sleep, spawn, DownReason, ExitSignal};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Log {
|
||||||
|
events: Arc<Mutex<Vec<&'static str>>>,
|
||||||
|
}
|
||||||
|
impl Log {
|
||||||
|
fn push(&self, e: &'static str) {
|
||||||
|
self.events.lock().unwrap().push(e);
|
||||||
|
}
|
||||||
|
fn get(&self) -> Vec<&'static str> {
|
||||||
|
self.events.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A server with configurable shutdown behaviour.
|
||||||
|
struct Srv {
|
||||||
|
log: Log,
|
||||||
|
trap: bool,
|
||||||
|
action: ShutdownAction,
|
||||||
|
stop: Option<StopHandle<Srv>>,
|
||||||
|
exits: Arc<Mutex<Vec<ExitSignal>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Srv {
|
||||||
|
fn new(log: &Log, trap: bool, action: ShutdownAction) -> Self {
|
||||||
|
Srv {
|
||||||
|
log: log.clone(),
|
||||||
|
trap,
|
||||||
|
action,
|
||||||
|
stop: None,
|
||||||
|
exits: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Cast {
|
||||||
|
Note(&'static str),
|
||||||
|
StopNow,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenServer for Srv {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = Cast;
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
|
if self.trap {
|
||||||
|
ctx.trap_exit();
|
||||||
|
}
|
||||||
|
self.stop = Some(ctx.stop_handle());
|
||||||
|
}
|
||||||
|
fn handle_call(&mut self, _: ()) {}
|
||||||
|
fn handle_cast(&mut self, c: Cast) {
|
||||||
|
match c {
|
||||||
|
Cast::Note(s) => self.log.push(s),
|
||||||
|
Cast::StopNow => self.stop.as_ref().unwrap().stop(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_shutdown(&mut self) -> ShutdownAction {
|
||||||
|
self.log.push("handle_shutdown");
|
||||||
|
self.action
|
||||||
|
}
|
||||||
|
fn handle_exit(&mut self, sig: ExitSignal) {
|
||||||
|
self.log.push("handle_exit");
|
||||||
|
self.exits.lock().unwrap().push(sig);
|
||||||
|
}
|
||||||
|
fn terminate(&mut self) {
|
||||||
|
// Allowed to block on the graceful path.
|
||||||
|
if self.trap {
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
self.log.push("terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_settled<G: GenServer>(state: G) -> GenServerRef<G> {
|
||||||
|
let r = start(state);
|
||||||
|
sleep(Duration::from_millis(20)); // let init (trap_exit) run
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_trapping_server_is_stopped_outright() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = spawn_settled(Srv::new(&l, false, ShutdownAction::Exit));
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
let d = mon.rx.recv().unwrap();
|
||||||
|
assert_eq!(d.reason, DownReason::Stopped);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trapping_server_exits_normally_via_handle_shutdown() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = spawn_settled(Srv::new(&l, true, ShutdownAction::Exit));
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
let d = mon.rx.recv().unwrap();
|
||||||
|
assert_eq!(d.reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["handle_shutdown", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn continue_keeps_dispatching_until_stop_handle() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = spawn_settled(Srv::new(&l, true, ShutdownAction::Continue));
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
r.cast(Cast::Note("after-shutdown-request")).unwrap();
|
||||||
|
r.cast(Cast::StopNow).unwrap();
|
||||||
|
let d = mon.rx.recv().unwrap();
|
||||||
|
assert_eq!(d.reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
log.get(),
|
||||||
|
vec!["handle_shutdown", "after-shutdown-request", "terminate"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_handle_is_a_normal_exit_that_transient_does_not_restart() {
|
||||||
|
let starts = Arc::new(AtomicUsize::new(0));
|
||||||
|
let s = starts.clone();
|
||||||
|
run(move || {
|
||||||
|
let s2 = s.clone();
|
||||||
|
let sup = spawn(move || {
|
||||||
|
let s3 = s2.clone();
|
||||||
|
OneForOne::new()
|
||||||
|
.child(ChildSpec::new(Restart::Transient, move || {
|
||||||
|
s3.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let log = Log::default();
|
||||||
|
let r = GenServerBuilder::new(Srv::new(&log, false, ShutdownAction::Exit))
|
||||||
|
.under(self_pid())
|
||||||
|
.start();
|
||||||
|
r.cast(Cast::StopNow).unwrap();
|
||||||
|
// Block until the server is gone; a bare spawn parent
|
||||||
|
// returning would not itself end the server.
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
let _ = mon.rx.recv();
|
||||||
|
}))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
sup.join().unwrap(); // returns only if the child was not restarted forever
|
||||||
|
});
|
||||||
|
assert_eq!(starts.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linked_peer_death_reaches_handle_exit() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
let alive = Arc::new(AtomicBool::new(false));
|
||||||
|
let a = alive.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = spawn_settled(Srv::new(&l, true, ShutdownAction::Exit));
|
||||||
|
let pid = r.pid();
|
||||||
|
let peer = spawn(move || {
|
||||||
|
link(pid);
|
||||||
|
panic!("peer dies");
|
||||||
|
});
|
||||||
|
let _ = peer.join();
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
r.cast(Cast::Note("still-serving")).unwrap();
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
a.store(true, Ordering::SeqCst);
|
||||||
|
r.shutdown(); // graceful; waits for terminate
|
||||||
|
});
|
||||||
|
assert!(alive.load(Ordering::SeqCst));
|
||||||
|
assert_eq!(
|
||||||
|
log.get(),
|
||||||
|
vec![
|
||||||
|
"handle_exit",
|
||||||
|
"still-serving",
|
||||||
|
"handle_shutdown",
|
||||||
|
"terminate"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_server_ref_shutdown_is_graceful_for_a_trapping_server() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = spawn_settled(Srv::new(&l, true, ShutdownAction::Exit));
|
||||||
|
r.shutdown();
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["handle_shutdown", "terminate"]);
|
||||||
|
}
|
||||||
+91
-15
@@ -100,7 +100,15 @@ fn state_timeout_fires() {
|
|||||||
let got = Arc::new(Mutex::new(0u32));
|
let got = Arc::new(Mutex::new(0u32));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 5 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 5,
|
||||||
|
},
|
||||||
|
);
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 5ms state-timeout
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 5ms state-timeout
|
||||||
smarm::sleep(Duration::from_millis(40)); // let it fire
|
smarm::sleep(Duration::from_millis(40)); // let it fire
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||||
@@ -116,13 +124,25 @@ fn state_timeout_auto_resets_on_transition() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
// Long window so the explicit Disarm beats it comfortably.
|
// Long window so the explicit Disarm beats it comfortably.
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 50 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 50,
|
||||||
|
},
|
||||||
|
);
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 50ms state-timeout
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed, arms 50ms state-timeout
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle, auto-resets it
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle, auto-resets it
|
||||||
smarm::sleep(Duration::from_millis(80)); // past the original window
|
smarm::sleep(Duration::from_millis(80)); // past the original window
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), 0, "auto-reset cancelled the pending state-timeout");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
0,
|
||||||
|
"auto-reset cancelled the pending state-timeout"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A named timeout survives a state change: armed in Idle, it still fires after
|
// A named timeout survives a state change: armed in Idle, it still fires after
|
||||||
@@ -133,14 +153,26 @@ fn named_timeout_survives_transition() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
// Armed's own state-timeout is long so it doesn't interfere.
|
// Armed's own state-timeout is long so it doesn't interfere.
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 200 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
m.send(Ev2::Cast(TCast::Ping(20))).unwrap(); // arm "ping" for 20ms (in Idle)
|
m.send(Ev2::Cast(TCast::Ping(20))).unwrap(); // arm "ping" for 20ms (in Idle)
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed (ping must survive this)
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // -> Armed (ping must survive this)
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle (and this)
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // -> Idle (and this)
|
||||||
smarm::sleep(Duration::from_millis(60)); // let "ping" fire
|
smarm::sleep(Duration::from_millis(60)); // let "ping" fire
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), 1, "named timeout fired across the transitions");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
1,
|
||||||
|
"named timeout fired across the transitions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancelling a named timeout before its window prevents the fire.
|
// Cancelling a named timeout before its window prevents the fire.
|
||||||
@@ -149,13 +181,25 @@ fn named_timeout_cancel() {
|
|||||||
let got = Arc::new(Mutex::new(99u32));
|
let got = Arc::new(Mutex::new(99u32));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 200 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
m.send(Ev2::Cast(TCast::Ping(30))).unwrap(); // arm "ping" for 30ms
|
m.send(Ev2::Cast(TCast::Ping(30))).unwrap(); // arm "ping" for 30ms
|
||||||
m.send(Ev2::Cast(TCast::CancelPing)).unwrap(); // cancel before it fires
|
m.send(Ev2::Cast(TCast::CancelPing)).unwrap(); // cancel before it fires
|
||||||
smarm::sleep(Duration::from_millis(60)); // past the original window
|
smarm::sleep(Duration::from_millis(60)); // past the original window
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::NamedFires(r))).unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), 0, "cancel prevented the named-timeout fire");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
0,
|
||||||
|
"cancel prevented the named-timeout fire"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -170,15 +214,27 @@ fn cast_then_call_roundtrip() {
|
|||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
// Long state-timeout window so it never fires during the test.
|
// Long state-timeout window so it never fires during the test.
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 });
|
let m = TimerSm::start(
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||||
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
m.send(Ev2::Cast(TCast::Arm)).unwrap(); // Idle -> Armed (enter)
|
||||||
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
m.send(Ev2::Cast(TCast::Disarm)).unwrap(); // Armed -> Idle (enter)
|
||||||
// enters = 1 (start) + 4 transitions = 5.
|
// enters = 1 (start) + 4 transitions = 5.
|
||||||
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
*got2.lock().unwrap() = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), 5, "one enter on start, one per real transition");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
5,
|
||||||
|
"one enter on start, one per real transition"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// `enter` fires once on start and once per *real* transition; a stay (a call
|
// `enter` fires once on start and once per *real* transition; a stay (a call
|
||||||
@@ -188,7 +244,15 @@ fn enter_on_start_and_each_transition_but_not_stay() {
|
|||||||
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
let got = Arc::new(Mutex::new((0u32, 0u32, 0u32)));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 }); // enter -> 1
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
); // enter -> 1
|
||||||
let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
let after_start = m.call(|r| Ev2::Call(TCall::Enters(r))).unwrap();
|
||||||
// A stay (a counter read returns `prev`) must not bump enters.
|
// A stay (a counter read returns `prev`) must not bump enters.
|
||||||
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
let _ = m.call(|r| Ev2::Call(TCall::StFires(r))).unwrap();
|
||||||
@@ -208,7 +272,15 @@ fn call_to_panicking_handler_is_down() {
|
|||||||
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
let got = Arc::new(Mutex::new(None::<Result<u32, CallError>>));
|
||||||
let got2 = got.clone();
|
let got2 = got.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let m = TimerSm::start(T::Idle, TData { enters: 0, st_fires: 0, named_fires: 0, st_window: 10_000 });
|
let m = TimerSm::start(
|
||||||
|
T::Idle,
|
||||||
|
TData {
|
||||||
|
enters: 0,
|
||||||
|
st_fires: 0,
|
||||||
|
named_fires: 0,
|
||||||
|
st_window: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
|
let r = m.call(|rep| Ev2::Call(TCall::Boom(rep)));
|
||||||
*got2.lock().unwrap() = Some(r);
|
*got2.lock().unwrap() = Some(r);
|
||||||
});
|
});
|
||||||
@@ -299,7 +371,11 @@ fn postponed_call_answered_after_transition() {
|
|||||||
smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
|
smarm::sleep(Duration::from_millis(20)); // let the child wake with its reply
|
||||||
*g2.lock().unwrap() = *taken.lock().unwrap();
|
*g2.lock().unwrap() = *taken.lock().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(*got.lock().unwrap(), Some(42), "postponed call answered by the Filled state");
|
assert_eq!(
|
||||||
|
*got.lock().unwrap(),
|
||||||
|
Some(42),
|
||||||
|
"postponed call answered by the Filled state"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//! gen_statem lifetime parity with gen_server: a machine lives until it
|
||||||
|
//! stops, is shut down, or is killed — its refs are addresses. And
|
||||||
|
//! `gen_statem::run_named` runs a machine inline as the current actor, so it
|
||||||
|
//! is a direct `ChildSpec` child addressed by name.
|
||||||
|
|
||||||
|
use smarm::gen_statem::{self, GenStatemName, Reply};
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
|
||||||
|
use smarm::{monitor, request_shutdown, run, sleep, spawn, DownReason};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Log(Arc<Mutex<Vec<&'static str>>>);
|
||||||
|
impl Log {
|
||||||
|
fn push(&self, e: &'static str) {
|
||||||
|
self.0.lock().unwrap().push(e);
|
||||||
|
}
|
||||||
|
fn get(&self) -> Vec<&'static str> {
|
||||||
|
self.0.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum S {
|
||||||
|
On,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct D {
|
||||||
|
log: Log,
|
||||||
|
trap: bool,
|
||||||
|
n: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Cast {
|
||||||
|
Inc,
|
||||||
|
StopNow,
|
||||||
|
}
|
||||||
|
enum Call {
|
||||||
|
Get(Reply<u64>),
|
||||||
|
}
|
||||||
|
|
||||||
|
smarm::gen_statem! {
|
||||||
|
machine: Sm { state: S, data: D };
|
||||||
|
event: Ev { cast: Cast, call: Call, info: () };
|
||||||
|
context(data, prev, cx);
|
||||||
|
|
||||||
|
enter {
|
||||||
|
S::On => { data.log.push("enter"); if data.trap { cx.trap_exit() } },
|
||||||
|
}
|
||||||
|
|
||||||
|
on S::On => {
|
||||||
|
cast Cast::Inc => { data.n += 1; prev },
|
||||||
|
cast Cast::StopNow => stop,
|
||||||
|
call Call::Get(r) => { r.reply(data.n); prev },
|
||||||
|
shutdown => { data.log.push("shutdown"); cx.stop(); prev },
|
||||||
|
state_timeout => unhandled,
|
||||||
|
timeout _ => unhandled,
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate { data.log.push("terminate"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn d(log: &Log, trap: bool) -> D {
|
||||||
|
D {
|
||||||
|
log: log.clone(),
|
||||||
|
trap,
|
||||||
|
n: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropping_last_ref_does_not_end_machine() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let m = Sm::start(S::On, d(&l, true));
|
||||||
|
let pid = m.pid();
|
||||||
|
m.send(Ev::Cast(Cast::Inc)).unwrap();
|
||||||
|
assert_eq!(m.call(|r| Ev::Call(Call::Get(r))).unwrap(), 1);
|
||||||
|
let mon = monitor(pid);
|
||||||
|
drop(m);
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
assert!(
|
||||||
|
mon.rx.try_recv().unwrap().is_none(),
|
||||||
|
"machine must outlive its refs"
|
||||||
|
);
|
||||||
|
assert_eq!(l.get(), vec!["enter"]);
|
||||||
|
request_shutdown(pid);
|
||||||
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["enter", "shutdown", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SM: GenStatemName<Sm> = GenStatemName::new("lifetime-sm");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_named_is_a_direct_supervised_child() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let l2 = l.clone();
|
||||||
|
let sup = spawn(move || {
|
||||||
|
let l3 = l2.clone();
|
||||||
|
OneForOne::new()
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
gen_statem::run_named(SM, Sm::new(S::On, d(&l3, true))).expect("name free");
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Infinity),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
gen_statem::send(SM, Ev::Cast(Cast::Inc)).unwrap();
|
||||||
|
assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 1);
|
||||||
|
// Normal self-exit → Permanent restart → fresh data, same name.
|
||||||
|
gen_statem::send(SM, Ev::Cast(Cast::StopNow)).unwrap();
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 0);
|
||||||
|
request_shutdown(sup.pid());
|
||||||
|
sup.join().unwrap();
|
||||||
|
assert!(gen_statem::whereis_machine(SM).is_none());
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
log.get(),
|
||||||
|
vec!["enter", "terminate", "enter", "shutdown", "terminate"]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//! gen_statem graceful shutdown — the gen_server surface, in state-machine
|
||||||
|
//! clothes. Where gen_server routes a shutdown request to a `handle_shutdown`
|
||||||
|
//! method, a gen_statem gets it as an **event** so it can be routed by state:
|
||||||
|
//!
|
||||||
|
//! - A machine that does not opt in (`cx.trap_exit()` in the initial `enter`)
|
||||||
|
//! is stopped outright by `request_shutdown`, exactly as by `request_stop`.
|
||||||
|
//! - A trapping machine sees the request as a `shutdown` row (a unit event
|
||||||
|
//! like `state_timeout`). The macro's default, when a state writes no
|
||||||
|
//! `shutdown` row, is `stop` — the loop breaks and `terminate` runs on the
|
||||||
|
//! normal path. A row may instead transition (e.g. into a Draining state)
|
||||||
|
//! and `stop` later from any row via the `stop` tail keyword.
|
||||||
|
//! - Linked-peer deaths reach a trapping machine as `exit <pat>` rows; an
|
||||||
|
//! unmatched exit is silently dropped, like an unmatched info.
|
||||||
|
//! - `terminate { … }` is an optional macro block, run on every exit path.
|
||||||
|
|
||||||
|
use smarm::gen_statem;
|
||||||
|
use smarm::gen_statem::{GenStatemRef, Reply};
|
||||||
|
use smarm::{link, monitor, request_shutdown, run, sleep, spawn, DownReason, ExitSignal};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Log(Arc<Mutex<Vec<&'static str>>>);
|
||||||
|
impl Log {
|
||||||
|
fn push(&self, e: &'static str) {
|
||||||
|
self.0.lock().unwrap().push(e);
|
||||||
|
}
|
||||||
|
fn get(&self) -> Vec<&'static str> {
|
||||||
|
self.0.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum S {
|
||||||
|
Idle,
|
||||||
|
Draining,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct D {
|
||||||
|
log: Log,
|
||||||
|
trap: bool,
|
||||||
|
exits: Vec<ExitSignal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Cast {
|
||||||
|
Note(&'static str),
|
||||||
|
StopNow,
|
||||||
|
}
|
||||||
|
enum Call {
|
||||||
|
Exits(Reply<usize>),
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_statem! {
|
||||||
|
machine: Sm { state: S, data: D };
|
||||||
|
event: Ev { cast: Cast, call: Call, info: () };
|
||||||
|
context(data, prev, cx);
|
||||||
|
|
||||||
|
enter {
|
||||||
|
S::Idle => if data.trap { cx.trap_exit() },
|
||||||
|
S::Draining => { data.log.push("draining"); cx.state_timeout(Duration::from_millis(30)); },
|
||||||
|
}
|
||||||
|
|
||||||
|
on S::Idle => {
|
||||||
|
// Shutdown in Idle: go drain first, stop later.
|
||||||
|
shutdown => S::Draining,
|
||||||
|
cast Cast::StopNow => stop,
|
||||||
|
state_timeout => unhandled,
|
||||||
|
}
|
||||||
|
|
||||||
|
on S::Draining => {
|
||||||
|
// Drained: end the machine normally.
|
||||||
|
state_timeout => { data.log.push("drained"); cx.stop(); prev },
|
||||||
|
// A second request while draining is ignored.
|
||||||
|
shutdown => unhandled,
|
||||||
|
cast Cast::StopNow => stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
on _ => {
|
||||||
|
cast Cast::Note(s) => { data.log.push(s); prev },
|
||||||
|
call Call::Exits(r) => { r.reply(data.exits.len()); prev },
|
||||||
|
exit sig => { data.log.push("exit"); data.exits.push(sig); prev },
|
||||||
|
timeout _ => unhandled,
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate {
|
||||||
|
data.log.push("terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A machine with no `shutdown` rows at all: the macro default applies.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum P {
|
||||||
|
On,
|
||||||
|
}
|
||||||
|
struct PD {
|
||||||
|
log: Log,
|
||||||
|
}
|
||||||
|
enum PCast {}
|
||||||
|
enum PCall {}
|
||||||
|
|
||||||
|
gen_statem! {
|
||||||
|
machine: Plain { state: P, data: PD };
|
||||||
|
event: PEv { cast: PCast, call: PCall, info: () };
|
||||||
|
context(data, prev, cx);
|
||||||
|
enter { P::On => cx.trap_exit(), }
|
||||||
|
on P::On => {
|
||||||
|
cast _ => unhandled,
|
||||||
|
call _ => unhandled,
|
||||||
|
state_timeout => unhandled,
|
||||||
|
timeout _ => unhandled,
|
||||||
|
}
|
||||||
|
terminate { data.log.push("terminate"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settled(log: &Log, trap: bool) -> GenStatemRef<Sm> {
|
||||||
|
let r = Sm::start(
|
||||||
|
S::Idle,
|
||||||
|
D {
|
||||||
|
log: log.clone(),
|
||||||
|
trap,
|
||||||
|
exits: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(20)); // let on_start (trap_exit) run
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_trapping_machine_is_stopped_outright() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = settled(&l, false);
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Stopped);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shutdown_row_routes_by_state_and_stop_tail_exits_normally() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = settled(&l, true);
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
// The second request lands in Draining and is `unhandled` (ignored).
|
||||||
|
sleep(Duration::from_millis(5));
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["draining", "drained", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_shutdown_is_stop() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = Plain::start(P::On, PD { log: l });
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
request_shutdown(r.pid());
|
||||||
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_tail_from_a_cast_is_a_normal_exit() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = settled(&l, false);
|
||||||
|
let mon = monitor(r.pid());
|
||||||
|
r.send(Ev::Cast(Cast::Note("a"))).unwrap();
|
||||||
|
r.send(Ev::Cast(Cast::StopNow)).unwrap();
|
||||||
|
r.send(Ev::Cast(Cast::Note("after-stop"))).unwrap(); // never dispatched
|
||||||
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit);
|
||||||
|
});
|
||||||
|
assert_eq!(log.get(), vec!["a", "terminate"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linked_peer_death_reaches_exit_row() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = settled(&l, true);
|
||||||
|
let pid = r.pid();
|
||||||
|
let peer = spawn(move || {
|
||||||
|
link(pid);
|
||||||
|
panic!("peer dies");
|
||||||
|
});
|
||||||
|
let _ = peer.join();
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
r.send(Ev::Cast(Cast::Note("still-running"))).unwrap();
|
||||||
|
assert_eq!(r.call(|r| Ev::Call(Call::Exits(r))).unwrap(), 1);
|
||||||
|
r.shutdown();
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
log.get(),
|
||||||
|
vec!["exit", "still-running", "draining", "drained", "terminate"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ref_shutdown_is_graceful_and_waits() {
|
||||||
|
let log = Log::default();
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let r = settled(&l, true);
|
||||||
|
r.shutdown();
|
||||||
|
// terminate has run by the time shutdown() returns.
|
||||||
|
assert_eq!(l.get(), vec!["draining", "drained", "terminate"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
+40
-9
@@ -56,7 +56,11 @@ fn snapshot_lists_actors_with_parent_edge() {
|
|||||||
|
|
||||||
// The root itself is on-CPU (it's running this code) and rooted under
|
// The root itself is on-CPU (it's running this code) and rooted under
|
||||||
// the forest sentinel.
|
// the forest sentinel.
|
||||||
let root = snap.actors.iter().find(|a| a.pid == me).expect("root present");
|
let root = snap
|
||||||
|
.actors
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.pid == me)
|
||||||
|
.expect("root present");
|
||||||
assert_eq!(root.state, ActorState::Running);
|
assert_eq!(root.state, ActorState::Running);
|
||||||
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
||||||
|
|
||||||
@@ -200,7 +204,11 @@ fn tree_places_child_under_its_spawner() {
|
|||||||
|
|
||||||
// The root is parented at the forest sentinel, so it's a genuine root,
|
// The root is parented at the forest sentinel, so it's a genuine root,
|
||||||
// and the worker it spawned hangs beneath it.
|
// and the worker it spawned hangs beneath it.
|
||||||
let root = t.roots.iter().find(|n| n.info.pid == me).expect("root in forest");
|
let root = t
|
||||||
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == me)
|
||||||
|
.expect("root in forest");
|
||||||
assert!(!root.orphaned);
|
assert!(!root.orphaned);
|
||||||
assert!(
|
assert!(
|
||||||
root.children.iter().any(|c| c.info.pid == h.pid()),
|
root.children.iter().any(|c| c.info.pid == h.pid()),
|
||||||
@@ -258,14 +266,25 @@ fn tree_from_nests_children_and_reroots_orphans() {
|
|||||||
let t = tree_from(snap);
|
let t = tree_from(snap);
|
||||||
assert_eq!(t.roots.len(), 2);
|
assert_eq!(t.roots.len(), 2);
|
||||||
|
|
||||||
let root = t.roots.iter().find(|n| n.info.pid == root_pid).expect("root present");
|
let root = t
|
||||||
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == root_pid)
|
||||||
|
.expect("root present");
|
||||||
assert!(!root.orphaned);
|
assert!(!root.orphaned);
|
||||||
assert_eq!(root.children.len(), 1);
|
assert_eq!(root.children.len(), 1);
|
||||||
assert_eq!(root.children[0].info.pid, child);
|
assert_eq!(root.children[0].info.pid, child);
|
||||||
assert!(!root.children[0].orphaned);
|
assert!(!root.children[0].orphaned);
|
||||||
|
|
||||||
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted");
|
let o = t
|
||||||
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned");
|
.roots
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.info.pid == orphan)
|
||||||
|
.expect("orphan re-rooted");
|
||||||
|
assert!(
|
||||||
|
o.orphaned,
|
||||||
|
"an actor whose parent is absent must be flagged orphaned"
|
||||||
|
);
|
||||||
assert!(o.children.is_empty());
|
assert!(o.children.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +417,11 @@ fn stack_info_reports_defaults_and_sampled_depth() {
|
|||||||
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
||||||
let s = info.stack;
|
let s = info.stack;
|
||||||
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
assert_eq!(s.reserve, 64 * 1024, "default reserve");
|
||||||
assert_eq!(s.guard, 1024 * 1024, "default guard (kernel stack_guard_gap convention)");
|
assert_eq!(
|
||||||
|
s.guard,
|
||||||
|
1024 * 1024,
|
||||||
|
"default guard (kernel stack_guard_gap convention)"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
s.depth_high_water >= 8 * 4096,
|
s.depth_high_water >= 8 * 4096,
|
||||||
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
"hwm sampled at the deep yield: expected ≥ 32 KiB, got {}",
|
||||||
@@ -432,7 +455,10 @@ fn stack_info_shrink_counters_are_live() {
|
|||||||
let spike = 768 * 4096;
|
let spike = 768 * 4096;
|
||||||
assert!(spike > SHRINK_THRESHOLD);
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
let worker = spawn_with(
|
let worker = spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
std::hint::black_box(burn_stack_yielding(768));
|
std::hint::black_box(burn_stack_yielding(768));
|
||||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
@@ -457,9 +483,14 @@ fn stack_info_shrink_counters_are_live() {
|
|||||||
spin_until(wpid, |a| a.state == ActorState::Parked);
|
spin_until(wpid, |a| a.state == ActorState::Parked);
|
||||||
park_tx.send(()).unwrap();
|
park_tx.send(()).unwrap();
|
||||||
}
|
}
|
||||||
let info = spin_until(wpid, |a| a.state == ActorState::Parked && a.stack.shrinks >= 1);
|
let info = spin_until(wpid, |a| {
|
||||||
|
a.state == ActorState::Parked && a.stack.shrinks >= 1
|
||||||
|
});
|
||||||
let s = info.stack;
|
let s = info.stack;
|
||||||
assert!(s.shrinks >= 1, "cooldown was crossed with a spike above threshold");
|
assert!(
|
||||||
|
s.shrinks >= 1,
|
||||||
|
"cooldown was crossed with a spike above threshold"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
s.parks_since_shrink < SHRINK_COOLDOWN,
|
s.parks_since_shrink < SHRINK_COOLDOWN,
|
||||||
"counter must reset at shrink: {}",
|
"counter must reset at shrink: {}",
|
||||||
|
|||||||
+13
-3
@@ -56,8 +56,16 @@ fn other_actors_run_while_block_on_io_is_in_flight() {
|
|||||||
let pos_2 = v.iter().position(|&x| x == 2).unwrap();
|
let pos_2 = v.iter().position(|&x| x == 2).unwrap();
|
||||||
let pos_3 = v.iter().position(|&x| x == 3).unwrap();
|
let pos_3 = v.iter().position(|&x| x == 3).unwrap();
|
||||||
let pos_4 = v.iter().position(|&x| x == 4).unwrap();
|
let pos_4 = v.iter().position(|&x| x == 4).unwrap();
|
||||||
assert!(pos_2 < pos_4, "B's first step ran after A resumed: {:?}", *v);
|
assert!(
|
||||||
assert!(pos_3 < pos_4, "B's second step ran after A resumed: {:?}", *v);
|
pos_2 < pos_4,
|
||||||
|
"B's first step ran after A resumed: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pos_3 < pos_4,
|
||||||
|
"B's second step ran after A resumed: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -76,7 +84,9 @@ fn many_concurrent_block_on_io_calls_all_complete() {
|
|||||||
cc.fetch_add(n, Ordering::SeqCst);
|
cc.fetch_add(n, Ordering::SeqCst);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -144,8 +144,7 @@ fn write_sugar_sends_bytes_to_pipe() {
|
|||||||
// Pipe is empty + has buffer space, so this returns immediately
|
// Pipe is empty + has buffer space, so this returns immediately
|
||||||
// after wait_writable wakes (which happens fast because the
|
// after wait_writable wakes (which happens fast because the
|
||||||
// kernel marks an empty pipe as immediately writable).
|
// kernel marks an empty pipe as immediately writable).
|
||||||
let n = smarm::scheduler::write(p_writer.write, b"smarm")
|
let n = smarm::scheduler::write(p_writer.write, b"smarm").expect("write failed");
|
||||||
.expect("write failed");
|
|
||||||
assert_eq!(n, 5);
|
assert_eq!(n, 5);
|
||||||
c.fetch_add(1, Ordering::SeqCst);
|
c.fetch_add(1, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
@@ -209,10 +208,18 @@ fn other_actors_run_while_one_is_parked_on_wait_readable() {
|
|||||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||||
let big_b_count = v.iter().filter(|&&c| c == b'B').count();
|
let big_b_count = v.iter().filter(|&&c| c == b'B').count();
|
||||||
assert_eq!(big_b_count, 3, "B should have made 3 steps: {:?}", *v);
|
assert_eq!(big_b_count, 3, "B should have made 3 steps: {:?}", *v);
|
||||||
assert!(pos_big_a < pos_lit_a, "A pre-park before A post-park: {:?}", *v);
|
assert!(
|
||||||
|
pos_big_a < pos_lit_a,
|
||||||
|
"A pre-park before A post-park: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
// At least the last B step should be before A resumes.
|
// At least the last B step should be before A resumes.
|
||||||
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap();
|
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap();
|
||||||
assert!(last_big_b < pos_lit_a, "B should finish before A resumes: {:?}", *v);
|
assert!(
|
||||||
|
last_big_b < pos_lit_a,
|
||||||
|
"B should finish before A resumes: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+10
-4
@@ -57,7 +57,10 @@ fn linked_pair_one_panics_other_is_stopped() {
|
|||||||
panic!("boom");
|
panic!("boom");
|
||||||
});
|
});
|
||||||
|
|
||||||
let dn = down_b.rx.recv().expect("monitor channel closed before Down");
|
let dn = down_b
|
||||||
|
.rx
|
||||||
|
.recv()
|
||||||
|
.expect("monitor channel closed before Down");
|
||||||
assert_eq!(dn.pid, b, "Down reported the wrong pid");
|
assert_eq!(dn.pid, b, "Down reported the wrong pid");
|
||||||
if matches!(dn.reason, DownReason::Stopped) {
|
if matches!(dn.reason, DownReason::Stopped) {
|
||||||
s.store(true, Ordering::SeqCst);
|
s.store(true, Ordering::SeqCst);
|
||||||
@@ -117,7 +120,7 @@ fn normal_exit_does_not_propagate() {
|
|||||||
let a = ha.pid();
|
let a = ha.pid();
|
||||||
link(a);
|
link(a);
|
||||||
yield_now(); // let A run to completion and finalize
|
yield_now(); // let A run to completion and finalize
|
||||||
// A exited normally: nothing should have landed on the inbox.
|
// A exited normally: nothing should have landed on the inbox.
|
||||||
if let Ok(None) = inbox.try_recv() {
|
if let Ok(None) = inbox.try_recv() {
|
||||||
e.store(true, Ordering::SeqCst);
|
e.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -152,7 +155,10 @@ fn link_to_dead_pid_stops_a_nontrapping_caller() {
|
|||||||
});
|
});
|
||||||
let b = hb.pid();
|
let b = hb.pid();
|
||||||
let down_b = monitor(b);
|
let down_b = monitor(b);
|
||||||
let dn = down_b.rx.recv().expect("monitor channel closed before Down");
|
let dn = down_b
|
||||||
|
.rx
|
||||||
|
.recv()
|
||||||
|
.expect("monitor channel closed before Down");
|
||||||
if matches!(dn.reason, DownReason::Stopped) {
|
if matches!(dn.reason, DownReason::Stopped) {
|
||||||
s.store(true, Ordering::SeqCst);
|
s.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -208,7 +214,7 @@ fn unlink_prevents_propagation() {
|
|||||||
panic!("boom"); // abnormal, but the link is gone
|
panic!("boom"); // abnormal, but the link is gone
|
||||||
});
|
});
|
||||||
yield_now(); // let A link, unlink, and panic
|
yield_now(); // let A link, unlink, and panic
|
||||||
// Unlinked before death → no ExitSignal should have arrived.
|
// Unlinked before death → no ExitSignal should have arrived.
|
||||||
if let Ok(None) = inbox.try_recv() {
|
if let Ok(None) = inbox.try_recv() {
|
||||||
sv.store(true, Ordering::SeqCst);
|
sv.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-6
@@ -67,7 +67,10 @@ fn monitor_already_dead_target_is_noproc() {
|
|||||||
// and its generation bumped, so `pid` is now stale.
|
// and its generation bumped, so `pid` is now stale.
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
let down = monitor(pid);
|
let down = monitor(pid);
|
||||||
let d = down.rx.recv().expect("NoProc Down should be delivered immediately");
|
let d = down
|
||||||
|
.rx
|
||||||
|
.recv()
|
||||||
|
.expect("NoProc Down should be delivered immediately");
|
||||||
assert_eq!(d.pid, pid);
|
assert_eq!(d.pid, pid);
|
||||||
if matches!(d.reason, DownReason::NoProc) {
|
if matches!(d.reason, DownReason::NoProc) {
|
||||||
o.store(true, Ordering::SeqCst);
|
o.store(true, Ordering::SeqCst);
|
||||||
@@ -91,7 +94,11 @@ fn multiple_monitors_all_notified() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(count.load(Ordering::SeqCst), 3, "every monitor should see the Down");
|
assert_eq!(
|
||||||
|
count.load(Ordering::SeqCst),
|
||||||
|
3,
|
||||||
|
"every monitor should see the Down"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -103,8 +110,15 @@ fn demonitor_stops_delivery() {
|
|||||||
let h = spawn(|| {});
|
let h = spawn(|| {});
|
||||||
let pid = h.pid();
|
let pid = h.pid();
|
||||||
let m = monitor(pid);
|
let m = monitor(pid);
|
||||||
assert_eq!(demonitor(&m), Some(m.id), "live registration should be removed");
|
assert_eq!(
|
||||||
assert!(m.rx.recv().is_err(), "no Down should arrive after demonitor");
|
demonitor(&m),
|
||||||
|
Some(m.id),
|
||||||
|
"live registration should be removed"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
m.rx.recv().is_err(),
|
||||||
|
"no Down should arrive after demonitor"
|
||||||
|
);
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -122,7 +136,10 @@ fn demonitor_one_of_many() {
|
|||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit));
|
assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit));
|
||||||
assert!(matches!(ms[2].rx.recv().unwrap().reason, DownReason::Exit));
|
assert!(matches!(ms[2].rx.recv().unwrap().reason, DownReason::Exit));
|
||||||
assert!(ms[1].rx.recv().is_err(), "demonitored channel should be closed");
|
assert!(
|
||||||
|
ms[1].rx.recv().is_err(),
|
||||||
|
"demonitored channel should be closed"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +153,11 @@ fn demonitor_after_fire_is_none() {
|
|||||||
let m = monitor(pid);
|
let m = monitor(pid);
|
||||||
let d = m.rx.recv().expect("Down before close");
|
let d = m.rx.recv().expect("Down before close");
|
||||||
assert!(matches!(d.reason, DownReason::Exit));
|
assert!(matches!(d.reason, DownReason::Exit));
|
||||||
assert_eq!(demonitor(&m), None, "already-fired monitor has nothing to remove");
|
assert_eq!(
|
||||||
|
demonitor(&m),
|
||||||
|
None,
|
||||||
|
"already-fired monitor has nothing to remove"
|
||||||
|
);
|
||||||
let _ = h.join();
|
let _ = h.join();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -3,9 +3,9 @@
|
|||||||
//! needs to be able to park.
|
//! needs to be able to park.
|
||||||
|
|
||||||
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex as StdMutex;
|
use std::sync::Mutex as StdMutex;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -111,8 +111,16 @@ fn contended_lock_parks_until_holder_releases() {
|
|||||||
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
||||||
|
|
||||||
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
||||||
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v);
|
assert!(
|
||||||
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v);
|
pos_b_try < pos_a_dropped,
|
||||||
|
"B should attempt before A drops: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pos_a_dropped < pos_b_locked,
|
||||||
|
"B should lock only after A drops: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -209,7 +217,11 @@ fn waiters_are_granted_the_lock_in_fifo_order() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let v = order.lock().unwrap().clone();
|
let v = order.lock().unwrap().clone();
|
||||||
assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order");
|
assert_eq!(
|
||||||
|
v,
|
||||||
|
vec![1, 2, 3, 4],
|
||||||
|
"waiters should acquire in arrival order"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+5
-3
@@ -77,8 +77,7 @@ fn observer_reports_none_for_a_forged_pid() {
|
|||||||
// An index that is not in the slab at all — the verb relays the
|
// An index that is not in the slab at all — the verb relays the
|
||||||
// primitive's `None` faithfully.
|
// primitive's `None` faithfully.
|
||||||
let forged = smarm::Pid::new(u32::MAX - 1, 0);
|
let forged = smarm::Pid::new(u32::MAX - 1, 0);
|
||||||
let ObserverReply::ActorInfo(none) =
|
let ObserverReply::ActorInfo(none) = obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
|
||||||
obs.call(ObserverRequest::ActorInfo(forged)).unwrap()
|
|
||||||
else {
|
else {
|
||||||
panic!("ActorInfo verb must reply ActorInfo");
|
panic!("ActorInfo verb must reply ActorInfo");
|
||||||
};
|
};
|
||||||
@@ -113,7 +112,10 @@ fn observer_sees_a_parked_actor_as_parked() {
|
|||||||
}
|
}
|
||||||
smarm::yield_now();
|
smarm::yield_now();
|
||||||
}
|
}
|
||||||
assert!(parked, "observer should eventually report the worker as Parked");
|
assert!(
|
||||||
|
parked,
|
||||||
|
"observer should eventually report the worker as Parked"
|
||||||
|
);
|
||||||
|
|
||||||
gate_tx.send(()).unwrap();
|
gate_tx.send(()).unwrap();
|
||||||
worker.join().unwrap();
|
worker.join().unwrap();
|
||||||
|
|||||||
+13
-3
@@ -44,7 +44,10 @@ fn a_dead_actor_vanishes_from_every_group_it_joined() {
|
|||||||
// Drain-on-contact: touching g1 detects the death and sweeps the pid
|
// Drain-on-contact: touching g1 detects the death and sweeps the pid
|
||||||
// out of every group (g2 included), not just g1.
|
// out of every group (g2 included), not just g1.
|
||||||
assert!(members("g1").is_empty(), "evicted from the touched group");
|
assert!(members("g1").is_empty(), "evicted from the touched group");
|
||||||
assert!(members("g2").is_empty(), "and swept from the untouched group");
|
assert!(
|
||||||
|
members("g2").is_empty(),
|
||||||
|
"and swept from the untouched group"
|
||||||
|
);
|
||||||
assert_eq!(pick("g1"), None);
|
assert_eq!(pick("g1"), None);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,7 +86,11 @@ fn live_members_survive_a_peers_death() {
|
|||||||
tx_a.send(()).unwrap();
|
tx_a.send(()).unwrap();
|
||||||
a.join().unwrap();
|
a.join().unwrap();
|
||||||
|
|
||||||
assert_eq!(members("svc"), vec![b.pid()], "only the dead peer is reaped");
|
assert_eq!(
|
||||||
|
members("svc"),
|
||||||
|
vec![b.pid()],
|
||||||
|
"only the dead peer is reaped"
|
||||||
|
);
|
||||||
assert_eq!(pick("svc"), Some(b.pid()));
|
assert_eq!(pick("svc"), Some(b.pid()));
|
||||||
|
|
||||||
tx_b.send(()).unwrap();
|
tx_b.send(()).unwrap();
|
||||||
@@ -125,7 +132,10 @@ fn joining_an_already_dead_pid_is_evicted_on_next_contact() {
|
|||||||
// monitor() on a gone pid queues a NoProc Down immediately, so the
|
// monitor() on a gone pid queues a NoProc Down immediately, so the
|
||||||
// membership is reaped the next time the group is touched.
|
// membership is reaped the next time the group is touched.
|
||||||
join("late", pid);
|
join("late", pid);
|
||||||
assert!(members("late").is_empty(), "dead-at-join member is reaped on read");
|
assert!(
|
||||||
|
members("late").is_empty(),
|
||||||
|
"dead-at-join member is reaped on read"
|
||||||
|
);
|
||||||
assert_eq!(pick("late"), None);
|
assert_eq!(pick("late"), None);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -41,7 +41,11 @@ fn stop_storm_does_not_poison_runtime() {
|
|||||||
}
|
}
|
||||||
c.fetch_add(1, Ordering::SeqCst);
|
c.fetch_add(1, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly");
|
assert_eq!(
|
||||||
|
completed.load(Ordering::SeqCst),
|
||||||
|
1,
|
||||||
|
"root completed cleanly"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sharper repro: a stop-flagged actor whose *next allocation* is the
|
/// The sharper repro: a stop-flagged actor whose *next allocation* is the
|
||||||
@@ -85,5 +89,9 @@ fn self_stop_during_spawn_does_not_poison_shared_mutex() {
|
|||||||
}
|
}
|
||||||
c.fetch_add(1, Ordering::SeqCst);
|
c.fetch_add(1, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
assert_eq!(completed.load(Ordering::SeqCst), 1, "root completed cleanly");
|
assert_eq!(
|
||||||
|
completed.load(Ordering::SeqCst),
|
||||||
|
1,
|
||||||
|
"root completed cleanly"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -43,10 +43,21 @@ fn check_yields_when_timeslice_expired() {
|
|||||||
let pos_big_b = v.iter().position(|&c| c == b'B').unwrap();
|
let pos_big_b = v.iter().position(|&c| c == b'B').unwrap();
|
||||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||||
let pos_lit_b = v.iter().position(|&c| c == b'b').unwrap();
|
let pos_lit_b = v.iter().position(|&c| c == b'b').unwrap();
|
||||||
assert!(pos_big_a < pos_lit_a, "A's tail ran before B's head: {:?}", *v);
|
assert!(
|
||||||
assert!(pos_big_b < pos_lit_b, "B's tail ran before A's head: {:?}", *v);
|
pos_big_a < pos_lit_a,
|
||||||
assert!(pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b),
|
"A's tail ran before B's head: {:?}",
|
||||||
"preemption didn't interleave: {:?}", *v);
|
*v
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pos_big_b < pos_lit_b,
|
||||||
|
"B's tail ran before A's head: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b),
|
||||||
|
"preemption didn't interleave: {:?}",
|
||||||
|
*v
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+13
-4
@@ -65,7 +65,10 @@ fn name_held_by_live_actor_is_taken() {
|
|||||||
ready_rx.recv().unwrap();
|
ready_rx.recv().unwrap();
|
||||||
// Root tries to claim a live actor's name for itself -> NameTaken.
|
// Root tries to claim a live actor's name for itself -> NameTaken.
|
||||||
let (tx_b, _rx_b) = channel::<u64>();
|
let (tx_b, _rx_b) = channel::<u64>();
|
||||||
assert_eq!(register(SVC, tx_b), Err(RegisterError::NameTaken { holder: a.pid() }));
|
assert_eq!(
|
||||||
|
register(SVC, tx_b),
|
||||||
|
Err(RegisterError::NameTaken { holder: a.pid() })
|
||||||
|
);
|
||||||
send(SVC, 0).unwrap(); // release a (delivers to the holder, a)
|
send(SVC, 0).unwrap(); // release a (delivers to the holder, a)
|
||||||
a.join().unwrap();
|
a.join().unwrap();
|
||||||
});
|
});
|
||||||
@@ -105,7 +108,10 @@ fn dead_holder_is_pruned_and_name_taken_over() {
|
|||||||
fn send_errors_unresolved_and_no_channel() {
|
fn send_errors_unresolved_and_no_channel() {
|
||||||
run(|| {
|
run(|| {
|
||||||
// No actor at all.
|
// No actor at all.
|
||||||
assert!(matches!(send(Name::<u64>::new("ghost"), 1u64), Err(SendError::Unresolved(_))));
|
assert!(matches!(
|
||||||
|
send(Name::<u64>::new("ghost"), 1u64),
|
||||||
|
Err(SendError::Unresolved(_))
|
||||||
|
));
|
||||||
|
|
||||||
let (ready_tx, ready_rx) = channel::<()>();
|
let (ready_tx, ready_rx) = channel::<()>();
|
||||||
let (tx, rx) = channel::<u64>();
|
let (tx, rx) = channel::<u64>();
|
||||||
@@ -227,8 +233,11 @@ fn send_dyn_delivers_and_reports_wrong_type() {
|
|||||||
ready_rx.recv().unwrap();
|
ready_rx.recv().unwrap();
|
||||||
let p = h.pid(); // a bare Pid<Erased>, as if recovered off a Down
|
let p = h.pid(); // a bare Pid<Erased>, as if recovered off a Down
|
||||||
send_dyn::<u64>(p, 3u64).unwrap(); // right type: delivered
|
send_dyn::<u64>(p, 3u64).unwrap(); // right type: delivered
|
||||||
// Live actor, but it has no channel for &str — the genuinely-fallible case.
|
// Live actor, but it has no channel for &str — the genuinely-fallible case.
|
||||||
assert!(matches!(send_dyn::<&'static str>(p, "nope"), Err(SendError::NoChannel(_))));
|
assert!(matches!(
|
||||||
|
send_dyn::<&'static str>(p, "nope"),
|
||||||
|
Err(SendError::NoChannel(_))
|
||||||
|
));
|
||||||
done_tx.send(()).unwrap();
|
done_tx.send(()).unwrap();
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
//! Root exit — the run's initial actor returning means "the program is done".
|
||||||
|
//!
|
||||||
|
//! When the root finalizes, the runtime delivers `request_shutdown` to every
|
||||||
|
//! **forest root**: each live actor whose parent is the run itself (a plain
|
||||||
|
//! `spawn` from the root closure) or is already dead. Nothing below a live
|
||||||
|
//! parent is touched directly — a supervisor gets one request and runs its
|
||||||
|
//! own ordered shutdown per child `Shutdown` policy.
|
||||||
|
//!
|
||||||
|
//! - Non-trapping actors are stopped outright, exactly as by
|
||||||
|
//! `request_shutdown` — a `spawn(|| { sleep(..); work() })` the root did
|
||||||
|
//! not `join` does NOT get to finish. Join it, supervise it, or trap.
|
||||||
|
//! - Trapping actors get `handle_shutdown` / an `ExitSignal{Shutdown}` and
|
||||||
|
//! may keep running (`Continue`, drain, then stop themselves) — timers and
|
||||||
|
//! all; the run ends when they do. There is no second, forcing sweep.
|
||||||
|
//! - A periodic-timer daemon (the classic wedge) never blocks `run()`.
|
||||||
|
|
||||||
|
use smarm::gen_server::{start, GenServer, GenServerCtx, ShutdownAction, StopHandle, TimerHandle};
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
|
||||||
|
use smarm::{run, sleep, spawn, trap_exit, DownReason};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
fn assert_prompt(start: Instant, what: &str) {
|
||||||
|
assert!(
|
||||||
|
start.elapsed() < Duration::from_secs(2),
|
||||||
|
"{what}: run() took {:?}",
|
||||||
|
start.elapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bare (non-gen_server) actors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A non-trapping sleeper the root did not join is stopped, not waited for.
|
||||||
|
#[test]
|
||||||
|
fn unjoined_non_trapping_sleeper_is_stopped() {
|
||||||
|
let finished = Arc::new(AtomicBool::new(false));
|
||||||
|
let f = finished.clone();
|
||||||
|
let t = Instant::now();
|
||||||
|
run(move || {
|
||||||
|
spawn(move || {
|
||||||
|
sleep(Duration::from_secs(5));
|
||||||
|
f.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert_prompt(t, "sleeper");
|
||||||
|
assert!(
|
||||||
|
!finished.load(Ordering::SeqCst),
|
||||||
|
"sleeper should have been stopped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trapping bare actor sees `Shutdown` from the root's exit and may keep
|
||||||
|
/// working — here it sleeps (a timer!) after the signal, then returns. The
|
||||||
|
/// run waits for it: no forcing sweep.
|
||||||
|
#[test]
|
||||||
|
fn trapping_actor_may_finish_after_shutdown_signal() {
|
||||||
|
let finished = Arc::new(AtomicBool::new(false));
|
||||||
|
let f = finished.clone();
|
||||||
|
run(move || {
|
||||||
|
spawn(move || {
|
||||||
|
let inbox = trap_exit();
|
||||||
|
let sig = inbox.recv().expect("shutdown signal");
|
||||||
|
assert_eq!(sig.reason, DownReason::Shutdown);
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
f.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
// Trapping is a runtime opt-in: give the actor a chance to run
|
||||||
|
// `trap_exit()`; a not-yet-run actor is non-trapping and is stopped.
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
finished.load(Ordering::SeqCst),
|
||||||
|
"trapping actor must be allowed to finish"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The classic wedge: a lazily spawned daemon that never returns on its own.
|
||||||
|
#[test]
|
||||||
|
fn parked_forever_daemon_does_not_block_run() {
|
||||||
|
let t = Instant::now();
|
||||||
|
run(|| {
|
||||||
|
let (_tx, rx) = smarm::channel::<()>();
|
||||||
|
spawn(move || {
|
||||||
|
let _ = rx.recv(); // parked forever: sender is held by the root, which returns
|
||||||
|
});
|
||||||
|
// Leak the sender into the daemon's own scope so nothing else drops it.
|
||||||
|
std::mem::forget(_tx);
|
||||||
|
});
|
||||||
|
assert_prompt(t, "daemon");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// gen_servers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A non-trapping ticker with a periodic timer: the timer wheel is never empty,
|
||||||
|
/// and root exit must still end the run.
|
||||||
|
struct Ticker {
|
||||||
|
ticks: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
impl GenServer for Ticker {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
|
ctx.timer().tick_every(Duration::from_millis(5), ());
|
||||||
|
}
|
||||||
|
fn handle_call(&mut self, _: ()) {}
|
||||||
|
fn handle_cast(&mut self, _: ()) {}
|
||||||
|
fn handle_timer(&mut self, _: ()) {
|
||||||
|
self.ticks.fetch_add(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn periodic_timer_daemon_does_not_block_run() {
|
||||||
|
let ticks = Arc::new(AtomicUsize::new(0));
|
||||||
|
let tk = ticks.clone();
|
||||||
|
let t = Instant::now();
|
||||||
|
run(move || {
|
||||||
|
let _r = start(Ticker { ticks: tk });
|
||||||
|
sleep(Duration::from_millis(50));
|
||||||
|
});
|
||||||
|
assert_prompt(t, "ticker");
|
||||||
|
assert!(
|
||||||
|
ticks.load(Ordering::SeqCst) >= 3,
|
||||||
|
"ticker should have ticked"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trapping server that answers `Continue`, keeps ticking on its own timer
|
||||||
|
/// (draining), and stops itself later. Root exit must not cut it short.
|
||||||
|
struct Drainer {
|
||||||
|
log: Arc<Mutex<Vec<&'static str>>>,
|
||||||
|
shutdowns: Arc<AtomicUsize>,
|
||||||
|
ticks_after_shutdown: usize,
|
||||||
|
stop: Option<StopHandle<Drainer>>,
|
||||||
|
timer: Option<TimerHandle<Drainer>>,
|
||||||
|
draining: bool,
|
||||||
|
}
|
||||||
|
impl GenServer for Drainer {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
fn init(&mut self, ctx: &GenServerCtx<Self>) {
|
||||||
|
ctx.trap_exit();
|
||||||
|
self.stop = Some(ctx.stop_handle());
|
||||||
|
self.timer = Some(ctx.timer());
|
||||||
|
}
|
||||||
|
fn handle_call(&mut self, _: ()) {}
|
||||||
|
fn handle_cast(&mut self, _: ()) {}
|
||||||
|
fn handle_shutdown(&mut self) -> ShutdownAction {
|
||||||
|
self.shutdowns.fetch_add(1, Ordering::SeqCst);
|
||||||
|
self.log.lock().unwrap().push("handle_shutdown");
|
||||||
|
self.draining = true;
|
||||||
|
self.timer
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.tick_every(Duration::from_millis(10), ());
|
||||||
|
ShutdownAction::Continue
|
||||||
|
}
|
||||||
|
fn handle_timer(&mut self, _: ()) {
|
||||||
|
if !self.draining {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.ticks_after_shutdown += 1;
|
||||||
|
if self.ticks_after_shutdown == 3 {
|
||||||
|
self.log.lock().unwrap().push("drained");
|
||||||
|
self.stop.as_ref().unwrap().stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn terminate(&mut self) {
|
||||||
|
self.log.lock().unwrap().push("terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drainer(log: &Arc<Mutex<Vec<&'static str>>>, shutdowns: &Arc<AtomicUsize>) -> Drainer {
|
||||||
|
Drainer {
|
||||||
|
log: log.clone(),
|
||||||
|
shutdowns: shutdowns.clone(),
|
||||||
|
ticks_after_shutdown: 0,
|
||||||
|
stop: None,
|
||||||
|
timer: None,
|
||||||
|
draining: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trapping_server_drains_with_timers_after_root_exit() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let shutdowns = Arc::new(AtomicUsize::new(0));
|
||||||
|
let (l, s) = (log.clone(), shutdowns.clone());
|
||||||
|
run(move || {
|
||||||
|
let r = start(drainer(&l, &s));
|
||||||
|
// A gen_server's lifetime is governed by its refs: dropping the last
|
||||||
|
// one closes the inbox and ends the loop cleanly, which would cut the
|
||||||
|
// drain short for a reason unrelated to root exit. Pin it the way a
|
||||||
|
// registered name would.
|
||||||
|
std::mem::forget(r);
|
||||||
|
sleep(Duration::from_millis(20)); // let init (trap_exit) run
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
*log.lock().unwrap(),
|
||||||
|
vec!["handle_shutdown", "drained", "terminate"]
|
||||||
|
);
|
||||||
|
assert_eq!(shutdowns.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Supervision trees: only forest roots are addressed
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The supervisor gets ONE request and runs its ordered shutdown; a trapping
|
||||||
|
/// child under it sees exactly one `Shutdown` — from the supervisor, not a
|
||||||
|
/// second one from the runtime — and is allowed to finish its drain (a sleep,
|
||||||
|
/// i.e. a timer) under `Shutdown::Infinity`.
|
||||||
|
#[test]
|
||||||
|
fn supervised_children_are_shut_down_only_via_their_supervisor() {
|
||||||
|
let signals = Arc::new(AtomicUsize::new(0));
|
||||||
|
let drained = Arc::new(AtomicBool::new(false));
|
||||||
|
let sup_returned = Arc::new(AtomicBool::new(false));
|
||||||
|
let (sg, dr, sr) = (signals.clone(), drained.clone(), sup_returned.clone());
|
||||||
|
run(move || {
|
||||||
|
spawn(move || {
|
||||||
|
OneForOne::new()
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
let inbox = trap_exit();
|
||||||
|
while let Ok(sig) = inbox.recv() {
|
||||||
|
if sig.reason == DownReason::Shutdown {
|
||||||
|
sg.fetch_add(1, Ordering::SeqCst);
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
// A late second signal would land here.
|
||||||
|
while let Ok(Some(sig)) = inbox.try_recv() {
|
||||||
|
if sig.reason == DownReason::Shutdown {
|
||||||
|
sg.fetch_add(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dr.store(true, Ordering::SeqCst);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Infinity),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
sr.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
sleep(Duration::from_millis(30)); // let the tree settle
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
sup_returned.load(Ordering::SeqCst),
|
||||||
|
"supervisor should return normally"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
drained.load(Ordering::SeqCst),
|
||||||
|
"child should finish its drain"
|
||||||
|
);
|
||||||
|
assert_eq!(signals.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A supervised non-trapping child under `Shutdown::Timeout` is stopped by
|
||||||
|
/// the supervisor's policy, and the run ends promptly.
|
||||||
|
#[test]
|
||||||
|
fn supervisor_tree_is_torn_down_promptly_on_root_exit() {
|
||||||
|
let t = Instant::now();
|
||||||
|
run(|| {
|
||||||
|
spawn(|| {
|
||||||
|
OneForOne::new()
|
||||||
|
.child(
|
||||||
|
ChildSpec::new(Restart::Permanent, || loop {
|
||||||
|
sleep(Duration::from_millis(5));
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Timeout(Duration::from_millis(50))),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
});
|
||||||
|
assert_prompt(t, "tree");
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//! Under `smarm-trace`, every actor the root-exit sweep reaches is recorded
|
||||||
|
//! as a `root_sweep` event — the way to *see* unsupervised leftovers. One test
|
||||||
|
//! per binary: the trace file is process-global.
|
||||||
|
#![cfg(feature = "smarm-trace")]
|
||||||
|
|
||||||
|
use smarm::gen_server::{self, GenServer, GenServerCtx};
|
||||||
|
use smarm::run;
|
||||||
|
|
||||||
|
struct Quiet;
|
||||||
|
impl GenServer for Quiet {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
fn init(&mut self, _: &GenServerCtx<Self>) {}
|
||||||
|
fn handle_call(&mut self, _: ()) {}
|
||||||
|
fn handle_cast(&mut self, _: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forgotten_server_shows_up_as_root_sweep() {
|
||||||
|
let path = std::env::temp_dir().join(format!("smarm_root_sweep_{}.json", std::process::id()));
|
||||||
|
std::env::set_var("SMARM_TRACE_FILE", &path);
|
||||||
|
run(|| {
|
||||||
|
let srv = gen_server::start(Quiet);
|
||||||
|
srv.call(()).unwrap();
|
||||||
|
drop(srv); // forgotten: nobody supervises it, nobody holds it
|
||||||
|
});
|
||||||
|
let trace = std::fs::read_to_string(&path).expect("trace file written");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
assert!(
|
||||||
|
trace.contains("root_sweep stopped"),
|
||||||
|
"expected a root_sweep line for the non-trapping leftover; got:\n{trace}"
|
||||||
|
);
|
||||||
|
}
|
||||||
+77
-26
@@ -14,10 +14,17 @@
|
|||||||
//! - No slot leaks under high spawn/join churn
|
//! - No slot leaks under high spawn/join churn
|
||||||
//! - Panic on one scheduler thread doesn't kill others
|
//! - Panic on one scheduler thread doesn't kill others
|
||||||
|
|
||||||
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle};
|
use smarm::{
|
||||||
use std::sync::{atomic::{AtomicBool, AtomicU64, Ordering}, Arc};
|
channel,
|
||||||
use std::time::Duration;
|
runtime::{Config, Runtime},
|
||||||
|
spawn, yield_now, JoinHandle,
|
||||||
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -29,7 +36,9 @@ fn rt(n: usize) -> Runtime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenient single-threaded runtime (regression guard).
|
/// Convenient single-threaded runtime (regression guard).
|
||||||
fn rt1() -> Runtime { rt(1) }
|
fn rt1() -> Runtime {
|
||||||
|
rt(1)
|
||||||
|
}
|
||||||
|
|
||||||
/// Multi-threaded runtime using all available parallelism.
|
/// Multi-threaded runtime using all available parallelism.
|
||||||
fn rt_par() -> Runtime {
|
fn rt_par() -> Runtime {
|
||||||
@@ -79,7 +88,9 @@ fn config_min_1_max_1_is_single_threaded() {
|
|||||||
fn runtime_run_executes_closure() {
|
fn runtime_run_executes_closure() {
|
||||||
let flag = Arc::new(AtomicBool::new(false));
|
let flag = Arc::new(AtomicBool::new(false));
|
||||||
let f = flag.clone();
|
let f = flag.clone();
|
||||||
rt(1).run(move || { f.store(true, Ordering::SeqCst); });
|
rt(1).run(move || {
|
||||||
|
f.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
assert!(flag.load(Ordering::SeqCst));
|
assert!(flag.load(Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +122,12 @@ fn runtime_can_be_used_multiple_times_sequentially() {
|
|||||||
let b = Arc::new(AtomicU64::new(0));
|
let b = Arc::new(AtomicU64::new(0));
|
||||||
let ac = a.clone();
|
let ac = a.clone();
|
||||||
let bc = b.clone();
|
let bc = b.clone();
|
||||||
r.run(move || { ac.fetch_add(1, Ordering::SeqCst); });
|
r.run(move || {
|
||||||
r.run(move || { bc.fetch_add(1, Ordering::SeqCst); });
|
ac.fetch_add(1, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
r.run(move || {
|
||||||
|
bc.fetch_add(1, Ordering::SeqCst);
|
||||||
|
});
|
||||||
assert_eq!(a.load(Ordering::SeqCst), 1);
|
assert_eq!(a.load(Ordering::SeqCst), 1);
|
||||||
assert_eq!(b.load(Ordering::SeqCst), 1);
|
assert_eq!(b.load(Ordering::SeqCst), 1);
|
||||||
}
|
}
|
||||||
@@ -126,7 +141,9 @@ fn exact_1_spawn_join_works() {
|
|||||||
let v = Arc::new(AtomicU64::new(0));
|
let v = Arc::new(AtomicU64::new(0));
|
||||||
let vc = v.clone();
|
let vc = v.clone();
|
||||||
rt1().run(move || {
|
rt1().run(move || {
|
||||||
let h = spawn(move || { vc.store(42, Ordering::SeqCst); });
|
let h = spawn(move || {
|
||||||
|
vc.store(42, Ordering::SeqCst);
|
||||||
|
});
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(v.load(Ordering::SeqCst), 42);
|
assert_eq!(v.load(Ordering::SeqCst), 42);
|
||||||
@@ -155,7 +172,9 @@ fn exact_1_panic_captured() {
|
|||||||
let s = saw_err.clone();
|
let s = saw_err.clone();
|
||||||
rt1().run(move || {
|
rt1().run(move || {
|
||||||
let h = spawn(|| panic!("oops"));
|
let h = spawn(|| panic!("oops"));
|
||||||
if h.join().is_err() { s.store(true, Ordering::SeqCst); }
|
if h.join().is_err() {
|
||||||
|
s.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert!(saw_err.load(Ordering::SeqCst));
|
assert!(saw_err.load(Ordering::SeqCst));
|
||||||
}
|
}
|
||||||
@@ -176,7 +195,9 @@ fn multi_thread_all_actors_complete() {
|
|||||||
cc.fetch_add(1, Ordering::SeqCst);
|
cc.fetch_add(1, Ordering::SeqCst);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(counter.load(Ordering::SeqCst), 100);
|
assert_eq!(counter.load(Ordering::SeqCst), 100);
|
||||||
}
|
}
|
||||||
@@ -221,7 +242,9 @@ fn multi_thread_many_channels_no_lost_wakeups() {
|
|||||||
tx.send(1).unwrap();
|
tx.send(1).unwrap();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(count.load(Ordering::SeqCst), PAIRS as u64);
|
assert_eq!(count.load(Ordering::SeqCst), PAIRS as u64);
|
||||||
}
|
}
|
||||||
@@ -247,7 +270,9 @@ fn multi_thread_mutex_contention_no_deadlock() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
let g = m.lock_timeout(Duration::from_secs(1)).unwrap();
|
let g = m.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||||
t.store(*g, Ordering::SeqCst);
|
t.store(*g, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
@@ -262,7 +287,9 @@ fn multi_thread_join_across_threads() {
|
|||||||
rt_par().run(move || {
|
rt_par().run(move || {
|
||||||
let h = spawn(move || {
|
let h = spawn(move || {
|
||||||
// Do some work to make scheduling interesting.
|
// Do some work to make scheduling interesting.
|
||||||
for _ in 0..10 { yield_now(); }
|
for _ in 0..10 {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
vc.store(1, Ordering::SeqCst);
|
vc.store(1, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
h.join().unwrap();
|
h.join().unwrap();
|
||||||
@@ -279,8 +306,7 @@ fn multi_thread_join_across_threads() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn actors_run_on_multiple_os_threads() {
|
fn actors_run_on_multiple_os_threads() {
|
||||||
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> =
|
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> = Arc::new(smarm::Mutex::new(HashSet::new()));
|
||||||
Arc::new(smarm::Mutex::new(HashSet::new()));
|
|
||||||
|
|
||||||
rt_par().run({
|
rt_par().run({
|
||||||
let ids = thread_ids.clone();
|
let ids = thread_ids.clone();
|
||||||
@@ -294,11 +320,15 @@ fn actors_run_on_multiple_os_threads() {
|
|||||||
g.insert(tid);
|
g.insert(tid);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
for h in handles { h.join().unwrap(); }
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let n = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
let n = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
let ids = thread_ids.lock_timeout(Duration::from_secs(1)).unwrap();
|
let ids = thread_ids.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||||
// If we have >1 scheduler threads, we expect >1 OS thread IDs.
|
// If we have >1 scheduler threads, we expect >1 OS thread IDs.
|
||||||
@@ -326,11 +356,17 @@ fn scheduler_stats_run_queue_len_is_observable() {
|
|||||||
// run() completes (queue len == 0 at quiescence).
|
// run() completes (queue len == 0 at quiescence).
|
||||||
let r = rt_par();
|
let r = rt_par();
|
||||||
r.run(|| {
|
r.run(|| {
|
||||||
for _ in 0..10 { spawn(|| {}); }
|
for _ in 0..10 {
|
||||||
|
spawn(|| {});
|
||||||
|
}
|
||||||
// Don't join — let them drain naturally.
|
// Don't join — let them drain naturally.
|
||||||
});
|
});
|
||||||
let stats = r.stats();
|
let stats = r.stats();
|
||||||
assert_eq!(stats.total_run_queue_len(), 0, "queue should be empty after run()");
|
assert_eq!(
|
||||||
|
stats.total_run_queue_len(),
|
||||||
|
0,
|
||||||
|
"queue should be empty after run()"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -359,7 +395,9 @@ fn panic_in_actor_does_not_kill_runtime() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
let _ = bad.join(); // expect Err
|
let _ = bad.join(); // expect Err
|
||||||
for h in good_handles { h.join().unwrap(); }
|
for h in good_handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(completed.load(Ordering::SeqCst), 10);
|
assert_eq!(completed.load(Ordering::SeqCst), 10);
|
||||||
}
|
}
|
||||||
@@ -379,9 +417,11 @@ fn no_slot_leak_under_churn() {
|
|||||||
rt_par().run(move || {
|
rt_par().run(move || {
|
||||||
for _ in 0..500 {
|
for _ in 0..500 {
|
||||||
let cc = c.clone();
|
let cc = c.clone();
|
||||||
spawn(move || { cc.fetch_add(1, Ordering::SeqCst); })
|
spawn(move || {
|
||||||
.join()
|
cc.fetch_add(1, Ordering::SeqCst);
|
||||||
.unwrap();
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
||||||
@@ -474,7 +514,11 @@ fn multi_thread_timer_only_no_pipe_contention() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(count.load(Ordering::SeqCst), ACTORS as u64, "not all actors completed");
|
assert_eq!(
|
||||||
|
count.load(Ordering::SeqCst),
|
||||||
|
ACTORS as u64,
|
||||||
|
"not all actors completed"
|
||||||
|
);
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -515,7 +559,10 @@ fn runtime_reusable_after_root_panic() {
|
|||||||
let ran = Arc::new(AtomicBool::new(false));
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
let ran_t = ran.clone();
|
let ran_t = ran.clone();
|
||||||
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
r.run(move || ran_t.store(true, Ordering::Relaxed));
|
||||||
assert!(ran.load(Ordering::Relaxed), "runtime unusable after root panic");
|
assert!(
|
||||||
|
ran.load(Ordering::Relaxed),
|
||||||
|
"runtime unusable after root panic"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -528,7 +575,11 @@ fn runtime_reusable_after_root_panic() {
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -14,7 +14,9 @@ use std::sync::Arc;
|
|||||||
fn root_actor_runs() {
|
fn root_actor_runs() {
|
||||||
let captured = Arc::new(AtomicI64::new(0));
|
let captured = Arc::new(AtomicI64::new(0));
|
||||||
let c = captured.clone();
|
let c = captured.clone();
|
||||||
run(move || { c.store(99, Ordering::SeqCst); });
|
run(move || {
|
||||||
|
c.store(99, Ordering::SeqCst);
|
||||||
|
});
|
||||||
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +29,9 @@ fn spawn_and_join_returns_exit() {
|
|||||||
let captured = Arc::new(AtomicI64::new(0));
|
let captured = Arc::new(AtomicI64::new(0));
|
||||||
let c = captured.clone();
|
let c = captured.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let h = spawn(move || { c.store(7, Ordering::SeqCst); });
|
let h = spawn(move || {
|
||||||
|
c.store(7, Ordering::SeqCst);
|
||||||
|
});
|
||||||
let res = h.join();
|
let res = h.join();
|
||||||
assert!(res.is_ok(), "join returned {:?}", res);
|
assert!(res.is_ok(), "join returned {:?}", res);
|
||||||
});
|
});
|
||||||
@@ -68,8 +72,7 @@ fn yield_now_interleaves_actors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn self_pid_is_stable_within_an_actor() {
|
fn self_pid_is_stable_within_an_actor() {
|
||||||
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> =
|
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> = Arc::new(std::sync::Mutex::new(None));
|
||||||
Arc::new(std::sync::Mutex::new(None));
|
|
||||||
let p2 = pid_cell.clone();
|
let p2 = pid_cell.clone();
|
||||||
run(move || {
|
run(move || {
|
||||||
let h = spawn(move || {
|
let h = spawn(move || {
|
||||||
|
|||||||
+14
-3
@@ -19,7 +19,12 @@ fn ready_arm_returns_immediately_without_parking() {
|
|||||||
txa.send(42).unwrap();
|
txa.send(42).unwrap();
|
||||||
let i = select(&[&rxb, &rxa]);
|
let i = select(&[&rxb, &rxa]);
|
||||||
assert_eq!(i, 1);
|
assert_eq!(i, 1);
|
||||||
out2.store(rxa.try_recv().unwrap().expect("ready arm must hold a message"), Ordering::SeqCst);
|
out2.store(
|
||||||
|
rxa.try_recv()
|
||||||
|
.unwrap()
|
||||||
|
.expect("ready arm must hold a message"),
|
||||||
|
Ordering::SeqCst,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
assert_eq!(out.load(Ordering::SeqCst), 42);
|
assert_eq!(out.load(Ordering::SeqCst), 42);
|
||||||
}
|
}
|
||||||
@@ -276,7 +281,10 @@ fn select_timeout_ready_arm_wins_without_arming_a_timer() {
|
|||||||
let (txa, rxa) = channel::<i64>();
|
let (txa, rxa) = channel::<i64>();
|
||||||
let (_keep_b, rxb) = channel::<i64>();
|
let (_keep_b, rxb) = channel::<i64>();
|
||||||
txa.send(5).unwrap();
|
txa.send(5).unwrap();
|
||||||
assert_eq!(select_timeout(&[&rxb, &rxa], Duration::from_millis(500)), Some(1));
|
assert_eq!(
|
||||||
|
select_timeout(&[&rxb, &rxa], Duration::from_millis(500)),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
assert_eq!(rxa.try_recv().unwrap(), Some(5));
|
assert_eq!(rxa.try_recv().unwrap(), Some(5));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,7 +350,10 @@ fn select_timeout_closed_arm_is_ready_not_a_timeout() {
|
|||||||
let (_keep_a, rxa) = channel::<i64>();
|
let (_keep_a, rxa) = channel::<i64>();
|
||||||
let (txb, rxb) = channel::<i64>();
|
let (txb, rxb) = channel::<i64>();
|
||||||
drop(txb);
|
drop(txb);
|
||||||
assert_eq!(select_timeout(&[&rxa, &rxb], Duration::from_millis(200)), Some(1));
|
assert_eq!(
|
||||||
|
select_timeout(&[&rxa, &rxb], Duration::from_millis(200)),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
assert!(rxb.try_recv().is_err());
|
assert!(rxb.try_recv().is_err());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//! Graceful shutdown — `request_shutdown` (OTP `exit(Pid, shutdown)`).
|
||||||
|
//!
|
||||||
|
//! `request_stop` is `exit(Pid, kill)`: an uncatchable unwind at the target's
|
||||||
|
//! next observation point. `request_shutdown` is the polite form:
|
||||||
|
//! - a target that is NOT trapping exits is stopped exactly as by
|
||||||
|
//! `request_stop` (OTP's rule: don't trap, you die);
|
||||||
|
//! - a target that IS trapping receives an `ExitSignal { reason: Shutdown }`
|
||||||
|
//! on its trap inbox and keeps running — it is expected to wind down and
|
||||||
|
//! exit normally on its own.
|
||||||
|
|
||||||
|
use smarm::{monitor, request_shutdown, run, self_pid, sleep, spawn, trap_exit, DownReason, Pid};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{mpsc, Arc};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const WATCHDOG: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_shutdown_stops_a_non_trapping_actor() {
|
||||||
|
run(|| {
|
||||||
|
let h = spawn(|| sleep(Duration::from_secs(3600)));
|
||||||
|
let mon = monitor(h.pid());
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
let down = mon.rx.recv().expect("down");
|
||||||
|
assert_eq!(down.reason, DownReason::Stopped);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_shutdown_is_a_message_to_a_trapping_actor() {
|
||||||
|
let unwound = Arc::new(AtomicBool::new(false));
|
||||||
|
let u = unwound.clone();
|
||||||
|
run(move || {
|
||||||
|
struct Unwound(Arc<AtomicBool>);
|
||||||
|
impl Drop for Unwound {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if std::thread::panicking() {
|
||||||
|
self.0.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (tx, rx) = smarm::channel::<(Pid, DownReason)>();
|
||||||
|
let (ready_tx, ready_rx) = smarm::channel::<()>();
|
||||||
|
let h = spawn(move || {
|
||||||
|
let _g = Unwound(u);
|
||||||
|
let inbox = trap_exit();
|
||||||
|
let _ = ready_tx.send(());
|
||||||
|
let sig = inbox.recv().expect("exit signal");
|
||||||
|
let _ = tx.send((sig.from, sig.reason));
|
||||||
|
// Keep doing work after the request: shutdown is advisory.
|
||||||
|
sleep(Duration::from_millis(20));
|
||||||
|
});
|
||||||
|
// Trapping is set by the target itself; a request that beats it is a
|
||||||
|
// plain stop (same window as OTP's exit-before-process_flag).
|
||||||
|
ready_rx.recv().expect("ready");
|
||||||
|
let me = self_pid();
|
||||||
|
let mon = monitor(h.pid());
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
let (from, reason) = rx.recv().expect("relayed");
|
||||||
|
assert_eq!(from, me);
|
||||||
|
assert_eq!(reason, DownReason::Shutdown);
|
||||||
|
let down = mon.rx.recv().expect("down");
|
||||||
|
assert_eq!(
|
||||||
|
down.reason,
|
||||||
|
DownReason::Exit,
|
||||||
|
"target exited normally, not stopped"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
!unwound.load(Ordering::SeqCst),
|
||||||
|
"trapping target must not be unwound"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_shutdown_on_dead_pid_is_a_no_op() {
|
||||||
|
run(|| {
|
||||||
|
let h = spawn(|| {});
|
||||||
|
let pid = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
request_shutdown(pid); // must not panic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handle_request_shutdown_from_foreign_thread() {
|
||||||
|
let rt = smarm::init(smarm::Config::exact(2));
|
||||||
|
let handle = rt.handle();
|
||||||
|
|
||||||
|
let (pid_tx, pid_rx) = mpsc::channel::<Pid>();
|
||||||
|
let requester = thread::spawn(move || {
|
||||||
|
let pid = pid_rx.recv().expect("pid");
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
handle.request_shutdown(pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
rt.run(move || {
|
||||||
|
let (tx, rx) = smarm::channel::<DownReason>();
|
||||||
|
let (ready_tx, ready_rx) = smarm::channel::<()>();
|
||||||
|
let h = spawn(move || {
|
||||||
|
let inbox = trap_exit();
|
||||||
|
let _ = ready_tx.send(());
|
||||||
|
let sig = inbox.recv().expect("exit signal");
|
||||||
|
let _ = tx.send(sig.reason);
|
||||||
|
});
|
||||||
|
ready_rx.recv().expect("ready");
|
||||||
|
pid_tx.send(h.pid()).expect("send pid");
|
||||||
|
let reason = rx.recv().expect("relayed");
|
||||||
|
assert_eq!(reason, DownReason::Shutdown);
|
||||||
|
let _ = h.join();
|
||||||
|
});
|
||||||
|
let _ = done_tx.send(());
|
||||||
|
});
|
||||||
|
|
||||||
|
done_rx
|
||||||
|
.recv_timeout(WATCHDOG)
|
||||||
|
.expect("run did not return: foreign-thread request_shutdown never reached the target");
|
||||||
|
requester.join().expect("requester thread");
|
||||||
|
}
|
||||||
+34
-11
@@ -6,9 +6,7 @@
|
|||||||
//! big reserve behaviorally takes effect (deep recursion completes).
|
//! big reserve behaviorally takes effect (deep recursion completes).
|
||||||
|
|
||||||
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE};
|
||||||
use smarm::{
|
use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts};
|
||||||
self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts,
|
|
||||||
};
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -45,7 +43,10 @@ fn spawn_with_overrides_reserve_and_guard() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), ..SpawnOpts::default() };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
let h = spawn_with(opts, || {
|
let h = spawn_with(opts, || {
|
||||||
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let shape = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD));
|
||||||
@@ -57,7 +58,10 @@ fn spawn_with_partial_override_keeps_config_default_for_the_rest() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn spawn_with_rounds_to_pages() {
|
fn spawn_with_rounds_to_pages() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let opts = SpawnOpts { stack_reserve: Some(64 * 1024 + 1), guard_size: Some(4097) };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(64 * 1024 + 1),
|
||||||
|
guard_size: Some(4097),
|
||||||
|
};
|
||||||
let h = spawn_with(opts, || {
|
let h = spawn_with(opts, || {
|
||||||
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(reserve % 4096, 0);
|
assert_eq!(reserve % 4096, 0);
|
||||||
@@ -73,7 +77,10 @@ fn spawn_with_rounds_to_pages() {
|
|||||||
fn spawn_under_with_takes_opts() {
|
fn spawn_under_with_takes_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let me = self_pid();
|
let me = self_pid();
|
||||||
let opts = SpawnOpts { stack_reserve: Some(128 * 1024), ..SpawnOpts::default() };
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(128 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
let h = spawn_under_with(me, opts, || {
|
let h = spawn_under_with(me, opts, || {
|
||||||
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap();
|
||||||
assert_eq!(reserve, 128 * 1024);
|
assert_eq!(reserve, 128 * 1024);
|
||||||
@@ -90,7 +97,10 @@ fn spawn_under_with_takes_opts() {
|
|||||||
fn custom_stack_never_enters_the_pool() {
|
fn custom_stack_never_enters_the_pool() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
spawn_with(
|
spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(512 * 1024), guard_size: Some(128 * 1024) },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(512 * 1024),
|
||||||
|
guard_size: Some(128 * 1024),
|
||||||
|
},
|
||||||
|| {},
|
|| {},
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
@@ -122,7 +132,11 @@ fn default_stack_is_recycled() {
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
@@ -134,7 +148,10 @@ fn big_reserve_behaviorally_takes_effect() {
|
|||||||
let done = Arc::new(AtomicBool::new(false));
|
let done = Arc::new(AtomicBool::new(false));
|
||||||
let done2 = done.clone();
|
let done2 = done.clone();
|
||||||
spawn_with(
|
spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
std::hint::black_box(burn_stack(256));
|
std::hint::black_box(burn_stack(256));
|
||||||
done2.store(true, Ordering::SeqCst);
|
done2.store(true, Ordering::SeqCst);
|
||||||
@@ -167,7 +184,10 @@ impl smarm::GenServer for Echo {
|
|||||||
fn gen_server_builder_stack_opts() {
|
fn gen_server_builder_stack_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let server = GenServerBuilder::new(Echo)
|
let server = GenServerBuilder::new(Echo)
|
||||||
.stack_opts(SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() })
|
.stack_opts(SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
})
|
||||||
.start();
|
.start();
|
||||||
let (reserve, guard) = server.call(()).unwrap();
|
let (reserve, guard) = server.call(()).unwrap();
|
||||||
assert_eq!(reserve, 256 * 1024);
|
assert_eq!(reserve, 256 * 1024);
|
||||||
@@ -200,7 +220,10 @@ impl smarm::Machine for Probe {
|
|||||||
fn gen_statem_spawn_with_stack_opts() {
|
fn gen_statem_spawn_with_stack_opts() {
|
||||||
rt1().run(|| {
|
rt1().run(|| {
|
||||||
let m = smarm::gen_statem::spawn_with(
|
let m = smarm::gen_statem::spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
Probe,
|
Probe,
|
||||||
);
|
);
|
||||||
let (tx, rx) = smarm::channel::channel();
|
let (tx, rx) = smarm::channel::channel();
|
||||||
|
|||||||
+30
-6
@@ -78,14 +78,18 @@ fn run_as_child_if_requested() {
|
|||||||
Ok("wide_guard_top") => {
|
Ok("wide_guard_top") => {
|
||||||
// One byte below the usable region, 64 KiB guard: must fault.
|
// One byte below the usable region, 64 KiB guard: must fault.
|
||||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
unsafe { s.usable_base().sub(1).write_volatile(0xAB); }
|
unsafe {
|
||||||
|
s.usable_base().sub(1).write_volatile(0xAB);
|
||||||
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Ok("wide_guard_bottom") => {
|
Ok("wide_guard_bottom") => {
|
||||||
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
// The very bottom page of a 64 KiB guard: an unprobed C-style
|
||||||
// leap over a small guard lands here — must still fault.
|
// leap over a small guard lands here — must still fault.
|
||||||
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
let s = Stack::new(64 * 1024, 64 * 1024).unwrap();
|
||||||
unsafe { s.usable_base().sub(64 * 1024).write_volatile(0xAB); }
|
unsafe {
|
||||||
|
s.usable_base().sub(64 * 1024).write_volatile(0xAB);
|
||||||
|
}
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Ok("stack_overflow") => {
|
Ok("stack_overflow") => {
|
||||||
@@ -120,7 +124,12 @@ fn guard_page_causes_sigsegv() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +140,12 @@ fn stack_overflow_causes_sigsegv() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +189,12 @@ fn wide_guard_faults_at_top() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +205,11 @@ fn wide_guard_faults_at_bottom() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
assert_eq!(
|
||||||
|
status.signal(),
|
||||||
|
Some(11),
|
||||||
|
"expected SIGSEGV, got: {:?}",
|
||||||
|
status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -42,9 +42,15 @@ fn run_as_child_if_requested() {
|
|||||||
let opts = match mode.as_str() {
|
let opts = match mode.as_str() {
|
||||||
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
"rust_overflow" | "ffi_tier1" => SpawnOpts::default(),
|
||||||
// Small guard: the canary's 96 KiB displacement clears it.
|
// Small guard: the canary's 96 KiB displacement clears it.
|
||||||
"ffi_tier2" => SpawnOpts { guard_size: Some(4096), ..SpawnOpts::default() },
|
"ffi_tier2" => SpawnOpts {
|
||||||
|
guard_size: Some(4096),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
// Enough reserve: the same frame simply fits.
|
// Enough reserve: the same frame simply fits.
|
||||||
"ffi_clean" => SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() },
|
"ffi_clean" => SpawnOpts {
|
||||||
|
stack_reserve: Some(256 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
other => panic!("unknown subtest {other}"),
|
other => panic!("unknown subtest {other}"),
|
||||||
};
|
};
|
||||||
let is_rust = mode == "rust_overflow";
|
let is_rust = mode == "rust_overflow";
|
||||||
@@ -92,8 +98,14 @@ fn rust_overflow_dies_with_tier1_message() {
|
|||||||
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
err.contains("overflowed its stack") && err.contains("in the guard region"),
|
||||||
"missing tier-1 diagnostic; stderr:\n{err}"
|
"missing tier-1 diagnostic; stderr:\n{err}"
|
||||||
);
|
);
|
||||||
assert!(err.contains("reserve=65536"), "wrong reserve in message:\n{err}");
|
assert!(
|
||||||
assert!(err.contains("guard=1048576"), "wrong guard in message:\n{err}");
|
err.contains("reserve=65536"),
|
||||||
|
"wrong reserve in message:\n{err}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
err.contains("guard=1048576"),
|
||||||
|
"wrong guard in message:\n{err}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+16
-6
@@ -24,7 +24,11 @@ const RESERVE: usize = 4 * 1024 * 1024;
|
|||||||
fn burn_stack(frames: usize) -> u64 {
|
fn burn_stack(frames: usize) -> u64 {
|
||||||
let mut local = [0u8; 4096];
|
let mut local = [0u8; 4096];
|
||||||
local[0] = frames as u8;
|
local[0] = frames as u8;
|
||||||
let below = if frames == 0 { 0 } else { burn_stack(frames - 1) };
|
let below = if frames == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
burn_stack(frames - 1)
|
||||||
|
};
|
||||||
std::hint::black_box(&mut local);
|
std::hint::black_box(&mut local);
|
||||||
below.wrapping_add(local[0] as u64)
|
below.wrapping_add(local[0] as u64)
|
||||||
}
|
}
|
||||||
@@ -33,10 +37,13 @@ fn burn_stack(frames: usize) -> u64 {
|
|||||||
fn resident_pages(lo: usize, len: usize) -> usize {
|
fn resident_pages(lo: usize, len: usize) -> usize {
|
||||||
let page = 4096;
|
let page = 4096;
|
||||||
let mut vec = vec![0u8; len / page];
|
let mut vec = vec![0u8; len / page];
|
||||||
let ret = unsafe {
|
let ret = unsafe { libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr()) };
|
||||||
libc::mincore(lo as *mut libc::c_void, len, vec.as_mut_ptr())
|
assert_eq!(
|
||||||
};
|
ret,
|
||||||
assert_eq!(ret, 0, "mincore failed: {}", std::io::Error::last_os_error());
|
0,
|
||||||
|
"mincore failed: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
vec.iter().filter(|&&b| b & 1 != 0).count()
|
vec.iter().filter(|&&b| b & 1 != 0).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +125,9 @@ fn recycle_zaps_dead_stack_down_to_retain() {
|
|||||||
[{usable_base:#x}, +{zap_len:#x})"
|
[{usable_base:#x}, +{zap_len:#x})"
|
||||||
);
|
);
|
||||||
// Pooled, not munmapped: the mapping must still be there.
|
// Pooled, not munmapped: the mapping must still be there.
|
||||||
assert!(vma_exists(usable_base), "default-shaped stack was unmapped instead of pooled");
|
assert!(
|
||||||
|
vma_exists(usable_base),
|
||||||
|
"default-shaped stack was unmapped instead of pooled"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -66,7 +66,10 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
|||||||
assert!(spike > SHRINK_THRESHOLD);
|
assert!(spike > SHRINK_THRESHOLD);
|
||||||
|
|
||||||
let worker = spawn_with(
|
let worker = spawn_with(
|
||||||
SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() },
|
SpawnOpts {
|
||||||
|
stack_reserve: Some(8 * 1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
},
|
||||||
move || {
|
move || {
|
||||||
// Live data that must survive the shrink, and an anchor
|
// Live data that must survive the shrink, and an anchor
|
||||||
// address inside the stack for the smaps scan.
|
// address inside the stack for the smaps scan.
|
||||||
@@ -111,7 +114,11 @@ fn spike_then_parks_marks_lazyfree_and_keeps_live_data() {
|
|||||||
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
"expected ≥ 2 MiB LazyFree in the stack range, got {} bytes",
|
||||||
lazy
|
lazy
|
||||||
);
|
);
|
||||||
assert_eq!(checksum, 64 * 0xA5u64, "live stack data corrupted by shrink");
|
assert_eq!(
|
||||||
|
checksum,
|
||||||
|
64 * 0xA5u64,
|
||||||
|
"live stack data corrupted by shrink"
|
||||||
|
);
|
||||||
worker.join().unwrap();
|
worker.join().unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,7 +138,9 @@ fn shallow_actor_never_shrinks() {
|
|||||||
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
for _ in 0..(SHRINK_COOLDOWN + 8) {
|
||||||
park_rx.recv().unwrap();
|
park_rx.recv().unwrap();
|
||||||
}
|
}
|
||||||
done_tx.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096)).unwrap();
|
done_tx
|
||||||
|
.send(lazy_free_bytes_in(anchor - 64 * 1024, anchor + 4096))
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let wpid = worker.pid();
|
let wpid = worker.pid();
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
//! registry entry guarantees for every named server.
|
//! registry entry guarantees for every named server.
|
||||||
|
|
||||||
use smarm::{
|
use smarm::{
|
||||||
call, channel, init, request_stop, spawn, Config, GenServer, GenServerBuilder, GenServerName,
|
call, channel, init, request_stop, spawn, CallError, Config, GenServer, GenServerBuilder,
|
||||||
CallError, Receiver, RecvTimeoutError,
|
GenServerName, Receiver, RecvTimeoutError,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -73,10 +73,12 @@ fn named_server_request_stop_releases_queued_caller_with_server_down() {
|
|||||||
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
let (res_tx, res_rx) = channel::<Result<(), CallError>>();
|
||||||
|
|
||||||
// 1. Start the named server and keep its ref alive.
|
// 1. Start the named server and keep its ref alive.
|
||||||
let server = GenServerBuilder::new(Blocker { gate: Some(gate_rx) })
|
let server = GenServerBuilder::new(Blocker {
|
||||||
.named(BLOCKER)
|
gate: Some(gate_rx),
|
||||||
.start()
|
})
|
||||||
.expect("name should be free");
|
.named(BLOCKER)
|
||||||
|
.start()
|
||||||
|
.expect("name should be free");
|
||||||
let spid = server.pid();
|
let spid = server.pid();
|
||||||
|
|
||||||
// 2. Send the cast and let the server dequeue it and park on the gate.
|
// 2. Send the cast and let the server dequeue it and park on the gate.
|
||||||
|
|||||||
+16
-8
@@ -10,7 +10,11 @@
|
|||||||
//! out rather than produce a false pass — run with `cargo test -- --timeout`
|
//! out rather than produce a false pass — run with `cargo test -- --timeout`
|
||||||
//! or under a CI timeout.
|
//! or under a CI timeout.
|
||||||
|
|
||||||
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle};
|
use smarm::{
|
||||||
|
channel,
|
||||||
|
runtime::{Config, Runtime},
|
||||||
|
spawn, yield_now, JoinHandle,
|
||||||
|
};
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
@@ -199,7 +203,9 @@ fn thundering_herd_all_wake() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let all receivers park before we send.
|
// Let all receivers park before we send.
|
||||||
for _ in 0..4 { yield_now(); }
|
for _ in 0..4 {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
|
||||||
// Coordinator blasts all channels.
|
// Coordinator blasts all channels.
|
||||||
handles.push(spawn(move || {
|
handles.push(spawn(move || {
|
||||||
@@ -240,8 +246,7 @@ fn concurrent_spawn_join_churn() {
|
|||||||
for _ in 0..PARENTS {
|
for _ in 0..PARENTS {
|
||||||
let tc = t.clone();
|
let tc = t.clone();
|
||||||
parent_handles.push(spawn(move || {
|
parent_handles.push(spawn(move || {
|
||||||
let mut child_handles: Vec<JoinHandle> =
|
let mut child_handles: Vec<JoinHandle> = Vec::with_capacity(CHILDREN_PER_PARENT);
|
||||||
Vec::with_capacity(CHILDREN_PER_PARENT);
|
|
||||||
|
|
||||||
for _ in 0..CHILDREN_PER_PARENT {
|
for _ in 0..CHILDREN_PER_PARENT {
|
||||||
let tcc = tc.clone();
|
let tcc = tc.clone();
|
||||||
@@ -292,7 +297,9 @@ fn join_race_child_finishes_first() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Yield enough to let children run to completion before we join.
|
// Yield enough to let children run to completion before we join.
|
||||||
for _ in 0..8 { yield_now(); }
|
for _ in 0..8 {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
|
||||||
for h in handles {
|
for h in handles {
|
||||||
// If child already finished, join must return immediately with Ok.
|
// If child already finished, join must return immediately with Ok.
|
||||||
@@ -374,8 +381,7 @@ fn panic_storm_does_not_corrupt_scheduler() {
|
|||||||
fn pid_generation_increments_on_reuse() {
|
fn pid_generation_increments_on_reuse() {
|
||||||
use smarm::self_pid;
|
use smarm::self_pid;
|
||||||
|
|
||||||
let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> =
|
let pids: Arc<smarm::Mutex<Vec<smarm::Pid>>> = Arc::new(smarm::Mutex::new(Vec::new()));
|
||||||
Arc::new(smarm::Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let p = pids.clone();
|
let p = pids.clone();
|
||||||
rt(1).run(move || {
|
rt(1).run(move || {
|
||||||
@@ -392,7 +398,9 @@ fn pid_generation_increments_on_reuse() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let g = pids.lock_timeout(std::time::Duration::from_secs(1)).unwrap();
|
let g = pids
|
||||||
|
.lock_timeout(std::time::Duration::from_secs(1))
|
||||||
|
.unwrap();
|
||||||
// Any two PIDs that share an index must have different generations.
|
// Any two PIDs that share an index must have different generations.
|
||||||
for i in 0..g.len() {
|
for i in 0..g.len() {
|
||||||
for j in (i + 1)..g.len() {
|
for j in (i + 1)..g.len() {
|
||||||
|
|||||||
+10
-2
@@ -51,7 +51,11 @@ fn transient_child_is_restarted_on_panic_then_settles() {
|
|||||||
});
|
});
|
||||||
sup.join().unwrap();
|
sup.join().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(runs.load(Ordering::SeqCst), 3, "two restarts then a clean exit");
|
assert_eq!(
|
||||||
|
runs.load(Ordering::SeqCst),
|
||||||
|
3,
|
||||||
|
"two restarts then a clean exit"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -167,7 +171,11 @@ fn one_for_all_restarts_a_normally_exited_sibling() {
|
|||||||
sup.join().unwrap();
|
sup.join().unwrap();
|
||||||
});
|
});
|
||||||
assert_eq!(a.load(Ordering::SeqCst), 2, "A: crash then clean run");
|
assert_eq!(a.load(Ordering::SeqCst), 2, "A: crash then clean run");
|
||||||
assert_eq!(b.load(Ordering::SeqCst), 2, "B cycled with the group despite a clean exit");
|
assert_eq!(
|
||||||
|
b.load(Ordering::SeqCst),
|
||||||
|
2,
|
||||||
|
"B cycled with the group despite a clean exit"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
//! Supervisor shutdown — the OTP child-spec `shutdown` policy.
|
||||||
|
//!
|
||||||
|
//! A supervisor traps exits. A `request_shutdown` reaching it (from its parent
|
||||||
|
//! supervisor, or from the app via `request_shutdown`/`RuntimeHandle`) runs
|
||||||
|
//! the ordered shutdown: children are stopped in reverse start order, each
|
||||||
|
//! per its `Shutdown` policy — `request_shutdown`, wait up to the timeout for
|
||||||
|
//! its termination signal, `request_stop` if it overstays — and then `run()`
|
||||||
|
//! returns normally. Every supervisor-initiated child stop (ordered shutdown,
|
||||||
|
//! OneForAll/RestForOne sibling cycling) goes through the same policy.
|
||||||
|
//!
|
||||||
|
//! A *hard* `request_stop` on a supervisor unwinds it; a drop guard then
|
||||||
|
//! hard-stops its live children so the subtree is never orphaned.
|
||||||
|
|
||||||
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown, Strategy};
|
||||||
|
use smarm::{
|
||||||
|
monitor, request_shutdown, request_stop, run, sleep, spawn, trap_exit, DownReason, JoinHandle,
|
||||||
|
};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// A child that traps exits, records the order it was shut down in, and exits
|
||||||
|
/// normally on the request (after `delay`). Ignores the request if `comply`
|
||||||
|
/// is false — a straggler that must be hard-stopped.
|
||||||
|
fn polite_child(
|
||||||
|
tag: usize,
|
||||||
|
log: &Arc<Mutex<Vec<usize>>>,
|
||||||
|
delay: Duration,
|
||||||
|
comply: bool,
|
||||||
|
) -> impl Fn() + Send + Sync + 'static {
|
||||||
|
let log = log.clone();
|
||||||
|
move || {
|
||||||
|
let inbox = trap_exit();
|
||||||
|
loop {
|
||||||
|
let sig = match inbox.recv() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if sig.reason == DownReason::Shutdown {
|
||||||
|
log.lock().unwrap().push(tag);
|
||||||
|
if comply {
|
||||||
|
sleep(delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Not complying: keep running until hard-stopped.
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn `sup`, let its children reach `trap_exit`, return the handle.
|
||||||
|
fn spawn_settled(sup: OneForOne) -> JoinHandle {
|
||||||
|
let h = spawn(move || sup.run());
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shutdown_stops_children_in_reverse_order_and_returns_normally() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let sup = OneForOne::new()
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(1, &l, Duration::ZERO, true),
|
||||||
|
))
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(2, &l, Duration::ZERO, true),
|
||||||
|
))
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(3, &l, Duration::ZERO, true),
|
||||||
|
));
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
let mon = monitor(h.pid());
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
let down = mon.rx.recv().expect("down");
|
||||||
|
assert_eq!(
|
||||||
|
down.reason,
|
||||||
|
DownReason::Exit,
|
||||||
|
"supervisor exits normally after shutdown"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert_eq!(*log.lock().unwrap(), vec![3, 2, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_trapping_child_is_simply_stopped() {
|
||||||
|
let dropped = Arc::new(AtomicBool::new(false));
|
||||||
|
let d = dropped.clone();
|
||||||
|
run(move || {
|
||||||
|
struct G(Arc<AtomicBool>);
|
||||||
|
impl Drop for G {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sup = OneForOne::new().child(ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
let _g = G(d.clone());
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
});
|
||||||
|
assert!(dropped.load(Ordering::SeqCst));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn straggler_is_hard_stopped_after_timeout() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let sup = OneForOne::new().child(
|
||||||
|
ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(1, &l, Duration::ZERO, false),
|
||||||
|
)
|
||||||
|
.shutdown(Shutdown::Timeout(Duration::from_millis(50))),
|
||||||
|
);
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
let t0 = Instant::now();
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
let took = t0.elapsed();
|
||||||
|
assert!(
|
||||||
|
took >= Duration::from_millis(50),
|
||||||
|
"returned before the grace period: {took:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
took < Duration::from_secs(2),
|
||||||
|
"did not fall back to a hard stop: {took:?}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
*log.lock().unwrap(),
|
||||||
|
vec![1],
|
||||||
|
"the straggler did receive the request"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infinity_waits_for_a_slow_but_compliant_child() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
let finished = Arc::new(AtomicBool::new(false));
|
||||||
|
let f = finished.clone();
|
||||||
|
run(move || {
|
||||||
|
let f2 = f.clone();
|
||||||
|
let l2 = l.clone();
|
||||||
|
let sup = OneForOne::new().child(
|
||||||
|
ChildSpec::new(Restart::Permanent, move || {
|
||||||
|
let inbox = trap_exit();
|
||||||
|
let _ = inbox.recv();
|
||||||
|
l2.lock().unwrap().push(1);
|
||||||
|
sleep(Duration::from_millis(150));
|
||||||
|
f2.store(true, Ordering::SeqCst); // only reached if not hard-stopped
|
||||||
|
})
|
||||||
|
.shutdown(Shutdown::Infinity),
|
||||||
|
);
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
finished.load(Ordering::SeqCst),
|
||||||
|
"Infinity must not hard-stop a compliant child"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brutal_kill_skips_the_request() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let sup = OneForOne::new().child(
|
||||||
|
ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(1, &l, Duration::ZERO, true),
|
||||||
|
)
|
||||||
|
.shutdown(Shutdown::BrutalKill),
|
||||||
|
);
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
log.lock().unwrap().is_empty(),
|
||||||
|
"a BrutalKill child never sees the request"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hard_stop_of_supervisor_does_not_orphan_children() {
|
||||||
|
let alive = Arc::new(AtomicUsize::new(0));
|
||||||
|
let a = alive.clone();
|
||||||
|
run(move || {
|
||||||
|
struct Alive(Arc<AtomicUsize>);
|
||||||
|
impl Drop for Alive {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mk = |a: Arc<AtomicUsize>| {
|
||||||
|
move || {
|
||||||
|
a.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let _g = Alive(a.clone());
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let sup = OneForOne::new()
|
||||||
|
.child(ChildSpec::new(Restart::Permanent, mk(a.clone())))
|
||||||
|
.child(ChildSpec::new(Restart::Permanent, mk(a.clone())));
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
assert_eq!(a.load(Ordering::SeqCst), 2);
|
||||||
|
let mon = monitor(h.pid());
|
||||||
|
request_stop(h.pid());
|
||||||
|
let _ = mon.rx.recv();
|
||||||
|
sleep(Duration::from_millis(50));
|
||||||
|
assert_eq!(
|
||||||
|
a.load(Ordering::SeqCst),
|
||||||
|
0,
|
||||||
|
"children orphaned by a hard supervisor stop"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_shutdown_reaches_grandchildren() {
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
run(move || {
|
||||||
|
let l_inner = l.clone();
|
||||||
|
let inner = move || {
|
||||||
|
OneForOne::new()
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(10, &l_inner, Duration::ZERO, true),
|
||||||
|
))
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(11, &l_inner, Duration::ZERO, true),
|
||||||
|
))
|
||||||
|
.run()
|
||||||
|
};
|
||||||
|
let sup = OneForOne::new()
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(1, &l, Duration::ZERO, true),
|
||||||
|
))
|
||||||
|
.child(ChildSpec::new(Restart::Permanent, inner).shutdown(Shutdown::Infinity));
|
||||||
|
let h = spawn_settled(sup);
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
});
|
||||||
|
assert_eq!(*log.lock().unwrap(), vec![11, 10, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sibling_cycling_uses_graceful_shutdown() {
|
||||||
|
// OneForAll: when child A dies, sibling B (trapping) must receive a
|
||||||
|
// Shutdown request rather than a bare stop.
|
||||||
|
let log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let l = log.clone();
|
||||||
|
let a_runs = Arc::new(AtomicUsize::new(0));
|
||||||
|
let ar = a_runs.clone();
|
||||||
|
run(move || {
|
||||||
|
let ar2 = ar.clone();
|
||||||
|
let sup = OneForOne::new()
|
||||||
|
.strategy(Strategy::OneForAll)
|
||||||
|
.intensity(5, Duration::from_secs(60))
|
||||||
|
.child(ChildSpec::new(Restart::Transient, move || {
|
||||||
|
let n = ar2.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
sleep(Duration::from_millis(30));
|
||||||
|
if n == 1 {
|
||||||
|
panic!("first run dies");
|
||||||
|
}
|
||||||
|
// Second run: park until shut down.
|
||||||
|
let inbox = trap_exit();
|
||||||
|
let _ = inbox.recv();
|
||||||
|
}))
|
||||||
|
.child(ChildSpec::new(
|
||||||
|
Restart::Permanent,
|
||||||
|
polite_child(2, &l, Duration::ZERO, true),
|
||||||
|
));
|
||||||
|
let h = spawn(move || sup.run());
|
||||||
|
sleep(Duration::from_millis(150));
|
||||||
|
request_shutdown(h.pid());
|
||||||
|
h.join().expect("sup");
|
||||||
|
});
|
||||||
|
// B was shut down once by the cycle and once by the final shutdown.
|
||||||
|
assert_eq!(*log.lock().unwrap(), vec![2, 2]);
|
||||||
|
assert_eq!(a_runs.load(Ordering::SeqCst), 2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//! The terminal-record contract (bridge soak signature 4): a watch installed
|
||||||
|
//! *after* its target's death — the async-install race the bridge's proxies
|
||||||
|
//! live with — must be able to recover the real down reason instead of a
|
||||||
|
//! blanket `NoProc`. Two primitives carry it:
|
||||||
|
//!
|
||||||
|
//! - `finalize_actor` stamps the slot with `(generation, DownReason)`; the
|
||||||
|
//! record survives reclaim, registry pruning, and the next tenant's
|
||||||
|
//! install, and is overwritten only by the slot's next death.
|
||||||
|
//! [`terminal_reason`] reads it generation-matched.
|
||||||
|
//! - [`resolve_name`] is `whereis` with the corpse kept: the dead-holder arm
|
||||||
|
//! returns the stored pid it prunes ([`NameResolution::Corpse`]) instead
|
||||||
|
//! of discarding the only evidence of *who* died. `Unbound` stays the
|
||||||
|
//! Erlang-shaped `noproc` for names that were never (or are no longer)
|
||||||
|
//! bound.
|
||||||
|
//!
|
||||||
|
//! `monitor()` of a stale pid still queues plain `NoProc` — the upgrade is a
|
||||||
|
//! caller's deliberate act, not a semantics change.
|
||||||
|
|
||||||
|
use smarm::{
|
||||||
|
init, mark_watchable, request_stop, resolve_name, terminal_reason, CallError, Config,
|
||||||
|
DownReason, GenServer, GenServerBuilder, GenServerName, NameResolution,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const TARGET: GenServerName<Target> = GenServerName::new("terminal_target");
|
||||||
|
|
||||||
|
/// Named server that panics on cast — the sig-4 death.
|
||||||
|
struct Target;
|
||||||
|
|
||||||
|
impl GenServer for Target {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn handle_call(&mut self, _req: ()) {}
|
||||||
|
fn handle_cast(&mut self, _op: ()) {
|
||||||
|
panic!("terminal_target: induced panic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slot filler for the re-tenancy phase (distinct type, held alive).
|
||||||
|
struct Filler;
|
||||||
|
|
||||||
|
impl GenServer for Filler {
|
||||||
|
type Call = ();
|
||||||
|
type Reply = ();
|
||||||
|
type Cast = ();
|
||||||
|
type Info = ();
|
||||||
|
type Timer = ();
|
||||||
|
|
||||||
|
fn handle_call(&mut self, _req: ()) {}
|
||||||
|
fn handle_cast(&mut self, _op: ()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Observed {
|
||||||
|
exit_reason: Option<DownReason>,
|
||||||
|
anon_reason: Option<DownReason>,
|
||||||
|
/// Anonymous but export-marked while alive — must stamp (sig 5).
|
||||||
|
marked_reason: Option<DownReason>,
|
||||||
|
/// Marked only after death — must remain unknowable.
|
||||||
|
marked_late_reason: Option<DownReason>,
|
||||||
|
panic_reason: Option<DownReason>,
|
||||||
|
stopped_reason: Option<DownReason>,
|
||||||
|
live_reason: Option<DownReason>,
|
||||||
|
live_resolution_is_live: bool,
|
||||||
|
unknown_resolution: NameResolution,
|
||||||
|
/// First resolve after the named target's panic — must be Corpse(old pid).
|
||||||
|
corpse_resolution_matches: bool,
|
||||||
|
/// Second resolve — the Corpse arm pruned, so the name has healed.
|
||||||
|
resolution_after_prune: NameResolution,
|
||||||
|
/// Read AFTER the prune above: the record is slot-side, not registry-side.
|
||||||
|
corpse_reason_after_prune: Option<DownReason>,
|
||||||
|
/// Record survives the slot being re-tenanted (new tenant still alive).
|
||||||
|
corpse_reason_after_reuse: Option<DownReason>,
|
||||||
|
/// ... and dies with the next tenancy's death (overwritten).
|
||||||
|
corpse_reason_after_tenant_death: Option<DownReason>,
|
||||||
|
tenant_reason: Option<DownReason>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
|
||||||
|
let out: Arc<Mutex<Option<Observed>>> = Arc::new(Mutex::new(None));
|
||||||
|
let out_w = out.clone();
|
||||||
|
|
||||||
|
// Tiny slab: prompt slot recycling for the re-tenancy phase.
|
||||||
|
init(Config::exact(2).max_actors(32)).run(move || {
|
||||||
|
// --- Registered plain actors: one record per way of dying. The
|
||||||
|
// record is named-tenancy-only, so each actor self-registers a
|
||||||
|
// throwaway channel before dying; the anonymous control below pins
|
||||||
|
// the complement.
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_exit"), tx);
|
||||||
|
});
|
||||||
|
let pid_exit = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let exit_reason = terminal_reason(pid_exit);
|
||||||
|
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_panic"), tx);
|
||||||
|
panic!("induced");
|
||||||
|
});
|
||||||
|
let pid_panic = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let panic_reason = terminal_reason(pid_panic);
|
||||||
|
|
||||||
|
let h = smarm::spawn(|| {
|
||||||
|
let (tx, _rx) = smarm::channel::<()>();
|
||||||
|
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_stop"), tx);
|
||||||
|
loop {
|
||||||
|
smarm::sleep(Duration::from_millis(2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pid_stop = h.pid();
|
||||||
|
request_stop(pid_stop);
|
||||||
|
let _ = h.join();
|
||||||
|
let stopped_reason = terminal_reason(pid_stop);
|
||||||
|
|
||||||
|
// --- Anonymous control: an unregistered death must NOT stamp (nor
|
||||||
|
// evict) — the free list is LIFO, so green-thread churn would
|
||||||
|
// otherwise overwrite a watchable record faster than any race
|
||||||
|
// window this exists to cover.
|
||||||
|
let h = smarm::spawn(|| panic!("anonymous"));
|
||||||
|
let pid_anon = h.pid();
|
||||||
|
let _ = h.join();
|
||||||
|
let anon_reason = terminal_reason(pid_anon);
|
||||||
|
|
||||||
|
// --- mark_watchable: the bridge's export-seam eligibility (sig 5).
|
||||||
|
// An anonymous actor marked while alive stamps like a named one ...
|
||||||
|
let h = smarm::spawn(|| loop {
|
||||||
|
smarm::sleep(Duration::from_millis(2));
|
||||||
|
});
|
||||||
|
let pid_marked = h.pid();
|
||||||
|
mark_watchable(pid_marked);
|
||||||
|
request_stop(pid_marked);
|
||||||
|
let _ = h.join();
|
||||||
|
let marked_reason = terminal_reason(pid_marked);
|
||||||
|
|
||||||
|
// ... while marking a pid whose tenancy already ended is a no-op:
|
||||||
|
// the history is honestly unknowable, not retroactively invented.
|
||||||
|
mark_watchable(pid_anon);
|
||||||
|
let marked_late_reason = terminal_reason(pid_anon);
|
||||||
|
|
||||||
|
// --- The named target: live readings first. -----------------------
|
||||||
|
let target = GenServerBuilder::new(Target)
|
||||||
|
.named(TARGET)
|
||||||
|
.start()
|
||||||
|
.expect("name free at test start");
|
||||||
|
let old_pid = target.pid();
|
||||||
|
let live_reason = terminal_reason(old_pid);
|
||||||
|
let live_resolution_is_live =
|
||||||
|
resolve_name(TARGET.as_str()) == NameResolution::Live(old_pid.erase());
|
||||||
|
let unknown_resolution = resolve_name("terminal_never_bound");
|
||||||
|
|
||||||
|
// --- Kill it by panic; confirm death via the ref, NEVER the name
|
||||||
|
// (any name reader would take the prune arm and destroy the corpse
|
||||||
|
// precondition — the same trap stale_name_slot_reuse.rs documents).
|
||||||
|
let _ = target.cast(());
|
||||||
|
loop {
|
||||||
|
match target.call(()) {
|
||||||
|
Err(CallError::ServerDown) => break,
|
||||||
|
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let corpse_resolution_matches =
|
||||||
|
resolve_name(TARGET.as_str()) == NameResolution::Corpse(old_pid.erase());
|
||||||
|
let resolution_after_prune = resolve_name(TARGET.as_str());
|
||||||
|
let corpse_reason_after_prune = terminal_reason(old_pid);
|
||||||
|
|
||||||
|
// --- Re-tenant the freed slot; the record must outlive the install
|
||||||
|
// and die only with the next tenancy's death.
|
||||||
|
let mut fillers = Vec::new();
|
||||||
|
let mut tenant = None;
|
||||||
|
for i in 0..24 {
|
||||||
|
let name: &'static str = Box::leak(format!("terminal_filler_{i}").into_boxed_str());
|
||||||
|
let f = GenServerBuilder::new(Filler)
|
||||||
|
.named(GenServerName::<Filler>::new(name))
|
||||||
|
.start()
|
||||||
|
.expect("filler names are fresh");
|
||||||
|
let fp = f.pid();
|
||||||
|
let landed = fp.index() == old_pid.index();
|
||||||
|
fillers.push(f);
|
||||||
|
if landed {
|
||||||
|
tenant = Some((fillers.len() - 1, fp));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (tenant_at, tenant_pid) = tenant.expect(
|
||||||
|
"precondition: the freed slot must be re-tenanted within the tiny slab \
|
||||||
|
(slots are recycled; every filler is held alive)",
|
||||||
|
);
|
||||||
|
let corpse_reason_after_reuse = terminal_reason(old_pid);
|
||||||
|
|
||||||
|
request_stop(tenant_pid);
|
||||||
|
loop {
|
||||||
|
match fillers[tenant_at].call(()) {
|
||||||
|
Err(CallError::ServerDown) => break,
|
||||||
|
Ok(()) => smarm::sleep(Duration::from_millis(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let corpse_reason_after_tenant_death = terminal_reason(old_pid);
|
||||||
|
let tenant_reason = terminal_reason(tenant_pid);
|
||||||
|
|
||||||
|
*out_w.lock().unwrap() = Some(Observed {
|
||||||
|
exit_reason,
|
||||||
|
anon_reason,
|
||||||
|
panic_reason,
|
||||||
|
stopped_reason,
|
||||||
|
live_reason,
|
||||||
|
live_resolution_is_live,
|
||||||
|
unknown_resolution,
|
||||||
|
corpse_resolution_matches,
|
||||||
|
resolution_after_prune,
|
||||||
|
marked_reason,
|
||||||
|
marked_late_reason,
|
||||||
|
corpse_reason_after_prune,
|
||||||
|
corpse_reason_after_reuse,
|
||||||
|
corpse_reason_after_tenant_death,
|
||||||
|
tenant_reason,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let o = out.lock().unwrap().take().expect("runtime body completed");
|
||||||
|
assert_eq!(o.exit_reason, Some(DownReason::Exit), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.anon_reason, None,
|
||||||
|
"anonymous deaths must not stamp: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.panic_reason, Some(DownReason::Panic), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.marked_reason,
|
||||||
|
Some(DownReason::Stopped),
|
||||||
|
"mark_watchable while alive must make the death stamp: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.marked_late_reason, None,
|
||||||
|
"marking a dead tenancy must not invent history: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.stopped_reason, Some(DownReason::Stopped), "{o:?}");
|
||||||
|
assert_eq!(
|
||||||
|
o.live_reason, None,
|
||||||
|
"live tenancy must have no record: {o:?}"
|
||||||
|
);
|
||||||
|
assert!(o.live_resolution_is_live, "{o:?}");
|
||||||
|
assert_eq!(o.unknown_resolution, NameResolution::Unbound, "{o:?}");
|
||||||
|
assert!(
|
||||||
|
o.corpse_resolution_matches,
|
||||||
|
"first post-death resolve must carry the corpse: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.resolution_after_prune,
|
||||||
|
NameResolution::Unbound,
|
||||||
|
"the Corpse arm prunes — the name heals: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_prune,
|
||||||
|
Some(DownReason::Panic),
|
||||||
|
"the record is slot-side; registry pruning must not touch it: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_reuse,
|
||||||
|
Some(DownReason::Panic),
|
||||||
|
"a new tenant's install must leave the previous tenancy's record: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
o.corpse_reason_after_tenant_death, None,
|
||||||
|
"the next death overwrites — the old generation no longer matches: {o:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(o.tenant_reason, Some(DownReason::Stopped), "{o:?}");
|
||||||
|
}
|
||||||
@@ -35,7 +35,10 @@ impl PipePair {
|
|||||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||||
assert_eq!(r, 0, "pipe2 failed");
|
assert_eq!(r, 0, "pipe2 failed");
|
||||||
PipePair { read: fds[0], write: fds[1] }
|
PipePair {
|
||||||
|
read: fds[0],
|
||||||
|
write: fds[1],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +70,9 @@ fn run_with_watchdog(limit: Duration, body: impl FnOnce() + Send + 'static) {
|
|||||||
rt.run(body);
|
rt.run(body);
|
||||||
let _ = done_tx.send(());
|
let _ = done_tx.send(());
|
||||||
});
|
});
|
||||||
done_rx
|
done_rx.recv_timeout(limit).expect(
|
||||||
.recv_timeout(limit)
|
"Runtime::run did not return: idle scheduler thread was never woken at termination",
|
||||||
.expect("Runtime::run did not return: idle scheduler thread was never woken at termination");
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permanent-hang variant: sibling blocked in `poll_wake(wake_fd, None)`
|
/// Permanent-hang variant: sibling blocked in `poll_wake(wake_fd, None)`
|
||||||
|
|||||||
+30
-10
@@ -166,14 +166,19 @@ fn timers_only_pop_entries_whose_deadline_has_passed() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn timers_mix_sleep_and_wait_timeout_reasons() {
|
fn timers_mix_sleep_and_wait_timeout_reasons() {
|
||||||
let mut t = Timers::new();
|
let mut t = Timers::new();
|
||||||
let target = Arc::new(RecordingTarget { calls: Mutex::new(Vec::new()) });
|
let target = Arc::new(RecordingTarget {
|
||||||
|
calls: Mutex::new(Vec::new()),
|
||||||
|
});
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1);
|
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1);
|
||||||
t.insert(
|
t.insert(
|
||||||
now + Duration::from_millis(10),
|
now + Duration::from_millis(10),
|
||||||
Pid::new(1, 0),
|
Pid::new(1, 0),
|
||||||
Reason::WaitTimeout { target: target.clone(), epoch: 42 },
|
Reason::WaitTimeout {
|
||||||
|
target: target.clone(),
|
||||||
|
epoch: 42,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let due = t.pop_due(now + Duration::from_millis(20));
|
let due = t.pop_due(now + Duration::from_millis(20));
|
||||||
@@ -238,7 +243,10 @@ fn armed_send_timer_is_returned_and_fires() {
|
|||||||
|
|
||||||
let mut due = t.pop_due(now + Duration::from_millis(20));
|
let mut due = t.pop_due(now + Duration::from_millis(20));
|
||||||
assert_eq!(due.len(), 1, "an armed send timer should pop when due");
|
assert_eq!(due.len(), 1, "an armed send timer should pop when due");
|
||||||
assert!(!fired.load(Ordering::SeqCst), "pop must not fire on its own");
|
assert!(
|
||||||
|
!fired.load(Ordering::SeqCst),
|
||||||
|
"pop must not fire on its own"
|
||||||
|
);
|
||||||
run_fire(due.pop().unwrap());
|
run_fire(due.pop().unwrap());
|
||||||
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
|
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
|
||||||
assert!(t.is_empty());
|
assert!(t.is_empty());
|
||||||
@@ -282,7 +290,11 @@ fn cancel_after_fire_returns_false() {
|
|||||||
fn cancel_unknown_id_returns_false() {
|
fn cancel_unknown_id_returns_false() {
|
||||||
let mut t = Timers::new();
|
let mut t = Timers::new();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let id = t.insert_send(now + Duration::from_millis(5), Pid::new(0, 0), Box::new(|| {}));
|
let id = t.insert_send(
|
||||||
|
now + Duration::from_millis(5),
|
||||||
|
Pid::new(0, 0),
|
||||||
|
Box::new(|| {}),
|
||||||
|
);
|
||||||
assert!(t.cancel(id));
|
assert!(t.cancel(id));
|
||||||
// Second cancel of the same id: already gone.
|
// Second cancel of the same id: already gone.
|
||||||
assert!(!t.cancel(id));
|
assert!(!t.cancel(id));
|
||||||
@@ -293,7 +305,11 @@ fn send_timers_interleave_with_sleep_in_deadline_order() {
|
|||||||
let mut t = Timers::new();
|
let mut t = Timers::new();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1);
|
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1);
|
||||||
let _id = t.insert_send(now + Duration::from_millis(10), Pid::new(1, 0), Box::new(|| {}));
|
let _id = t.insert_send(
|
||||||
|
now + Duration::from_millis(10),
|
||||||
|
Pid::new(1, 0),
|
||||||
|
Box::new(|| {}),
|
||||||
|
);
|
||||||
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1);
|
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1);
|
||||||
|
|
||||||
let due = t.pop_due(now + Duration::from_millis(50));
|
let due = t.pop_due(now + Duration::from_millis(50));
|
||||||
@@ -308,7 +324,11 @@ fn send_timers_interleave_with_sleep_in_deadline_order() {
|
|||||||
fn clear_drops_armed_send_timers() {
|
fn clear_drops_armed_send_timers() {
|
||||||
let mut t = Timers::new();
|
let mut t = Timers::new();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let id = t.insert_send(now + Duration::from_millis(10), Pid::new(0, 0), Box::new(|| {}));
|
let id = t.insert_send(
|
||||||
|
now + Duration::from_millis(10),
|
||||||
|
Pid::new(0, 0),
|
||||||
|
Box::new(|| {}),
|
||||||
|
);
|
||||||
t.clear();
|
t.clear();
|
||||||
assert!(t.is_empty());
|
assert!(t.is_empty());
|
||||||
// The arm record is gone too: cancelling reports nothing to cancel.
|
// The arm record is gone too: cancelling reports nothing to cancel.
|
||||||
@@ -356,7 +376,7 @@ fn send_after_to_unresolved_name_is_silent() {
|
|||||||
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
||||||
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
||||||
sleep(Duration::from_millis(40)); // let it fire and no-op
|
sleep(Duration::from_millis(40)); // let it fire and no-op
|
||||||
// Reaching here without a panic is the assertion.
|
// Reaching here without a panic is the assertion.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,9 +421,9 @@ fn send_after_to_dead_typed_pid_is_silent() {
|
|||||||
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
||||||
let _id = send_after(Duration::from_millis(15), sink, 2);
|
let _id = send_after(Duration::from_millis(15), sink, 2);
|
||||||
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
||||||
// No panic; the sink is gone, so its report sender dropped with it —
|
// No panic; the sink is gone, so its report sender dropped with it —
|
||||||
// closed+empty is Err (documented), which also proves nothing
|
// closed+empty is Err (documented), which also proves nothing
|
||||||
// further was delivered.
|
// further was delivered.
|
||||||
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
assert!(report_rx.try_recv().is_err(), "nothing further delivered");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
//! Non-panicking spawn at slab capacity (`try_spawn`).
|
||||||
|
//!
|
||||||
|
//! Covers: parity with `spawn` when slots are free; `Err(AtCapacity)` instead
|
||||||
|
//! of a panic on a full slab (the spawning actor survives — the crash-loop
|
||||||
|
//! from the motivating slowloris incident cannot start); self-heal (a freed
|
||||||
|
//! slot makes the next `try_spawn` succeed); and exact claim-or-report
|
||||||
|
//! accounting under a multi-thread race for the last slots (no TOCTOU
|
||||||
|
//! overshoot, no panic).
|
||||||
|
|
||||||
|
use smarm::runtime::Config;
|
||||||
|
use smarm::{spawn, try_spawn, try_spawn_under_with, yield_now, SpawnError, SpawnOpts};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// A child that holds its slot until `release` flips, without parking
|
||||||
|
/// machinery: busy-yield keeps the scheduler moving and the slot occupied.
|
||||||
|
fn holder(release: Arc<AtomicBool>) -> impl FnOnce() + Send + 'static {
|
||||||
|
move || {
|
||||||
|
while !release.load(Ordering::Acquire) {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_spawn_is_spawn_when_slots_free() {
|
||||||
|
smarm::runtime::init(Config::exact(1)).run(|| {
|
||||||
|
let ran = Arc::new(AtomicBool::new(false));
|
||||||
|
let flag = ran.clone();
|
||||||
|
let h = try_spawn(move || flag.store(true, Ordering::Release))
|
||||||
|
.expect("slots free — must behave exactly like spawn");
|
||||||
|
h.join().unwrap();
|
||||||
|
assert!(ran.load(Ordering::Acquire));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_capacity_is_err_not_panic_and_accounting_is_exact() {
|
||||||
|
const MAX: usize = 8;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
// Fill the slab from the initial actor: slots are claimed at spawn
|
||||||
|
// time, so children need not have run yet. Count until refusal.
|
||||||
|
let mut held = Vec::new();
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => held.push(h),
|
||||||
|
Err(e) => {
|
||||||
|
assert_eq!(e, SpawnError::AtCapacity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initial actor occupies one slot; the rest were spawnable.
|
||||||
|
assert_eq!(held.len(), MAX - 1, "slab accounting must be exact");
|
||||||
|
// Still refusing (and still not panicking) on repeat.
|
||||||
|
assert!(matches!(try_spawn(|| ()), Err(SpawnError::AtCapacity)));
|
||||||
|
// The `_with` surface refuses identically — a custom shape must not
|
||||||
|
// reach stack allocation when there is no slot for it.
|
||||||
|
let opts = SpawnOpts {
|
||||||
|
stack_reserve: Some(1024 * 1024),
|
||||||
|
..SpawnOpts::default()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
try_spawn_under_with(smarm::self_pid(), opts, || ()),
|
||||||
|
Err(SpawnError::AtCapacity)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Self-heal: free the slots, join, and the next try_spawn succeeds.
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
let h = try_spawn(|| ()).expect("slots freed — must succeed again");
|
||||||
|
h.join().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_spawn_still_panics_at_capacity() {
|
||||||
|
// The existing invariant-check semantics of `spawn` are untouched: at a
|
||||||
|
// full slab it panics, the panic is caught at the actor isolation
|
||||||
|
// boundary, and it surfaces as a join error — exactly as before. The
|
||||||
|
// bomb actor is spawned into the LAST slot (so the slab is full only
|
||||||
|
// once the bomb itself is live) and the panic lands inside the bomb,
|
||||||
|
// not the initial actor.
|
||||||
|
const MAX: usize = 6;
|
||||||
|
smarm::runtime::init(Config::exact(1).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut held = Vec::new();
|
||||||
|
for _ in 0..MAX - 2 {
|
||||||
|
held.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
let armed = Arc::new(AtomicBool::new(false));
|
||||||
|
let armed2 = armed.clone();
|
||||||
|
let bomb = spawn(move || {
|
||||||
|
armed2.store(true, Ordering::Release);
|
||||||
|
// Slab is now full (initial + MAX−2 holders + this actor); the
|
||||||
|
// plain spawn must panic this actor.
|
||||||
|
let _ = spawn(|| ());
|
||||||
|
unreachable!("allocate_slot must have panicked");
|
||||||
|
});
|
||||||
|
let err = bomb
|
||||||
|
.join()
|
||||||
|
.expect_err("bomb must die by panic, not run through");
|
||||||
|
assert!(armed.load(Ordering::Acquire), "bomb must have actually run");
|
||||||
|
// The panic message is a formatted String (panic! with args).
|
||||||
|
let msg = err
|
||||||
|
.payload
|
||||||
|
.downcast_ref::<String>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "<non-string payload>".into());
|
||||||
|
assert!(
|
||||||
|
msg.contains("slot table exhausted"),
|
||||||
|
"panic must be the slab-exhaustion invariant message, got: {msg}"
|
||||||
|
);
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in held {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn racing_try_spawns_claim_exactly_the_free_slots() {
|
||||||
|
// 4 scheduler threads, 4 spawner actors hammering try_spawn for a small
|
||||||
|
// pool of remaining slots. Claim-or-report must hand out exactly the
|
||||||
|
// free slots across all racers — no overshoot (TOCTOU), no panic.
|
||||||
|
const MAX: usize = 32;
|
||||||
|
const SPAWNERS: usize = 4;
|
||||||
|
smarm::runtime::init(Config::exact(4).max_actors(MAX)).run(|| {
|
||||||
|
let release = Arc::new(AtomicBool::new(false));
|
||||||
|
let won = Arc::new(AtomicUsize::new(0));
|
||||||
|
let done = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
// Occupy some slots up front so the racers fight over a remainder.
|
||||||
|
let mut pre = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
pre.push(spawn(holder(release.clone())));
|
||||||
|
}
|
||||||
|
// Free slots now: MAX − 1 (initial) − 8 (pre) − SPAWNERS.
|
||||||
|
let up_for_grabs = MAX - 1 - 8 - SPAWNERS;
|
||||||
|
|
||||||
|
let mut spawners = Vec::new();
|
||||||
|
for _ in 0..SPAWNERS {
|
||||||
|
let release = release.clone();
|
||||||
|
let won = won.clone();
|
||||||
|
let done = done.clone();
|
||||||
|
spawners.push(spawn(move || {
|
||||||
|
loop {
|
||||||
|
match try_spawn(holder(release.clone())) {
|
||||||
|
Ok(h) => {
|
||||||
|
won.fetch_add(1, Ordering::AcqRel);
|
||||||
|
drop(h); // detached; slot held by the holder
|
||||||
|
}
|
||||||
|
Err(SpawnError::AtCapacity) => break,
|
||||||
|
Err(_) => unreachable!("non_exhaustive future-proofing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done.fetch_add(1, Ordering::AcqRel);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Wait for every racer to hit AtCapacity.
|
||||||
|
while done.load(Ordering::Acquire) < SPAWNERS {
|
||||||
|
yield_now();
|
||||||
|
}
|
||||||
|
assert_eq!(won.load(Ordering::Acquire), up_for_grabs);
|
||||||
|
|
||||||
|
release.store(true, Ordering::Release);
|
||||||
|
for h in pre.into_iter().chain(spawners) {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_error_is_a_real_error() {
|
||||||
|
let e = SpawnError::AtCapacity;
|
||||||
|
let msg = format!("{e}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("capacity"),
|
||||||
|
"Display should name the condition: {msg}"
|
||||||
|
);
|
||||||
|
let _: &dyn std::error::Error = &e;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user