Skip to content

Commit f134731

Browse files
committed
🧪 test(driver): tell the worker its request is gone
A client that abandons an export leaves nothing waiting for the summary, so the send that opens the snapshot has nowhere to go. The worker has to learn the request went rather than carry on producing a body no one will read. The order is forced rather than raced. The driver parks inside the plan and says so, the test drops the request and waits for that drop to finish, and only then is the driver released to send. Nothing here depends on which thread wins.
1 parent a0b30b4 commit f134731

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

  • crates/peryx-driver/tests/unit/retention

crates/peryx-driver/tests/unit/retention/tests.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,71 @@ async fn test_export_body_reports_a_second_snapshot_through_the_stream() {
719719

720720
assert!(format!("{error:?}").contains("more than one snapshot"), "{error:?}");
721721
}
722+
723+
/// Parks inside the plan until the test releases it, so the request can be abandoned first and the
724+
/// summary send lands on a receiver that is already gone.
725+
struct GatedDriver {
726+
entered: tokio::sync::mpsc::UnboundedSender<()>,
727+
release: std::sync::Mutex<Option<std::sync::mpsc::Receiver<()>>>,
728+
saw: tokio::sync::mpsc::UnboundedSender<String>,
729+
}
730+
731+
impl RetentionDriver for GatedDriver {
732+
fn validate_retention(&self, _policy: &RetentionPolicy) -> Result<(), String> {
733+
Ok(())
734+
}
735+
736+
fn plan_retention(
737+
&self,
738+
scan: &crate::serving::RetentionScan<'_>,
739+
start: &mut dyn FnMut(peryx_policy::RetentionSummary) -> Result<(), String>,
740+
_emit: &mut dyn FnMut(RetentionDecision) -> Result<(), String>,
741+
) -> Result<(), String> {
742+
self.entered.send(()).unwrap();
743+
// Blocking is correct here: the plan already runs on the blocking pool.
744+
let release = self.release.lock().unwrap().take().expect("released once");
745+
release.recv().unwrap();
746+
let summary = peryx_policy::RetentionSummary {
747+
policy_version: scan.policy.version(),
748+
frontier: peryx_policy::RetentionFrontier::default(),
749+
};
750+
start(summary).inspect_err(|reason| self.saw.send(reason.clone()).unwrap())
751+
}
752+
}
753+
754+
/// A client that abandons an export leaves nothing waiting for the summary, so the send that opens
755+
/// the snapshot has nowhere to go. The worker has to learn the request is gone rather than carry on
756+
/// producing a body no one will read.
757+
///
758+
/// The order is forced rather than raced: the driver parks inside the plan, the test drops the
759+
/// request and waits for that drop to complete, and only then is the driver released.
760+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
761+
async fn test_export_body_tells_the_worker_when_the_request_is_gone() {
762+
let (_dir, meta) = store();
763+
let (entered, mut entered_rx) = tokio::sync::mpsc::unbounded_channel();
764+
let (saw, mut saw_rx) = tokio::sync::mpsc::unbounded_channel();
765+
let (release, release_rx) = std::sync::mpsc::channel();
766+
let driver: Arc<dyn RetentionDriver> = Arc::new(GatedDriver {
767+
entered,
768+
release: std::sync::Mutex::new(Some(release_rx)),
769+
saw,
770+
});
771+
let gates = super::RetentionGates::new(1);
772+
let permit = gates.try_enter("alpha").unwrap();
773+
let export = super::RetentionExport {
774+
index: "alpha".to_owned(),
775+
ecosystem: "example".to_owned(),
776+
policy: empty_policy(),
777+
now: None,
778+
after: 0,
779+
expect: None,
780+
};
781+
let request = tokio::spawn(async move { super::export_body(driver, meta, export, permit).await.map(|_| ()) });
782+
783+
entered_rx.recv().await.expect("the plan parked");
784+
request.abort();
785+
assert!(request.await.unwrap_err().is_cancelled());
786+
release.send(()).unwrap();
787+
788+
assert_eq!(saw_rx.recv().await.as_deref(), Some("export request gone"));
789+
}

0 commit comments

Comments
 (0)