test: define actor retry and transition guarantees
Cover failed-message retries from committed state and stop/restart FIFO preservation. Document in-memory all-or-nothing transitions and their process-local limits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
1cbb3ddbf3
commit
e27a40cbf5
|
|
@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file.
|
||||||
- Runnable two-actor counter demonstration and tests for runaway fairness,
|
- Runnable two-actor counter demonstration and tests for runaway fairness,
|
||||||
pending-message isolation, failures, backpressure, and all-or-nothing effects.
|
pending-message isolation, failures, backpressure, and all-or-nothing effects.
|
||||||
- Execution-model decision and Phase 1 runtime documentation.
|
- Execution-model decision and Phase 1 runtime documentation.
|
||||||
|
- Lifecycle tests proving failed-message retry from committed state and
|
||||||
|
stop/restart preservation of reserved-first FIFO order.
|
||||||
|
|
||||||
- Runnable Phase 0 experiment pinned to Rune 0.14.2, covering script execution,
|
- Runnable Phase 0 experiment pinned to Rune 0.14.2, covering script execution,
|
||||||
native calls, async suspension, instruction budgets, frame inspection,
|
native calls, async suspension, instruction budgets, frame inspection,
|
||||||
|
|
|
||||||
|
|
@ -483,4 +483,109 @@ mod tests {
|
||||||
assert!(snapshot.reserved.is_some());
|
assert!(snapshot.reserved.is_some());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restart_retries_failed_reservation_from_last_committed_state() -> ExperimentResult<()> {
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
let fail_once = Arc::new(AtomicBool::new(true));
|
||||||
|
let native_fail_once = Arc::clone(&fail_once);
|
||||||
|
let mut module = rune::Module::new();
|
||||||
|
module
|
||||||
|
.function("fail_once", move || {
|
||||||
|
native_fail_once.swap(false, Ordering::SeqCst)
|
||||||
|
})
|
||||||
|
.build()?;
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let code = runtime.publish(CompiledProgram::compile_with_module(
|
||||||
|
"fn check() { if fail_once() { 1 / 0 } else { 0 } } pub fn handle(state, kind, value) { let _ = check(); (state + value, []) }",
|
||||||
|
module,
|
||||||
|
)?)?;
|
||||||
|
let actor = runtime.spawn(code, 10, capacity(3))?;
|
||||||
|
let first = Message { kind: 1, value: 2 };
|
||||||
|
let second = Message { kind: 1, value: 3 };
|
||||||
|
runtime.send(actor, first)?;
|
||||||
|
runtime.send(actor, second)?;
|
||||||
|
for _ in 0..100 {
|
||||||
|
runtime.tick(capacity(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
let failed = runtime.inspect(actor)?;
|
||||||
|
assert!(matches!(failed.status, Status::Failed(_)));
|
||||||
|
assert_eq!(failed.state, 10);
|
||||||
|
assert_eq!(failed.reserved, Some(first));
|
||||||
|
assert_eq!(failed.queued, vec![second]);
|
||||||
|
assert!(runtime.send(actor, Message { kind: 1, value: 4 }).is_err());
|
||||||
|
assert!(
|
||||||
|
runtime
|
||||||
|
.drain_events()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, Event::Failed(id, _) if *id == actor))
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.restart(actor)?;
|
||||||
|
let restarted = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(restarted.id, failed.id);
|
||||||
|
assert_eq!(restarted.mailbox, failed.mailbox);
|
||||||
|
assert_eq!(restarted.implementation, failed.implementation);
|
||||||
|
assert_eq!(restarted.incarnation, failed.incarnation + 1);
|
||||||
|
assert_eq!(restarted.state, 10);
|
||||||
|
assert_eq!(restarted.queued, vec![first, second]);
|
||||||
|
assert_eq!(restarted.reserved, None);
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
runtime.tick(capacity(5));
|
||||||
|
}
|
||||||
|
let completed = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(completed.state, 15, "{completed:?}");
|
||||||
|
assert_eq!(completed.status, Status::Idle);
|
||||||
|
assert_eq!(completed.queued, Vec::<Message>::new());
|
||||||
|
assert_eq!(completed.reserved, None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_and_restart_preserve_reserved_and_queued_fifo_order() -> ExperimentResult<()> {
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let code = runtime.publish(CompiledProgram::compile(
|
||||||
|
"pub fn handle(state, kind, value) { (state * 10 + value, []) }",
|
||||||
|
)?)?;
|
||||||
|
let actor = runtime.spawn(code, 0, capacity(3))?;
|
||||||
|
let first = Message { kind: 0, value: 1 };
|
||||||
|
let second = Message { kind: 0, value: 2 };
|
||||||
|
let third = Message { kind: 0, value: 3 };
|
||||||
|
runtime.send(actor, first)?;
|
||||||
|
runtime.send(actor, second)?;
|
||||||
|
runtime.send(actor, third)?;
|
||||||
|
|
||||||
|
runtime.tick(capacity(1));
|
||||||
|
let running = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(running.status, Status::Running);
|
||||||
|
assert_eq!(running.state, 0);
|
||||||
|
assert_eq!(running.reserved, Some(first));
|
||||||
|
assert_eq!(running.queued, vec![second, third]);
|
||||||
|
|
||||||
|
runtime.stop(actor)?;
|
||||||
|
let stopped = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(stopped.status, Status::Stopped);
|
||||||
|
assert_eq!(stopped.state, 0);
|
||||||
|
assert_eq!(stopped.reserved, Some(first));
|
||||||
|
assert_eq!(stopped.queued, vec![second, third]);
|
||||||
|
assert!(runtime.send(actor, Message { kind: 0, value: 4 }).is_err());
|
||||||
|
|
||||||
|
runtime.restart(actor)?;
|
||||||
|
let restarted = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(restarted.state, 0);
|
||||||
|
assert_eq!(restarted.reserved, None);
|
||||||
|
assert_eq!(restarted.queued, vec![first, second, third]);
|
||||||
|
for _ in 0..100 {
|
||||||
|
runtime.tick(capacity(5));
|
||||||
|
}
|
||||||
|
let completed = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(completed.state, 123);
|
||||||
|
assert_eq!(completed.status, Status::Idle);
|
||||||
|
assert_eq!(completed.queued, Vec::<Message>::new());
|
||||||
|
assert!(completed.reserved.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,17 +45,51 @@ runnable; the caller still needs to drive ticks.
|
||||||
Successful handlers validate every send target and its aggregate queue
|
Successful handlers validate every send target and its aggregate queue
|
||||||
capacity before updating state, acknowledging the reserved message, and
|
capacity before updating state, acknowledging the reserved message, and
|
||||||
enqueueing effects. Invalid effects fail the actor without partial sends or a
|
enqueueing effects. Invalid effects fail the actor without partial sends or a
|
||||||
state commit. This all-or-nothing behavior is in-memory only, not a durable
|
state commit. This is a **logical, single-worker in-memory atomicity guarantee**:
|
||||||
transaction.
|
observers using the runtime see either the transition before commit or after
|
||||||
|
commit, not an intermediate state/effect set. It is not crash-safe or durable.
|
||||||
|
External side effects performed by native callbacks are outside this boundary
|
||||||
|
and cannot be rolled back.
|
||||||
|
|
||||||
Runtime errors retain the last committed state and reserved message, set an
|
Runtime errors retain the last committed state and reserved message, set an
|
||||||
observable failed status, and emit a failure event. External sends return errors
|
observable failed status, and emit a failure event. The failed message is not
|
||||||
|
silently acknowledged or automatically retried. External sends return errors
|
||||||
for unknown, unavailable, or full actors.
|
for unknown, unavailable, or full actors.
|
||||||
|
|
||||||
Stopping drops the invocation, retaining committed state, mailbox and reserved
|
Stopping drops the invocation, retaining committed state, mailbox and reserved
|
||||||
message. Restarting a stopped or failed actor preserves IDs, increments its
|
message. Restarting a stopped or failed actor preserves its actor ID, mailbox
|
||||||
incarnation, and puts its reserved message at the front of the queue. Restart
|
ID, implementation ID, committed state, and queued-message order; it increments
|
||||||
is explicit; there is no automatic poison-message retry or supervision policy.
|
its incarnation and puts the reserved message at the front of the queue.
|
||||||
|
Restart is explicit. Repeated failure of the same reserved message can therefore
|
||||||
|
block later messages until an operator changes the implementation or otherwise
|
||||||
|
resolves it; there is no automatic poison-message policy or supervision.
|
||||||
|
|
||||||
|
### Transition and retry guarantees
|
||||||
|
|
||||||
|
| Situation | State | Reserved input | Effects | Status |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Handler returns a valid transition | New state commits | Acknowledged | All validated sends enqueue | `Idle` |
|
||||||
|
| Rune execution or transition decoding fails | Last committed state retained | Retained | None from the returned transition | `Failed` |
|
||||||
|
| Any target is unknown, stopped, failed, or over capacity | Last committed state retained | Retained | No sends from the transition enqueue | `Failed` |
|
||||||
|
| Stop during an invocation | Last committed state retained | Retained | None from the incomplete transition | `Stopped` |
|
||||||
|
| Restart after stop or failure | Retained until retried | Moved to queue front | None until retry completes | `Idle` |
|
||||||
|
|
||||||
|
Mailbox capacity counts queued plus reserved messages. Before applying a
|
||||||
|
transition, the runtime validates all effects, including aggregate sends to a
|
||||||
|
single target. For a self-send, the completing input is acknowledged as part
|
||||||
|
of the same transition, freeing its reserved slot before the new effects are
|
||||||
|
enqueued. An invalid target or over-capacity effect rejects the entire returned
|
||||||
|
transition.
|
||||||
|
|
||||||
|
These are process-local guarantees only. A host crash loses state, mailboxes,
|
||||||
|
reservations and events. External/native callback effects may already have
|
||||||
|
occurred even when the Rune invocation later fails or is stopped.
|
||||||
|
|
||||||
|
Lifecycle tests verify that a failed reservation retries from the last
|
||||||
|
committed state, that stop/restart preserves reserved-first FIFO order, and
|
||||||
|
that a running actor's queued messages are not processed until the reservation
|
||||||
|
finishes. Transition tests cover malformed results, runtime failures, invalid
|
||||||
|
targets, aggregate capacity, and successful message effects.
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue