Skip to content

Commit 017fc0c

Browse files
committed
Add Ticket and the transit race-test catalog
Ticket pairs a seat's Pass with its result receiver and is the only type aware of T, keeping Ride and Logue payload-agnostic. The catalog covers the seat lifecycle and both signals under real cross-thread concurrency, repeated to surface interleavings.
1 parent 2099806 commit 017fc0c

1 file changed

Lines changed: 223 additions & 2 deletions

File tree

src/transit.rs

Lines changed: 223 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use std::collections::{BTreeMap, HashMap};
2+
use std::future::Future;
23
use std::hash::Hash;
4+
use std::pin::Pin;
35
use std::sync::{Arc, Mutex, MutexGuard, Weak};
6+
use std::task::{Context, Poll};
47

5-
use tokio::sync::Notify;
8+
use tokio::sync::{Notify, oneshot};
69

710
// Mutex guard that recovers a poisoned lock via into_inner.
811
// A panic under the lock must not brick the ride.
@@ -152,6 +155,10 @@ impl<P: Send + 'static> Ride<P> {
152155
pub fn board(&self, cargo: P) -> Pass {
153156
let pass = {
154157
let mut state = locked(&self.inner.state);
158+
debug_assert!(
159+
!(state.abandoned || state.departed || state.drained),
160+
"board on a terminal ride; join through Logue instead"
161+
);
155162
self.seat(&mut state, cargo)
156163
};
157164
self.inner.boarded.notify_waiters();
@@ -306,7 +313,7 @@ impl<K, P> Logue<K, P> {
306313
}
307314
}
308315

309-
impl<K: Hash + Eq + Clone, P: Send + 'static> Logue<K, P> {
316+
impl<K: Hash + Eq, P: Send + 'static> Logue<K, P> {
310317
// Join the key's live ride, or found a new one.
311318
// An abandoned ride under the key is not joined: it is replaced by the freshly founded one.
312319
pub fn book(&self, key: K, cargo: P) -> Booking<P> {
@@ -339,9 +346,35 @@ impl<K: Hash + Eq + Clone, P: Send + 'static> Logue<K, P> {
339346
}
340347
}
341348

349+
// A caller-side handle: the seat's Pass plus the receiver for its result.
350+
// The only transit type that knows the result type T.
351+
#[must_use = "await the Ticket for the result; dropping it cancels the request"]
352+
pub struct Ticket<T> {
353+
// Held only for its Drop: keeps the seat aboard, leaves it if the Ticket drops.
354+
_pass: Pass,
355+
rx: oneshot::Receiver<T>,
356+
}
357+
358+
impl<T> Ticket<T> {
359+
pub fn new(pass: Pass, rx: oneshot::Receiver<T>) -> Self {
360+
Ticket { _pass: pass, rx }
361+
}
362+
}
363+
364+
impl<T> Future for Ticket<T> {
365+
type Output = Result<T, oneshot::error::RecvError>;
366+
367+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
368+
Pin::new(&mut self.get_mut().rx).poll(cx)
369+
}
370+
}
371+
342372
#[cfg(test)]
343373
mod tests {
344374
use super::*;
375+
use std::sync::Barrier;
376+
use std::thread;
377+
use std::time::Duration;
345378

346379
#[test]
347380
fn boarding_accumulates_seats() {
@@ -473,4 +506,192 @@ mod tests {
473506
let second = logue.book(1, 101);
474507
assert!(matches!(&second, Booking::Founded(..)));
475508
}
509+
510+
#[tokio::test]
511+
async fn a_delivered_ticket_resolves_to_the_value() {
512+
let ride: Ride<(u32, oneshot::Sender<u32>)> = Ride::new();
513+
let (tx, rx) = oneshot::channel();
514+
let ticket = Ticket::new(ride.board((1, tx)), rx);
515+
for (input, tx) in ride.into_iter() {
516+
tx.send(input * 10).expect("receiver is alive");
517+
}
518+
assert_eq!(ticket.await, Ok(10));
519+
}
520+
521+
#[test]
522+
fn dropping_a_ticket_leaves_the_ride() {
523+
let ride: Ride<(u32, oneshot::Sender<u32>)> = Ride::new();
524+
let (tx, rx) = oneshot::channel();
525+
let ticket = Ticket::new(ride.board((1, tx)), rx);
526+
assert_eq!(ride.len(), 1);
527+
drop(ticket); // drops the pass, so the seat leaves
528+
assert_eq!(ride.len(), 0);
529+
}
530+
531+
// Race catalog. The primitive has no internal timers,
532+
// so each race is real cross-thread Mutex/Notify concurrency,
533+
// repeated to shake out interleavings.
534+
// The timeout in the signal races only guards against a hung (lost) wakeup.
535+
536+
#[test]
537+
fn race_concurrent_boards_reach_one_drain() {
538+
for _ in 0..50 {
539+
let ride: Ride<u32> = Ride::new();
540+
let n = 8u32;
541+
let start = Barrier::new(n as usize + 1);
542+
let passes = Mutex::new(Vec::new());
543+
thread::scope(|s| {
544+
for i in 0..n {
545+
let ride = &ride;
546+
let passes = &passes;
547+
let start = &start;
548+
s.spawn(move || {
549+
let pass = ride.board(i);
550+
locked(passes).push(pass);
551+
start.wait();
552+
});
553+
}
554+
start.wait();
555+
});
556+
let mut drained: Vec<u32> = ride.into_iter().collect();
557+
drained.sort_unstable();
558+
assert_eq!(drained, (0..n).collect::<Vec<_>>());
559+
}
560+
}
561+
562+
#[test]
563+
fn race_take_against_leave() {
564+
for _ in 0..100 {
565+
let ride: Ride<u32> = Ride::new();
566+
let mut passes: Vec<Pass> = (0..6).map(|i| ride.board(i)).collect();
567+
let victim = passes.remove(0); // seat 0 sits in the take(3) prefix
568+
let start = Barrier::new(2);
569+
let taken = thread::scope(|s| {
570+
let taker = {
571+
let ride = &ride;
572+
let start = &start;
573+
s.spawn(move || {
574+
start.wait();
575+
ride.take(3)
576+
})
577+
};
578+
start.wait();
579+
drop(victim); // leave seat 0 as it may be relocating
580+
taker.join().unwrap()
581+
});
582+
// 6 boarded minus the one that left: removed exactly once, never doubled.
583+
assert_eq!(taken.len() + ride.len(), 5);
584+
}
585+
}
586+
587+
#[test]
588+
fn race_drain_against_leave() {
589+
for _ in 0..100 {
590+
let ride: Ride<u32> = Ride::new();
591+
let mut passes: Vec<Pass> = (0..6).map(|i| ride.board(i)).collect();
592+
let victim = passes.remove(0);
593+
let start = Barrier::new(2);
594+
let delivered = thread::scope(|s| {
595+
let drainer = {
596+
let ride = &ride;
597+
let start = &start;
598+
s.spawn(move || {
599+
start.wait();
600+
ride.clone().into_iter().count()
601+
})
602+
};
603+
start.wait();
604+
drop(victim);
605+
drainer.join().unwrap()
606+
});
607+
// seat 0 either left before the drain or was delivered by it, never both.
608+
assert!(delivered == 5 || delivered == 6);
609+
}
610+
}
611+
612+
#[test]
613+
fn race_book_against_abandonment() {
614+
for _ in 0..100 {
615+
let logue: Logue<u32, u32> = Logue::new();
616+
let Booking::Founded(founder, ride) = logue.book(1, 0) else {
617+
unreachable!("first book founds");
618+
};
619+
let start = Barrier::new(2);
620+
let booking = thread::scope(|s| {
621+
let booker = {
622+
let logue = &logue;
623+
let start = &start;
624+
s.spawn(move || {
625+
start.wait();
626+
logue.book(1, 1)
627+
})
628+
};
629+
start.wait();
630+
drop(founder); // last seat leaves as the booking runs
631+
booker.join().unwrap()
632+
});
633+
match booking {
634+
// joined the founder's ride: the booker's seat kept it alive
635+
Booking::Joined(_pass) => assert_eq!(ride.len(), 1),
636+
// founded a replacement: the founder's ride had already abandoned
637+
Booking::Founded(_pass, fresh) => assert_eq!(fresh.len(), 1),
638+
}
639+
}
640+
}
641+
642+
#[test]
643+
fn race_board_wakes_a_boarded_waiter() {
644+
for _ in 0..50 {
645+
let ride: Ride<u32> = Ride::new();
646+
let start = Barrier::new(2);
647+
thread::scope(|s| {
648+
let waiter = {
649+
let ride = &ride;
650+
let start = &start;
651+
s.spawn(move || {
652+
let rt = tokio::runtime::Builder::new_current_thread()
653+
.enable_time()
654+
.build()
655+
.unwrap();
656+
start.wait();
657+
rt.block_on(async {
658+
tokio::time::timeout(Duration::from_secs(2), ride.boarded())
659+
.await
660+
.expect("board must wake the waiter");
661+
});
662+
})
663+
};
664+
start.wait();
665+
let _pass = ride.board(1);
666+
waiter.join().unwrap(); // join while the seat is still aboard
667+
});
668+
}
669+
}
670+
671+
#[test]
672+
fn race_last_leave_wakes_an_abandoned_waiter() {
673+
for _ in 0..50 {
674+
let ride: Ride<u32> = Ride::new();
675+
let pass = ride.board(1);
676+
let start = Barrier::new(2);
677+
thread::scope(|s| {
678+
let ride = &ride;
679+
let start = &start;
680+
s.spawn(move || {
681+
let rt = tokio::runtime::Builder::new_current_thread()
682+
.enable_time()
683+
.build()
684+
.unwrap();
685+
start.wait();
686+
rt.block_on(async {
687+
tokio::time::timeout(Duration::from_secs(2), ride.abandoned())
688+
.await
689+
.expect("the last leave must wake the waiter");
690+
});
691+
});
692+
start.wait();
693+
drop(pass); // last seat leaves, latching abandonment
694+
});
695+
}
696+
}
476697
}

0 commit comments

Comments
 (0)