Skip to main content

cratefield_testing/
actor.rs

1//! The `Actors` contract (issue #583), asserted against a host rather than
2//! trusted from its docs. A host that returns an `Ok` reply without
3//! serializing its actor's calls, commits a write a failing handler staged,
4//! or fires an alarm early would pass a hand-written spot check and lose a
5//! counter, a cursor or a lease in production.
6//!
7//! [`assert_actor_contract`] drives one host through every promise the port
8//! makes — serialization, all-or-nothing commits, the size bounds, the alarm
9//! lifecycle — using [`CONTRACT_ACTOR_KIND`]'s handler, which the kit ships
10//! alongside it so any implementation can be held to the same standard.
11
12use std::future::Future;
13use std::future::poll_fn;
14use std::sync::Arc;
15use std::task::Poll;
16use std::time::Duration;
17
18use async_trait::async_trait;
19use cratefield_core::{
20    ActorContext, ActorError, ActorHandler, ActorHandlers, Actors, MAX_ACTOR_MESSAGE_BYTES,
21    MAX_ACTOR_REPLY_BYTES, MAX_ACTOR_VALUE_BYTES,
22};
23use futures_util::future::join_all;
24use time::Duration as TimeDuration;
25
26/// The kind [`contract_actor_handler`] answers for and
27/// [`assert_actor_contract`] drives.
28pub const CONTRACT_ACTOR_KIND: &str = "contract";
29
30/// Message opcodes. The first byte of every message selects the behaviour;
31/// a few carry a little-endian `u32` length after it.
32const OP_INCR: u8 = 1;
33const OP_GET: u8 = 2;
34const OP_INCR_THEN_FAIL: u8 = 3;
35const OP_PUT_N: u8 = 4;
36const OP_DATA_LEN: u8 = 5;
37const OP_REPLY_N: u8 = 6;
38const OP_MARKER: u8 = 7;
39const OP_PAD: u8 = 8;
40const OP_ARM: u8 = 9;
41const OP_ALARMS: u8 = 10;
42
43/// Storage keys the contract handler uses. Every count is a decimal string,
44/// so a reply is readable in a failure message.
45const KEY_COUNTER: &str = "counter";
46const KEY_DATA: &str = "data";
47const KEY_ALARMS: &str = "alarms";
48const KEY_MARKER: &str = "marker";
49const KEY_REARM: &str = "rearm";
50const KEY_SECS: &str = "secs";
51
52/// The one handler the contract drives.
53///
54/// It is deliberately stateful-looking while staying stateless itself: every
55/// value lives in the actor and is reached through [`ActorContext`], so a
56/// host that drops the handler between events (a hibernating Durable Object)
57/// still passes.
58struct ContractActor;
59
60#[async_trait]
61impl ActorHandler for ContractActor {
62    async fn on_message(
63        &self,
64        ctx: &mut dyn ActorContext,
65        message: &[u8],
66    ) -> Result<Vec<u8>, ActorError> {
67        let Some((&op, rest)) = message.split_first() else {
68            return Err(ActorError::Handler("contract: empty message".to_owned()));
69        };
70        match op {
71            OP_INCR => {
72                let next = stored_count(ctx, KEY_COUNTER).await? + 1;
73                // Yield once before committing: a host that does not
74                // serialize calls to one actor lets a second call read the
75                // same value here, and the lost update is what the contract
76                // looks for.
77                yield_once().await;
78                ctx.put(KEY_COUNTER, next.to_string().into_bytes()).await?;
79                Ok(next.to_string().into_bytes())
80            }
81            OP_GET => Ok(stored_count(ctx, KEY_COUNTER)
82                .await?
83                .to_string()
84                .into_bytes()),
85            OP_INCR_THEN_FAIL => {
86                let next = stored_count(ctx, KEY_COUNTER).await? + 1;
87                ctx.put(KEY_COUNTER, next.to_string().into_bytes()).await?;
88                Err(ActorError::Handler("contract: refused".to_owned()))
89            }
90            OP_PUT_N => {
91                let n = read_size(rest)?;
92                ctx.put(KEY_DATA, vec![0_u8; n]).await?;
93                Ok(Vec::new())
94            }
95            OP_DATA_LEN => {
96                let len = ctx.get(KEY_DATA).await?.map(|value| value.len());
97                Ok(match len {
98                    Some(len) => len.to_string().into_bytes(),
99                    None => b"absent".to_vec(),
100                })
101            }
102            OP_REPLY_N => {
103                let n = read_size(rest)?;
104                // Stage a marker before the oversized reply: a host that
105                // committed it would prove it never checked the bound.
106                ctx.put(KEY_MARKER, b"1".to_vec()).await?;
107                Ok(vec![0_u8; n])
108            }
109            OP_MARKER => Ok(if ctx.get(KEY_MARKER).await?.is_some() {
110                b"yes".to_vec()
111            } else {
112                b"no".to_vec()
113            }),
114            OP_PAD => Ok(b"ok".to_vec()),
115            OP_ARM => {
116                let raw = rest
117                    .get(..4)
118                    .ok_or_else(|| ActorError::Handler("contract: arm needs seconds".to_owned()))?;
119                let rearm = rest.get(4).copied().unwrap_or(0) != 0;
120                ctx.put(KEY_DATA, b"present".to_vec()).await?;
121                ctx.put(KEY_SECS, raw.to_vec()).await?;
122                if rearm {
123                    ctx.put(KEY_REARM, b"1".to_vec()).await?;
124                } else {
125                    ctx.delete(KEY_REARM).await?;
126                }
127                let secs = i64::from(u32::from_le_bytes(
128                    raw.try_into().expect("exactly four bytes"),
129                ));
130                ctx.set_alarm(ctx.now() + TimeDuration::seconds(secs))
131                    .await?;
132                Ok(Vec::new())
133            }
134            OP_ALARMS => Ok(stored_count(ctx, KEY_ALARMS)
135                .await?
136                .to_string()
137                .into_bytes()),
138            other => Err(ActorError::Handler(format!(
139                "contract: unknown opcode {other}"
140            ))),
141        }
142    }
143
144    async fn on_alarm(&self, ctx: &mut dyn ActorContext) -> Result<(), ActorError> {
145        let alarms = stored_count(ctx, KEY_ALARMS).await? + 1;
146        ctx.put(KEY_ALARMS, alarms.to_string().into_bytes()).await?;
147        if ctx.get(KEY_REARM).await?.is_some() {
148            // Re-arm once: consume the flag and schedule the next firing.
149            ctx.delete(KEY_REARM).await?;
150            let secs = stored_secs(ctx).await?;
151            let at = ctx.now() + TimeDuration::seconds(secs);
152            ctx.set_alarm(at).await?;
153        } else {
154            // No re-arm: the alarm is spent and the actor's data is dropped.
155            ctx.delete(KEY_DATA).await?;
156        }
157        Ok(())
158    }
159}
160
161/// The contract handler, for a host that takes one [`Arc`].
162#[must_use]
163pub fn contract_actor_handler() -> Arc<dyn ActorHandler> {
164    Arc::new(ContractActor)
165}
166
167/// A registry holding just the contract handler under
168/// [`CONTRACT_ACTOR_KIND`], for a host built from [`ActorHandlers`].
169#[must_use]
170pub fn contract_actor_handlers() -> ActorHandlers {
171    ActorHandlers::new().with(CONTRACT_ACTOR_KIND, contract_actor_handler())
172}
173
174/// Proves `actors` keeps the `Actors` contract, using `advance` to move the
175/// host's clock forward by a [`Duration`] so a pending alarm fires — an
176/// in-process host advances its own clock, an adapter's test drives
177/// whatever its runtime exposes.
178///
179/// It runs its own keys under [`CONTRACT_ACTOR_KIND`], so it can be called
180/// against any host with the contract handler registered.
181///
182/// # Panics
183///
184/// Panics when the contract is violated: concurrent calls to one actor lose
185/// an update, a failing handler commits a write, a message, reply or value
186/// over its bound is accepted (or one at the bound is refused), an alarm
187/// fires early or not at all, or an unknown kind is not refused.
188pub async fn assert_actor_contract<F, Fut>(actors: Arc<dyn Actors>, advance: F)
189where
190    F: Fn(Duration) -> Fut,
191    Fut: Future<Output = ()>,
192{
193    let actors = actors.as_ref();
194    check_serialization(actors).await;
195    check_atomic_commit(actors).await;
196    check_size_bounds(actors).await;
197    check_alarm(actors, &advance).await;
198
199    // (f) A kind the host has no handler for is refused, not guessed at.
200    let err = actors.call("no-such-kind", "key", b"x").await.unwrap_err();
201    assert!(
202        matches!(err, ActorError::NotConfigured),
203        "an unknown kind is NotConfigured: {err}"
204    );
205}
206
207/// (a) and (b): calls to one actor run one at a time, and two actors do not
208/// share a fate.
209async fn check_serialization(actors: &dyn Actors) {
210    // One hundred concurrent calls each see a distinct value and the counter
211    // settles at exactly 100 — the serialization promise, read through the
212    // increment's yield to the executor.
213    let calls = (0..100).map(|_| actors.call(CONTRACT_ACTOR_KIND, "concurrent", &[OP_INCR]));
214    let replies = join_all(calls).await;
215    let mut values: Vec<usize> = replies
216        .into_iter()
217        .map(|reply| parse_decimal(&reply.expect("each concurrent increment succeeds")))
218        .collect();
219    values.sort_unstable();
220    assert_eq!(
221        values,
222        (1..=100).collect::<Vec<usize>>(),
223        "100 serialized increments must each see a distinct value"
224    );
225    assert_eq!(
226        count(actors, "concurrent").await,
227        100,
228        "the counter settles at 100, no update lost"
229    );
230
231    // Two actors progress independently: incrementing one leaves the other
232    // where it was.
233    assert_eq!(increment(actors, "independent-a").await, 1);
234    assert_eq!(increment(actors, "independent-b").await, 1);
235    assert_eq!(increment(actors, "independent-a").await, 2);
236    assert_eq!(count(actors, "independent-a").await, 2);
237    assert_eq!(count(actors, "independent-b").await, 1);
238}
239
240/// (c): a handler that stages a write and then fails commits nothing.
241async fn check_atomic_commit(actors: &dyn Actors) {
242    assert_eq!(increment(actors, "failing").await, 1);
243    let err = actors
244        .call(CONTRACT_ACTOR_KIND, "failing", &[OP_INCR_THEN_FAIL])
245        .await
246        .unwrap_err();
247    assert!(
248        matches!(err, ActorError::Handler(_)),
249        "a handler error surfaces as Handler: {err}"
250    );
251    assert_eq!(
252        count(actors, "failing").await,
253        1,
254        "the staged write of a failing handler never lands"
255    );
256}
257
258/// (d): the three size bounds, each at the limit and one byte over.
259async fn check_size_bounds(actors: &dyn Actors) {
260    let at_bound = vec![OP_PAD; MAX_ACTOR_MESSAGE_BYTES];
261    call_ok(actors, "message-max", &at_bound).await;
262    let over = vec![OP_PAD; MAX_ACTOR_MESSAGE_BYTES + 1];
263    let err = actors
264        .call(CONTRACT_ACTOR_KIND, "message-max", &over)
265        .await
266        .unwrap_err();
267    assert!(
268        matches!(err, ActorError::TooLarge(_)),
269        "a message one byte over the bound is refused: {err}"
270    );
271
272    let reply = call_ok(
273        actors,
274        "reply-max",
275        &sized_message(OP_REPLY_N, MAX_ACTOR_REPLY_BYTES),
276    )
277    .await;
278    assert_eq!(
279        reply.len(),
280        MAX_ACTOR_REPLY_BYTES,
281        "a reply at the bound is accepted"
282    );
283    assert!(
284        marker_present(actors, "reply-max").await,
285        "the accepted reply's marker write committed"
286    );
287    let err = actors
288        .call(
289            CONTRACT_ACTOR_KIND,
290            "reply-over",
291            &sized_message(OP_REPLY_N, MAX_ACTOR_REPLY_BYTES + 1),
292        )
293        .await
294        .unwrap_err();
295    assert!(
296        matches!(err, ActorError::TooLarge(_)),
297        "a reply one byte over the bound is refused: {err}"
298    );
299    assert!(
300        !marker_present(actors, "reply-over").await,
301        "an oversized reply commits nothing"
302    );
303
304    call_ok(
305        actors,
306        "value-max",
307        &sized_message(OP_PUT_N, MAX_ACTOR_VALUE_BYTES),
308    )
309    .await;
310    assert_eq!(
311        data_len(actors, "value-max").await,
312        Some(MAX_ACTOR_VALUE_BYTES),
313        "a value at the bound is stored"
314    );
315    let err = actors
316        .call(
317            CONTRACT_ACTOR_KIND,
318            "value-over",
319            &sized_message(OP_PUT_N, MAX_ACTOR_VALUE_BYTES + 1),
320        )
321        .await
322        .unwrap_err();
323    assert!(
324        matches!(err, ActorError::TooLarge(_)),
325        "a value one byte over the bound is refused: {err}"
326    );
327    assert_eq!(
328        data_len(actors, "value-over").await,
329        None,
330        "an oversized value stores nothing"
331    );
332}
333
334/// (e): an alarm fires only once the clock reaches it, re-arms once, fires
335/// again, and then the actor's data is gone.
336async fn check_alarm<F, Fut>(actors: &dyn Actors, advance: &F)
337where
338    F: Fn(Duration) -> Fut,
339    Fut: Future<Output = ()>,
340{
341    call_ok(actors, "alarm", &arm_message(10, true)).await;
342    assert_eq!(
343        data_len(actors, "alarm").await,
344        Some(b"present".len()),
345        "arming leaves the actor's data in place"
346    );
347    advance(Duration::from_secs(5)).await;
348    assert_eq!(
349        alarm_count(actors, "alarm").await,
350        0,
351        "the alarm does not fire before its time"
352    );
353    advance(Duration::from_secs(5)).await;
354    assert_eq!(
355        alarm_count(actors, "alarm").await,
356        1,
357        "the alarm fires at its time"
358    );
359    advance(Duration::from_secs(10)).await;
360    assert_eq!(
361        alarm_count(actors, "alarm").await,
362        2,
363        "the alarm re-armed once and fired again"
364    );
365    assert_eq!(
366        data_len(actors, "alarm").await,
367        None,
368        "a spent alarm deletes the actor's data"
369    );
370}
371
372/// A call that is expected to succeed.
373async fn call_ok(actors: &dyn Actors, key: &str, message: &[u8]) -> Vec<u8> {
374    actors
375        .call(CONTRACT_ACTOR_KIND, key, message)
376        .await
377        .expect("the contract call succeeds")
378}
379
380/// The value at `key`'s counter as a message reply.
381async fn count(actors: &dyn Actors, key: &str) -> usize {
382    parse_decimal(&call_ok(actors, key, &[OP_GET]).await)
383}
384
385/// Increments `key`'s counter and returns the reply.
386async fn increment(actors: &dyn Actors, key: &str) -> usize {
387    parse_decimal(&call_ok(actors, key, &[OP_INCR]).await)
388}
389
390/// How many times `key`'s alarm has fired.
391async fn alarm_count(actors: &dyn Actors, key: &str) -> usize {
392    parse_decimal(&call_ok(actors, key, &[OP_ALARMS]).await)
393}
394
395/// The length of `key`'s `data` value, or `None` when it is absent.
396async fn data_len(actors: &dyn Actors, key: &str) -> Option<usize> {
397    parse_len(&call_ok(actors, key, &[OP_DATA_LEN]).await)
398}
399
400/// Whether `key`'s marker write is present.
401async fn marker_present(actors: &dyn Actors, key: &str) -> bool {
402    call_ok(actors, key, &[OP_MARKER]).await.as_slice() == b"yes"
403}
404
405/// An opcode followed by a little-endian `u32` count.
406fn sized_message(op: u8, n: usize) -> Vec<u8> {
407    let mut message = vec![op];
408    let n = u32::try_from(n).expect("a bounded size fits u32");
409    message.extend_from_slice(&n.to_le_bytes());
410    message
411}
412
413/// [`OP_ARM`] with a delay in seconds and whether the handler re-arms once.
414fn arm_message(secs: u32, rearm: bool) -> Vec<u8> {
415    let mut message = vec![OP_ARM];
416    message.extend_from_slice(&secs.to_le_bytes());
417    message.push(u8::from(rearm));
418    message
419}
420
421/// The `u32` after an opcode.
422fn read_size(rest: &[u8]) -> Result<usize, ActorError> {
423    let raw: [u8; 4] = rest
424        .get(..4)
425        .and_then(|bytes| bytes.try_into().ok())
426        .ok_or_else(|| ActorError::Handler("contract: message too short".to_owned()))?;
427    Ok(usize::try_from(u32::from_le_bytes(raw)).expect("a u32 fits usize"))
428}
429
430/// An ASCII decimal count, or zero when the value is absent or malformed.
431fn parse_decimal(bytes: &[u8]) -> usize {
432    std::str::from_utf8(bytes)
433        .ok()
434        .and_then(|text| text.parse().ok())
435        .unwrap_or(0)
436}
437
438/// A decimal length reply, or `None` for the `absent` sentinel.
439fn parse_len(bytes: &[u8]) -> Option<usize> {
440    std::str::from_utf8(bytes).ok()?.parse().ok()
441}
442
443/// The counter stored at `key` inside the handler, zero when absent.
444async fn stored_count(ctx: &mut dyn ActorContext, key: &str) -> Result<usize, ActorError> {
445    Ok(parse_decimal(&ctx.get(key).await?.unwrap_or_default()))
446}
447
448/// The seconds the arming message stored, for a re-arm; zero if unreadable.
449async fn stored_secs(ctx: &mut dyn ActorContext) -> Result<i64, ActorError> {
450    let Some(raw) = ctx.get(KEY_SECS).await? else {
451        return Ok(0);
452    };
453    Ok(match <[u8; 4]>::try_from(raw) {
454        Ok(bytes) => i64::from(u32::from_le_bytes(bytes)),
455        Err(_) => 0,
456    })
457}
458
459/// Yields to the executor once, so a host that does not serialize calls to
460/// one actor interleaves two handlers here and the lost update the contract
461/// looks for becomes visible.
462async fn yield_once() {
463    let mut yielded = false;
464    poll_fn(move |cx| {
465        if yielded {
466            Poll::Ready(())
467        } else {
468            yielded = true;
469            cx.waker().wake_by_ref();
470            Poll::Pending
471        }
472    })
473    .await;
474}