rsactor 0.17.0

A Simple and Efficient In-Process Actor Model Implementation for Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
// Copyright 2022 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0

//! Deadlock Detection Tests
//!
//! Tests for runtime deadlock detection via wait-for graph cycle analysis.
//! Requires the `deadlock-detection` feature to be enabled.

use rsactor::{spawn, Actor, ActorRef, Message};

// ============================================================
// Shared utility actor — handles Ping, ForwardPing, SequentialPing
// ============================================================

#[derive(Debug)]
struct WorkerActor;

#[derive(Debug)]
struct Ping;

// Contains ActorRef, so Debug cannot be derived automatically.
struct ForwardPing(ActorRef<WorkerActor>);
struct SequentialPing(ActorRef<WorkerActor>, ActorRef<WorkerActor>);

impl Actor for WorkerActor {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(WorkerActor)
    }
}

impl Message<Ping> for WorkerActor {
    type Reply = String;

    async fn handle(&mut self, _: Ping, _: &ActorRef<Self>) -> String {
        "pong".to_string()
    }
}

impl Message<ForwardPing> for WorkerActor {
    type Reply = String;

    async fn handle(&mut self, msg: ForwardPing, _: &ActorRef<Self>) -> String {
        msg.0.ask(Ping).await.unwrap()
    }
}

impl Message<SequentialPing> for WorkerActor {
    type Reply = (String, String);

    async fn handle(&mut self, msg: SequentialPing, _: &ActorRef<Self>) -> (String, String) {
        let r1 = msg.0.ask(Ping).await.unwrap();
        let r2 = msg.1.ask(Ping).await.unwrap();
        (r1, r2)
    }
}

// ============================================================
// Self-ask deadlock
// ============================================================

#[derive(Debug)]
struct SelfAskActor;

#[derive(Debug)]
struct TriggerSelfAsk;

impl Actor for SelfAskActor {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(SelfAskActor)
    }
}

impl Message<TriggerSelfAsk> for SelfAskActor {
    type Reply = ();

    async fn handle(&mut self, _: TriggerSelfAsk, actor_ref: &ActorRef<Self>) {
        let _ = actor_ref.ask(TriggerSelfAsk).await;
    }
}

#[tokio::test]
async fn test_self_ask_deadlock_detected() {
    let (actor_ref, join_handle) = spawn::<SelfAskActor>(());

    // The handler calls self.ask() → deadlock detection panics inside the actor task.
    // The reply channel is dropped → ask returns Err.
    let _ = actor_ref.ask(TriggerSelfAsk).await;

    let join_err = join_handle.await.unwrap_err();
    assert!(join_err.is_panic());
    let payload = join_err.into_panic();
    let msg = payload
        .downcast_ref::<String>()
        .expect("panic payload should be String");
    assert!(
        msg.contains("Deadlock detected"),
        "Expected deadlock panic, got: {msg}"
    );
}

// ============================================================
// 2-actor cycle: A → B → A
// ============================================================

#[derive(Debug)]
struct CycleActorA;
#[derive(Debug)]
struct CycleActorB;

struct StartCycle(ActorRef<CycleActorB>);

struct PingFromA(ActorRef<CycleActorA>);

#[derive(Debug)]
struct CyclePong;

impl Actor for CycleActorA {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(CycleActorA)
    }
}

impl Actor for CycleActorB {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(CycleActorB)
    }
}

impl Message<StartCycle> for CycleActorA {
    type Reply = String;

    async fn handle(&mut self, msg: StartCycle, actor_ref: &ActorRef<Self>) -> String {
        // Ask B, passing our ref so B can ask us back
        msg.0
            .ask(PingFromA(actor_ref.clone()))
            .await
            .unwrap_or_default()
    }
}

impl Message<CyclePong> for CycleActorA {
    type Reply = String;

    async fn handle(&mut self, _: CyclePong, _: &ActorRef<Self>) -> String {
        "pong from A".to_string()
    }
}

impl Message<PingFromA> for CycleActorB {
    type Reply = String;

    async fn handle(&mut self, msg: PingFromA, _: &ActorRef<Self>) -> String {
        // Ask A back → deadlock detected (A→B already in graph)
        msg.0.ask(CyclePong).await.unwrap_or_default()
    }
}

#[tokio::test]
async fn test_two_actor_cycle_deadlock_detected() {
    let (a_ref, a_handle) = spawn::<CycleActorA>(());
    let (b_ref, b_handle) = spawn::<CycleActorB>(());

    // A asks B, B asks A back → deadlock detected in B's task
    let _ = a_ref.ask(StartCycle(b_ref.clone())).await;

    let b_err = b_handle.await.unwrap_err();
    assert!(b_err.is_panic());
    let payload = b_err.into_panic();
    let msg = payload
        .downcast_ref::<String>()
        .expect("panic payload should be String");
    assert!(
        msg.contains("Deadlock detected"),
        "Expected deadlock panic, got: {msg}"
    );

    drop(a_ref);
    drop(b_ref);
    let _ = a_handle.await;
}

// ============================================================
// 3-actor chain: A → B → C → A
// ============================================================

#[derive(Debug)]
struct ChainActorA;
#[derive(Debug)]
struct ChainActorB;
#[derive(Debug)]
struct ChainActorC;

struct StartChain {
    b_ref: ActorRef<ChainActorB>,
    c_ref: ActorRef<ChainActorC>,
}

struct ForwardToC {
    a_ref: ActorRef<ChainActorA>,
    c_ref: ActorRef<ChainActorC>,
}

struct AskBackToA(ActorRef<ChainActorA>);

#[derive(Debug)]
struct ChainPong;

impl Actor for ChainActorA {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(ChainActorA)
    }
}

impl Actor for ChainActorB {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(ChainActorB)
    }
}

impl Actor for ChainActorC {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(ChainActorC)
    }
}

impl Message<StartChain> for ChainActorA {
    type Reply = String;

    async fn handle(&mut self, msg: StartChain, actor_ref: &ActorRef<Self>) -> String {
        msg.b_ref
            .ask(ForwardToC {
                a_ref: actor_ref.clone(),
                c_ref: msg.c_ref,
            })
            .await
            .unwrap_or_default()
    }
}

impl Message<ChainPong> for ChainActorA {
    type Reply = String;

    async fn handle(&mut self, _: ChainPong, _: &ActorRef<Self>) -> String {
        "chain pong".to_string()
    }
}

impl Message<ForwardToC> for ChainActorB {
    type Reply = String;

    async fn handle(&mut self, msg: ForwardToC, _: &ActorRef<Self>) -> String {
        msg.c_ref
            .ask(AskBackToA(msg.a_ref))
            .await
            .unwrap_or_default()
    }
}

impl Message<AskBackToA> for ChainActorC {
    type Reply = String;

    async fn handle(&mut self, msg: AskBackToA, _: &ActorRef<Self>) -> String {
        // Ask A → deadlock detected (A→B→C→A cycle)
        msg.0.ask(ChainPong).await.unwrap_or_default()
    }
}

#[tokio::test]
async fn test_three_actor_chain_deadlock_detected() {
    let (a_ref, a_handle) = spawn::<ChainActorA>(());
    let (b_ref, b_handle) = spawn::<ChainActorB>(());
    let (c_ref, c_handle) = spawn::<ChainActorC>(());

    let _ = a_ref
        .ask(StartChain {
            b_ref: b_ref.clone(),
            c_ref: c_ref.clone(),
        })
        .await;

    // C should have panicked (A→B→C→A cycle)
    let c_err = c_handle.await.unwrap_err();
    assert!(c_err.is_panic());
    let payload = c_err.into_panic();
    let msg = payload
        .downcast_ref::<String>()
        .expect("panic payload should be String");
    assert!(
        msg.contains("Deadlock detected"),
        "Expected deadlock panic, got: {msg}"
    );

    drop(a_ref);
    drop(b_ref);
    drop(c_ref);
    let _ = a_handle.await;
    let _ = b_handle.await;
}

// ============================================================
// Non-cycle: A → B (no cycle, should succeed)
// ============================================================

#[tokio::test]
async fn test_non_cycle_ask_succeeds() {
    let (a_ref, a_handle) = spawn::<WorkerActor>(());
    let (b_ref, b_handle) = spawn::<WorkerActor>(());

    // A asks B via ForwardPing — no cycle, B just replies with Ping
    let result = a_ref.ask(ForwardPing(b_ref.clone())).await;
    assert_eq!(result.unwrap(), "pong");

    drop(a_ref);
    drop(b_ref);
    let _ = a_handle.await;
    let _ = b_handle.await;
}

// ============================================================
// Sequential ask: A → B then A → C (guard cleanup between asks)
// ============================================================

#[tokio::test]
async fn test_sequential_ask_no_false_positive() {
    let (a_ref, a_handle) = spawn::<WorkerActor>(());
    let (b_ref, b_handle) = spawn::<WorkerActor>(());
    let (c_ref, c_handle) = spawn::<WorkerActor>(());

    // A asks B, then asks C sequentially — guards drop between asks
    let result = a_ref
        .ask(SequentialPing(b_ref.clone(), c_ref.clone()))
        .await;
    let (r1, r2) = result.unwrap();
    assert_eq!(r1, "pong");
    assert_eq!(r2, "pong");

    drop(a_ref);
    drop(b_ref);
    drop(c_ref);
    let _ = a_handle.await;
    let _ = b_handle.await;
    let _ = c_handle.await;
}

// ============================================================
// Reply-time edge removal: no false positive in the window
// between B sending its reply and A's ask future resuming
// ============================================================

#[derive(Debug)]
struct FpA;
#[derive(Debug)]
struct FpB;

#[derive(Debug)]
struct FpPing;
struct FpTrigger(ActorRef<FpB>);
struct FpMsg1(ActorRef<FpA>);
struct FpMsg2(ActorRef<FpA>);

impl Actor for FpA {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(FpA)
    }
}

impl Actor for FpB {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(FpB)
    }
}

impl Message<FpPing> for FpA {
    type Reply = u32;

    async fn handle(&mut self, _: FpPing, _: &ActorRef<Self>) -> u32 {
        7
    }
}

impl Message<FpTrigger> for FpA {
    type Reply = u32;

    async fn handle(&mut self, msg: FpTrigger, actor_ref: &ActorRef<Self>) -> u32 {
        // A asks B from inside a handler → edge A->B registered.
        msg.0.ask(FpMsg1(actor_ref.clone())).await.unwrap()
    }
}

impl Message<FpMsg1> for FpB {
    type Reply = u32;

    async fn handle(&mut self, msg: FpMsg1, actor_ref: &ActorRef<Self>) -> u32 {
        // Queue FpMsg2 before replying; the reply to A is sent when this
        // handler returns, and B's very next message asks back into A.
        actor_ref.tell(FpMsg2(msg.0)).await.unwrap();
        1
    }
}

impl Message<FpMsg2> for FpB {
    type Reply = u32;

    async fn handle(&mut self, msg: FpMsg2, _: &ActorRef<Self>) -> u32 {
        // Regression: with caller-side-only edge removal, the stale A->B edge
        // was still in the wait-for graph here (A's ask future had not been
        // polled yet), so this ask closed a phantom cycle and panicked even
        // though A was already runnable. Reply-time edge removal (the token
        // carried in the ask envelope) must make this succeed.
        msg.0.ask(FpPing).await.unwrap()
    }
}

#[tokio::test]
async fn test_reply_send_window_has_no_false_positive() {
    let (a_ref, a_handle) = spawn::<FpA>(());
    let (b_ref, b_handle) = spawn::<FpB>(());

    let r = tokio::time::timeout(
        std::time::Duration::from_secs(2),
        a_ref.ask(FpTrigger(b_ref.clone())),
    )
    .await
    .expect("must not hang")
    .expect("must not error");
    assert_eq!(r, 1);

    // Let B process FpMsg2 — its ask back into A must not panic.
    tokio::time::sleep(std::time::Duration::from_millis(200)).await;
    assert!(
        b_ref.is_alive(),
        "B must survive: the ask issued right after replying to A must not \
         trip a false-positive deadlock panic"
    );

    drop(a_ref);
    drop(b_ref);
    let _ = a_handle.await;
    let _ = b_handle.await;
}

// ============================================================
// Slow-path detection: blocking_ask from a current_thread
// runtime handler still panics immediately on a self-cycle
// ============================================================

#[derive(Debug)]
struct SlowPathActor;

#[derive(Debug)]
struct PlainPing;
#[derive(Debug)]
struct BlockingSelfAsk;

impl Actor for SlowPathActor {
    type Args = ();
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(_: (), _: &ActorRef<Self>) -> Result<Self, Self::Error> {
        Ok(SlowPathActor)
    }
}

impl Message<PlainPing> for SlowPathActor {
    type Reply = u32;

    async fn handle(&mut self, _: PlainPing, _: &ActorRef<Self>) -> u32 {
        1
    }
}

impl Message<BlockingSelfAsk> for SlowPathActor {
    type Reply = u32;

    async fn handle(&mut self, _: BlockingSelfAsk, actor_ref: &ActorRef<Self>) -> u32 {
        // On a current_thread runtime the blocking_* call takes the
        // dedicated-thread slow path. Regression: the CURRENT_ACTOR
        // task-local did not propagate to that thread, so this self-cycle
        // silently hung until the timeout instead of panicking.
        actor_ref
            .blocking_ask(PlainPing, Some(std::time::Duration::from_secs(5)))
            .unwrap_or(0)
    }
}

#[tokio::test]
async fn test_blocking_slow_path_self_cycle_panics_immediately() {
    let (actor_ref, handle) = spawn::<SlowPathActor>(());

    let start = std::time::Instant::now();
    let _ = tokio::time::timeout(
        std::time::Duration::from_secs(3),
        actor_ref.ask(BlockingSelfAsk),
    )
    .await;

    let res = handle.await;
    assert!(
        res.is_err() && res.unwrap_err().is_panic(),
        "self ask cycle through the blocking slow path must panic via deadlock detection"
    );
    assert!(
        start.elapsed() < std::time::Duration::from_secs(4),
        "detection must be immediate, not the 5s blocking timeout"
    );
}

// ============================================================
// Self-send on a full mailbox: unbounded waits panic, bounded
// waits stay recoverable
// ============================================================

#[derive(Debug)]
struct SelfSendActor {
    entered: std::sync::Arc<tokio::sync::Notify>,
    proceed: std::sync::Arc<tokio::sync::Notify>,
}

#[derive(Debug)]
struct Padding;
#[derive(Debug)]
struct TriggerSelfTell;
#[derive(Debug)]
struct TriggerSelfStop;
#[derive(Debug)]
struct TriggerTimedSelfTell;

impl Actor for SelfSendActor {
    type Args = (
        std::sync::Arc<tokio::sync::Notify>,
        std::sync::Arc<tokio::sync::Notify>,
    );
    type Error = anyhow::Error;
    type IdleEvent = ();

    async fn on_start(
        (entered, proceed): Self::Args,
        _: &ActorRef<Self>,
    ) -> Result<Self, Self::Error> {
        Ok(SelfSendActor { entered, proceed })
    }
}

impl Message<Padding> for SelfSendActor {
    type Reply = ();

    async fn handle(&mut self, _: Padding, _: &ActorRef<Self>) {}
}

impl Message<TriggerSelfTell> for SelfSendActor {
    type Reply = ();

    async fn handle(&mut self, _: TriggerSelfTell, actor_ref: &ActorRef<Self>) {
        self.entered.notify_one();
        self.proceed.notified().await;
        // The test has filled the capacity-1 mailbox: this unbounded self-send
        // can never be admitted (the loop is parked awaiting this handler) and
        // must panic under deadlock-detection instead of hanging.
        let _ = actor_ref.tell(Padding).await;
    }
}

impl Message<TriggerSelfStop> for SelfSendActor {
    type Reply = ();

    async fn handle(&mut self, _: TriggerSelfStop, actor_ref: &ActorRef<Self>) {
        self.entered.notify_one();
        self.proceed.notified().await;
        // Same hazard through stop(): StopGracefully travels the regular
        // mailbox, so a full-mailbox self-stop is equally unrecoverable.
        actor_ref.stop().await;
    }
}

impl Message<TriggerTimedSelfTell> for SelfSendActor {
    type Reply = bool;

    async fn handle(&mut self, _: TriggerTimedSelfTell, actor_ref: &ActorRef<Self>) -> bool {
        self.entered.notify_one();
        self.proceed.notified().await;
        // Bounded variant: must NOT panic — it resolves as a recoverable
        // Error::Timeout once the 50ms admission window elapses.
        actor_ref
            .tell_with_timeout(Padding, std::time::Duration::from_millis(50))
            .await
            .is_err()
    }
}

async fn run_self_send_panic_case<M>(msg: M) -> Result<(), tokio::task::JoinError>
where
    SelfSendActor: Message<M>,
    M: Send + 'static,
{
    let entered = std::sync::Arc::new(tokio::sync::Notify::new());
    let proceed = std::sync::Arc::new(tokio::sync::Notify::new());
    let (actor_ref, handle) = rsactor::spawn_with_mailbox_capacity::<SelfSendActor>(
        (entered.clone(), proceed.clone()),
        1,
    );

    actor_ref.tell(msg).await.unwrap();
    entered.notified().await;
    // Fill the capacity-1 mailbox while the handler is parked.
    actor_ref.tell::<Padding>(Padding).await.unwrap();
    proceed.notify_one();

    handle.await.map(|_| ())
}

#[tokio::test]
async fn test_self_tell_on_full_mailbox_panics() {
    let res = run_self_send_panic_case(TriggerSelfTell).await;
    assert!(
        res.is_err() && res.unwrap_err().is_panic(),
        "unbounded self-tell on a full mailbox must panic under deadlock-detection"
    );
}

#[tokio::test]
async fn test_self_stop_on_full_mailbox_panics() {
    let res = run_self_send_panic_case(TriggerSelfStop).await;
    assert!(
        res.is_err() && res.unwrap_err().is_panic(),
        "self-stop on a full mailbox must panic under deadlock-detection"
    );
}

#[tokio::test]
async fn test_timed_self_tell_on_full_mailbox_times_out_without_panic() {
    let entered = std::sync::Arc::new(tokio::sync::Notify::new());
    let proceed = std::sync::Arc::new(tokio::sync::Notify::new());
    let (actor_ref, handle) = rsactor::spawn_with_mailbox_capacity::<SelfSendActor>(
        (entered.clone(), proceed.clone()),
        1,
    );

    let asker = actor_ref.clone();
    let ask_task = tokio::spawn(async move { asker.ask(TriggerTimedSelfTell).await });

    entered.notified().await;
    actor_ref.tell(Padding).await.unwrap();
    proceed.notify_one();

    let timed_out = ask_task
        .await
        .expect("ask task must not panic")
        .expect("ask must succeed");
    assert!(
        timed_out,
        "bounded self-tell must resolve as a recoverable timeout, not a panic"
    );

    actor_ref.stop().await;
    let result = handle.await.expect("actor task must not panic");
    assert!(result.is_completed());
}