|
1 | 1 | use std::collections::{BTreeMap, HashMap}; |
| 2 | +use std::future::Future; |
2 | 3 | use std::hash::Hash; |
| 4 | +use std::pin::Pin; |
3 | 5 | use std::sync::{Arc, Mutex, MutexGuard, Weak}; |
| 6 | +use std::task::{Context, Poll}; |
4 | 7 |
|
5 | | -use tokio::sync::Notify; |
| 8 | +use tokio::sync::{Notify, oneshot}; |
6 | 9 |
|
7 | 10 | // Mutex guard that recovers a poisoned lock via into_inner. |
8 | 11 | // A panic under the lock must not brick the ride. |
@@ -152,6 +155,10 @@ impl<P: Send + 'static> Ride<P> { |
152 | 155 | pub fn board(&self, cargo: P) -> Pass { |
153 | 156 | let pass = { |
154 | 157 | 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 | + ); |
155 | 162 | self.seat(&mut state, cargo) |
156 | 163 | }; |
157 | 164 | self.inner.boarded.notify_waiters(); |
@@ -306,7 +313,7 @@ impl<K, P> Logue<K, P> { |
306 | 313 | } |
307 | 314 | } |
308 | 315 |
|
309 | | -impl<K: Hash + Eq + Clone, P: Send + 'static> Logue<K, P> { |
| 316 | +impl<K: Hash + Eq, P: Send + 'static> Logue<K, P> { |
310 | 317 | // Join the key's live ride, or found a new one. |
311 | 318 | // An abandoned ride under the key is not joined: it is replaced by the freshly founded one. |
312 | 319 | 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> { |
339 | 346 | } |
340 | 347 | } |
341 | 348 |
|
| 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 | + |
342 | 372 | #[cfg(test)] |
343 | 373 | mod tests { |
344 | 374 | use super::*; |
| 375 | + use std::sync::Barrier; |
| 376 | + use std::thread; |
| 377 | + use std::time::Duration; |
345 | 378 |
|
346 | 379 | #[test] |
347 | 380 | fn boarding_accumulates_seats() { |
@@ -473,4 +506,192 @@ mod tests { |
473 | 506 | let second = logue.book(1, 101); |
474 | 507 | assert!(matches!(&second, Booking::Founded(..))); |
475 | 508 | } |
| 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 | + } |
476 | 697 | } |
0 commit comments