feat!: retain actor state when handlers return unit
Allow omitted state returns in handlers and shutdown hooks. Preserve committed state rather than invocation-local mutations, while acknowledging successful messages and committing valid sends. BREAKING CHANGE: unit returns no longer replace actor state with null. Use host state editing to explicitly set null state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
d0a195e9f5
commit
867eabb957
|
|
@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **Breaking:** Handler and shutdown-hook unit returns (including omitted returns)
|
||||||
|
now retain the previously committed state instead of replacing it with null.
|
||||||
|
Draft mutations are discarded; valid sends and message acknowledgement still commit.
|
||||||
- Change the bundled counter's `send` operation to `start`, which queues an
|
- Change the bundled counter's `send` operation to `start`, which queues an
|
||||||
increment to itself using `ctx.this`.
|
increment to itself using `ctx.this`.
|
||||||
- **Breaking:** Protocol v5 adds actor log thresholds to snapshots, guarded
|
- **Breaking:** Protocol v5 adds actor log thresholds to snapshots, guarded
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,11 @@ for Rune conversions and value limits. Protocol version 5 uses
|
||||||
`handle(ctx, state, sender, message) -> new_state`; `ctx.send(recipient, message)`
|
`handle(ctx, state, sender, message) -> new_state`; `ctx.send(recipient, message)`
|
||||||
records sends with the calling actor as sender, and
|
records sends with the calling actor as sender, and
|
||||||
`ctx.send_from(sender, recipient, message)` supports forwarding.
|
`ctx.send_from(sender, recipient, message)` supports forwarding.
|
||||||
|
Returning state is optional: an omitted return, `return;`, or `()` retains the
|
||||||
|
previously committed state, discarding invocation-local state mutations.
|
||||||
|
Valid sends still commit and the message is acknowledged. The same rule applies
|
||||||
|
to `on_shutdown(state)`. To replace state, return a non-unit portable value.
|
||||||
|
Unit can no longer be used to reset state to null; use host state editing instead.
|
||||||
Sender metadata is separate from message data; host-originated messages have
|
Sender metadata is separate from message data; host-originated messages have
|
||||||
unit/null sender. System messages are trusted runtime commands, not data an
|
unit/null sender. System messages are trusted runtime commands, not data an
|
||||||
actor can construct.
|
actor can construct.
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,11 @@ impl Runtime {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self, id: &ActorRef, state: rune::Value) -> ExperimentResult<()> {
|
fn commit(&mut self, id: &ActorRef, state: rune::Value) -> ExperimentResult<()> {
|
||||||
let state = value_from_rune(&state)?;
|
let state = if state.into_unit().is_ok() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value_from_rune(&state)?)
|
||||||
|
};
|
||||||
let effects = self.actors[id].context.take_effects();
|
let effects = self.actors[id].context.take_effects();
|
||||||
let mut counts = BTreeMap::<ActorRef, usize>::new();
|
let mut counts = BTreeMap::<ActorRef, usize>::new();
|
||||||
for effect in &effects {
|
for effect in &effects {
|
||||||
|
|
@ -473,7 +477,9 @@ impl Runtime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let actor = self.actors.get_mut(id).expect("scheduled actor exists");
|
let actor = self.actors.get_mut(id).expect("scheduled actor exists");
|
||||||
actor.snapshot.state = state;
|
if let Some(state) = state {
|
||||||
|
actor.snapshot.state = state;
|
||||||
|
}
|
||||||
actor.snapshot.reserved = None;
|
actor.snapshot.reserved = None;
|
||||||
actor.snapshot.status = Status::Idle;
|
actor.snapshot.status = Status::Idle;
|
||||||
actor.active = None;
|
actor.active = None;
|
||||||
|
|
@ -600,6 +606,89 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use runa_core::MAX_VALUE_NODES;
|
use runa_core::MAX_VALUE_NODES;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optional_state_retains_committed_state_and_commits_sends() -> ExperimentResult<()> {
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let implementation = publish(
|
||||||
|
&mut runtime,
|
||||||
|
r#"pub fn handle(ctx, state, sender, message) {
|
||||||
|
state.value = 999;
|
||||||
|
if message == 1 { ctx.send(ctx.this, 2); return; }
|
||||||
|
if message == 2 { return (); }
|
||||||
|
#{value: 7}
|
||||||
|
}
|
||||||
|
pub fn on_shutdown(state) { state.value = 1000; }"#,
|
||||||
|
)?;
|
||||||
|
let initial = Value::Record(BTreeMap::from([("value".to_owned(), Value::Integer(3))]));
|
||||||
|
let actor = runtime.spawn(implementation, initial.clone(), capacity(4))?;
|
||||||
|
runtime.send(actor, Value::Integer(1))?;
|
||||||
|
for _ in 0..50 {
|
||||||
|
runtime.tick(capacity(1));
|
||||||
|
}
|
||||||
|
let snapshot = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(snapshot.state, initial);
|
||||||
|
assert_eq!(snapshot.status, Status::Idle);
|
||||||
|
assert!(snapshot.reserved.is_none());
|
||||||
|
assert!(snapshot.queued.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.drain_events()
|
||||||
|
.iter()
|
||||||
|
.filter(|event| matches!(event, Event::Committed(id) if *id == actor))
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
runtime.send(actor, Value::Integer(3))?;
|
||||||
|
for _ in 0..50 {
|
||||||
|
runtime.tick(capacity(32));
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
runtime.inspect(actor)?.state,
|
||||||
|
Value::Record(BTreeMap::from([("value".to_owned(), Value::Integer(7))]))
|
||||||
|
);
|
||||||
|
runtime.stop(actor)?;
|
||||||
|
for _ in 0..50 {
|
||||||
|
runtime.tick(capacity(32));
|
||||||
|
}
|
||||||
|
assert_eq!(runtime.inspect(actor)?.status, Status::Stopped);
|
||||||
|
assert_eq!(
|
||||||
|
runtime.inspect(actor)?.state,
|
||||||
|
Value::Record(BTreeMap::from([("value".to_owned(), Value::Integer(7))]))
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omitted_state_still_validates_effects_and_handles_null_state() -> ExperimentResult<()> {
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let implementation = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) { ctx.send(message, 1); }",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(implementation, Value::Integer(5), capacity(2))?;
|
||||||
|
runtime.send(actor, Value::ActorRef(ActorRef::new()))?;
|
||||||
|
for _ in 0..20 {
|
||||||
|
runtime.tick(capacity(32));
|
||||||
|
}
|
||||||
|
let snapshot = runtime.inspect(actor)?;
|
||||||
|
assert_eq!(snapshot.state, Value::Integer(5));
|
||||||
|
assert!(matches!(snapshot.status, Status::Failed(_)));
|
||||||
|
assert!(snapshot.reserved.is_some());
|
||||||
|
let implementation = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) {}",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(implementation, Value::Null, capacity(2))?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
for _ in 0..10 {
|
||||||
|
runtime.tick(capacity(32));
|
||||||
|
}
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Null);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.status, Status::Idle);
|
||||||
|
assert!(runtime.inspect(actor)?.reserved.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editing_is_guarded_atomic_and_can_repair_a_failed_reservation() -> ExperimentResult<()> {
|
fn editing_is_guarded_atomic_and_can_repair_a_failed_reservation() -> ExperimentResult<()> {
|
||||||
let mut runtime = Runtime::default();
|
let mut runtime = Runtime::default();
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ Start with bounded queues and observable backpressure. For durable mailboxes, in
|
||||||
|
|
||||||
### 4.3 State
|
### 4.3 State
|
||||||
|
|
||||||
State is owned by the actor runtime, logically private, and supplied to each scheduled handler invocation. The handler works with a controlled draft or value and returns new state; it cannot mutate committed state directly. State has a schema/version where useful and can be inspected via snapshots at safe points.
|
State is owned by the actor runtime, logically private, and supplied to each scheduled handler invocation. The handler works with a controlled draft or value and optionally returns new state; it cannot mutate committed state directly. Returning unit or omitting a return retains the previous committed state and discards draft mutations. State has a schema/version where useful and can be inspected via snapshots at safe points.
|
||||||
|
|
||||||
### 4.4 Stateless handler
|
### 4.4 Stateless handler
|
||||||
|
|
||||||
|
|
@ -479,7 +479,8 @@ explicit host-provided migration closure for structured actor state.
|
||||||
Actor snapshots expose active VM instruction progress, instruction pointer,
|
Actor snapshots expose active VM instruction progress, instruction pointer,
|
||||||
and call-frame depth. State and messages are owned portable trees of null,
|
and call-frame depth. State and messages are owned portable trees of null,
|
||||||
booleans, integers, strings, lists, records, and distinct actor references.
|
booleans, integers, strings, lists, records, and distinct actor references.
|
||||||
Rune handlers receive `(ctx, state, sender, message)` and return only state;
|
Rune handlers receive `(ctx, state, sender, message)` and optionally return state;
|
||||||
|
unit returns retain committed state and discard invocation-local draft mutations.
|
||||||
context sends are buffered and committed atomically with state.
|
context sends are buffered and committed atomically with state.
|
||||||
Rune heap objects never become shared committed actor data. State schemas,
|
Rune heap objects never become shared committed actor data. State schemas,
|
||||||
version compatibility checks, Rune-authored migration functions, source
|
version compatibility checks, Rune-authored migration functions, source
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,14 @@ handle(ctx, state, sender, message) -> new_state
|
||||||
on_shutdown(state) -> new_state // optional
|
on_shutdown(state) -> new_state // optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both functions may omit the returned state. A unit result (`()`, `return;`,
|
||||||
|
or falling through without a value) keeps the previously committed state,
|
||||||
|
not the invocation's mutable draft. Non-unit results replace state as before.
|
||||||
|
Successful handlers still acknowledge the reserved message and commit validated
|
||||||
|
context sends atomically. Invalid sends still fail the transition. A unit
|
||||||
|
shutdown result stops the actor without changing its state. Unit cannot reset
|
||||||
|
state to null anymore; host state editing can still set null explicitly.
|
||||||
|
|
||||||
The runtime uses internal envelopes containing sender and data, then passes
|
The runtime uses internal envelopes containing sender and data, then passes
|
||||||
them separately to the handler. Host-originated messages have unit/null sender.
|
them separately to the handler. Host-originated messages have unit/null sender.
|
||||||
Actors choose their own message data. For example:
|
Actors choose their own message data. For example:
|
||||||
|
|
@ -289,7 +297,8 @@ System messages are host-only commands, separate from ordinary data messages:
|
||||||
stop, kill, restart, pause, resume, reload, and become. A record containing
|
stop, kill, restart, pause, resume, reload, and become. A record containing
|
||||||
`kind: "stop"` sent by an actor is just application data.
|
`kind: "stop"` sent by an actor is just application data.
|
||||||
Graceful stop finishes the active handler, then runs optional `on_shutdown(state)`
|
Graceful stop finishes the active handler, then runs optional `on_shutdown(state)`
|
||||||
with cooperative instruction slices and commits its returned state. It does not
|
with cooperative instruction slices and commits a non-unit returned state,
|
||||||
|
or retains committed state for a unit return. It does not
|
||||||
drain queued input. A missing hook stops normally; a failing hook marks the actor
|
drain queued input. A missing hook stops normally; a failing hook marks the actor
|
||||||
failed and retains its last committed state. Stop unpauses a paused actor so
|
failed and retains its last committed state. Stop unpauses a paused actor so
|
||||||
shutdown can progress; explicit scheduler ticks are still needed.
|
shutdown can progress; explicit scheduler ticks are still needed.
|
||||||
|
|
@ -308,6 +317,7 @@ resolves it; there is no automatic poison-message policy or supervision.
|
||||||
| Situation | State | Reserved input | Effects | Status |
|
| Situation | State | Reserved input | Effects | Status |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| Handler returns a valid transition | New state commits | Acknowledged | All validated sends enqueue | `Idle` |
|
| Handler returns a valid transition | New state commits | Acknowledged | All validated sends enqueue | `Idle` |
|
||||||
|
| Handler returns unit / no state | Last committed state retained; draft discarded | Acknowledged | All validated sends enqueue | `Idle` |
|
||||||
| Rune execution or transition decoding fails | Last committed state retained | Retained | None from the returned transition | `Failed` |
|
| 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` |
|
| Any target is unknown, stopped, failed, or over capacity | Last committed state retained | Retained | No sends from the transition enqueue | `Failed` |
|
||||||
| Graceful stop | Active handler then shutdown state commits | Active input acknowledged | Valid active-handler sends commit | `Stopped` after shutdown |
|
| Graceful stop | Active handler then shutdown state commits | Active input acknowledged | Valid active-handler sends commit | `Stopped` after shutdown |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue