1use std::sync::{Arc, Mutex};
2use std::time::{Duration, Instant};
3
4use async_trait::async_trait;
5use cortexkit_bus_trait::{
6 map_connection_failure, map_credential_sign_failure, terminally_dispose, BackendKind, BusError,
7 ClaimOutcome, ConformanceBackend, ConnectionFailure, ContentDigest, CredentialSignFailure,
8 DurableOwner, Headers, PropertyOutcome, PropertySpec, Register, RegisterUpdate, RegisterWatch,
9 Stream, StreamCursor, WatchEvent, WorkQueue,
10};
11
12use crate::{BackendEvent, InMemoryBus, InMemoryConfig, WorkQueueConfig};
13
14pub struct InMemoryConformance;
15
16#[async_trait]
17impl ConformanceBackend for InMemoryConformance {
18 fn backend_kind(&self) -> BackendKind {
19 BackendKind::InMemory
20 }
21
22 async fn probe(&self, property: PropertySpec) -> PropertyOutcome {
23 let result = match property.name {
24 "publish_ack_monotonic_sequence" => publish_ack_sequence().await,
25 "consumer_replay_within_process_lifetime" => replay_from_cursor().await,
26 "stream_delivery_count_rises_on_each_nak" => stream_delivery_count().await,
27 "stream_term_is_never_redelivered" => stream_term().await,
28 "stream_in_progress_extends_ack_wait" => stream_in_progress().await,
29 "register_revision" => register_revision().await,
30 "register_absent_vs_empty" => register_absent_vs_empty().await,
31 "watch_snapshot_then_updates_with_connection_flag" => register_watch().await,
32 "work_redelivery_count" => work_redelivery().await,
33 "work_cap_exhaustion_and_dead_letter" => cap_exhaustion().await,
34 "full_work_queue_names_exhausted_limit" => full_queue().await,
35 "error_taxonomy_and_boundary_mapping" => error_taxonomy().await,
36 other => Err(format!("in-memory has no parity probe named {other}")),
37 };
38 match result {
39 Ok(()) => PropertyOutcome::Passed,
40 Err(reason) => PropertyOutcome::Failed { reason },
41 }
42 }
43}
44
45async fn publish_ack_sequence() -> Result<(), String> {
46 let bus = InMemoryBus::new(InMemoryConfig::default());
47 let first = bus
48 .publish("ck.box.room.a.post", "one", digest("one"), Headers::new())
49 .await
50 .map_err(debug)?;
51 let second = bus
52 .publish("ck.box.room.a.post", "two", digest("two"), Headers::new())
53 .await
54 .map_err(debug)?;
55 require(
56 !first.duplicate()
57 && !second.duplicate()
58 && second.stream_sequence() > first.stream_sequence(),
59 "publish sequence did not increase",
60 )
61}
62
63async fn replay_from_cursor() -> Result<(), String> {
64 let bus =
65 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
66 let receipt = bus
67 .publish(
68 "ck.box.peer.agent.s.deliver",
69 "m1",
70 digest("m1"),
71 Headers::new(),
72 )
73 .await
74 .map_err(debug)?;
75 require(!receipt.duplicate(), "unexpected duplicate publish")?;
76 let expected = bus
77 .consumer(DurableOwner::Agent("agent"))
78 .await
79 .map_err(debug)?
80 .next()
81 .await
82 .map_err(debug)?
83 .ok_or("first cursor saw no message")?;
84 let mut reopened = bus
85 .consumer(DurableOwner::Agent("agent"))
86 .await
87 .map_err(debug)?;
88 let replayed = reopened
89 .next()
90 .await
91 .map_err(debug)?
92 .ok_or("reopened cursor saw no replay")?;
93 require(
94 expected.stream_seq == replayed.stream_seq,
95 "cursor did not replay",
96 )?;
97 reopened.ack().await.map_err(debug)
98}
99
100const STREAM_SUBJECT: &str = "ck.box.peer.agent.s.deliver";
101
102async fn stream_delivery_count() -> Result<(), String> {
103 let bus =
104 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
105 let receipt = bus
106 .publish(STREAM_SUBJECT, "m1", digest("m1"), Headers::new())
107 .await
108 .map_err(debug)?;
109 require(!receipt.duplicate(), "unexpected duplicate publish")?;
110 let mut cursor = bus
111 .consumer(DurableOwner::Agent("agent"))
112 .await
113 .map_err(debug)?;
114 for expected in 1..=3 {
115 let delivery = cursor
116 .next()
117 .await
118 .map_err(debug)?
119 .ok_or("a nak'd message was not redelivered")?;
120 require(
121 delivery.delivery_count == expected,
122 &format!(
123 "delivery {expected} reported count {}",
124 delivery.delivery_count
125 ),
126 )?;
127 if expected < 3 {
128 cursor.nak(Duration::ZERO).await.map_err(debug)?;
129 }
130 }
131 cursor.ack().await.map_err(debug)
132}
133
134async fn stream_term() -> Result<(), String> {
135 let bus =
136 InMemoryBus::new(InMemoryConfig::default().with_durable(DurableOwner::Agent("agent")));
137 for id in ["m1", "m2"] {
138 let receipt = bus
139 .publish(STREAM_SUBJECT, id, digest(id), Headers::new())
140 .await
141 .map_err(debug)?;
142 require(!receipt.duplicate(), "unexpected duplicate publish")?;
143 }
144 let mut cursor = bus
145 .consumer(DurableOwner::Agent("agent"))
146 .await
147 .map_err(debug)?;
148 let first = cursor
149 .next()
150 .await
151 .map_err(debug)?
152 .ok_or("first message was not delivered")?;
153 require(first.message.id == "m1", "first delivery was not m1")?;
154 cursor.term(Some("body_absent")).await.map_err(debug)?;
155 let second = cursor
156 .next()
157 .await
158 .map_err(debug)?
159 .ok_or("the message after a term was not delivered")?;
160 require(
161 second.message.id == "m2" && second.delivery_count == 1,
162 "a terminated message was redelivered",
163 )?;
164 cursor.ack().await.map_err(debug)?;
165 let mut reopened = bus
166 .consumer(DurableOwner::Agent("agent"))
167 .await
168 .map_err(debug)?;
169 require(
170 reopened.next().await.map_err(debug)?.is_none(),
171 "a terminated message was redelivered to a reopened cursor",
172 )?;
173 require(
174 bus.events().iter().any(|event| {
175 matches!(
176 event,
177 BackendEvent::StreamTerminated { stream_seq, reason: Some(reason) }
178 if *stream_seq == first.stream_seq && reason == "body_absent"
179 )
180 }),
181 "the termination and its reason were not recorded",
182 )
183}
184
185async fn stream_in_progress() -> Result<(), String> {
186 let ack_wait = Duration::from_millis(200);
187 let clock = Arc::new(Mutex::new(Instant::now()));
188 let mut bus = InMemoryBus::new(
189 InMemoryConfig::default()
190 .with_durable(DurableOwner::Agent("agent"))
191 .with_stream_ack_wait(ack_wait),
192 );
193 bus.stream_clock = {
196 let clock = Arc::clone(&clock);
197 Arc::new(move || *clock.lock().expect("stream probe clock poisoned"))
198 };
199 for id in ["m1", "m2"] {
200 let receipt = bus
201 .publish(STREAM_SUBJECT, id, digest(id), Headers::new())
202 .await
203 .map_err(debug)?;
204 require(!receipt.duplicate(), "unexpected duplicate publish")?;
205 }
206 let mut slow = bus
207 .consumer(DurableOwner::Agent("agent"))
208 .await
209 .map_err(debug)?;
210 let mut other = bus
211 .consumer(DurableOwner::Agent("agent"))
212 .await
213 .map_err(debug)?;
214
215 slow.next()
218 .await
219 .map_err(debug)?
220 .ok_or("m1 was not delivered")?;
221 *clock.lock().expect("stream probe clock poisoned") += ack_wait * 3 / 2;
222 let redelivered = other
223 .next()
224 .await
225 .map_err(debug)?
226 .ok_or("m1 was not redelivered after the ack wait")?;
227 require(
228 redelivered.message.id == "m1" && redelivered.delivery_count == 2,
229 "the ack-wait redelivery was not m1's second delivery",
230 )?;
231 other.ack().await.map_err(debug)?;
232 let _ = slow.ack().await;
233
234 slow.next()
237 .await
238 .map_err(debug)?
239 .ok_or("m2 was not delivered")?;
240 *clock.lock().expect("stream probe clock poisoned") += ack_wait * 3 / 5;
241 slow.in_progress().await.map_err(debug)?;
242 *clock.lock().expect("stream probe clock poisoned") += ack_wait * 3 / 5;
243 require(
244 other.next().await.map_err(debug)?.is_none(),
245 "a message reported in progress was redelivered past the original ack wait",
246 )?;
247 slow.ack().await.map_err(debug)?;
248 require(
249 other.next().await.map_err(debug)?.is_none(),
250 "an acknowledged message was redelivered",
251 )
252}
253
254async fn register_revision() -> Result<(), String> {
255 let bus = InMemoryBus::new(InMemoryConfig::default());
256 let first = bus.put("module", b"one".to_vec()).await.map_err(debug)?;
257 let second = bus.put("module", b"two".to_vec()).await.map_err(debug)?;
258 let read = bus.get("module").await.map_err(debug)?;
259 require(second > first, "register revision did not increase")?;
260 require(read.revision == second, "get returned the wrong revision")
261}
262
263async fn register_absent_vs_empty() -> Result<(), String> {
264 let bus = InMemoryBus::new(InMemoryConfig::default());
265 bus.put("empty", Vec::new()).await.map_err(debug)?;
266 let empty = bus.get("empty").await.map_err(debug)?;
267 require(empty.value.is_empty(), "empty value was not preserved")?;
268 require(
269 matches!(bus.get("missing").await, Err(BusError::Absent { .. })),
270 "missing key was not Absent",
271 )
272}
273
274async fn register_watch() -> Result<(), String> {
275 let bus = InMemoryBus::new(InMemoryConfig::default());
276 bus.put("module.a", b"v1".to_vec()).await.map_err(debug)?;
277 let mut watch = bus.watch("module.").await.map_err(debug)?;
278 require(
279 !watch.has_snapshot(),
280 "watch claimed a snapshot before delivery",
281 )?;
282 let snapshot = watch.next().await.map_err(debug)?;
283 require(
284 matches!(snapshot, WatchEvent::Snapshot { ref entries, .. } if entries.len() == 1),
285 "watch did not deliver the complete initial snapshot",
286 )?;
287 require(
288 watch.has_snapshot(),
289 "watch did not record snapshot receipt",
290 )?;
291 bus.put("module.a", b"v2".to_vec()).await.map_err(debug)?;
292 require(
293 matches!(
294 watch.next().await.map_err(debug)?,
295 WatchEvent::Update(RegisterUpdate::Put(_))
296 ),
297 "watch did not distinguish an update",
298 )
299}
300
301async fn work_redelivery() -> Result<(), String> {
302 let bus = queue_bus(3, 4_096);
303 let receipt = bus
304 .publish(
305 "ck.box.effect.a.s.intent",
306 "m1",
307 digest("m1"),
308 Headers::new(),
309 )
310 .await
311 .map_err(debug)?;
312 require(!receipt.duplicate(), "unexpected duplicate publish")?;
313 let first = item(bus.claim().await.map_err(debug)?)?;
314 bus.nak(first.token, Duration::ZERO).await.map_err(debug)?;
315 let second = item(bus.claim().await.map_err(debug)?)?;
316 require(
317 first.delivery_count == 1,
318 "first delivery count was not one",
319 )?;
320 require(second.delivery_count == 2, "redelivery count was not two")
321}
322
323async fn cap_exhaustion() -> Result<(), String> {
324 let bus = queue_bus(1, 4_096);
325 let receipt = bus
326 .publish(
327 "ck.box.effect.a.s.intent",
328 "m1",
329 digest("m1"),
330 Headers::new(),
331 )
332 .await
333 .map_err(debug)?;
334 require(!receipt.duplicate(), "unexpected duplicate publish")?;
335 let first = item(bus.claim().await.map_err(debug)?)?;
336 bus.nak(first.token, Duration::ZERO).await.map_err(debug)?;
337 let exhausted = match bus.claim().await.map_err(debug)? {
338 ClaimOutcome::MaxDeliveriesExceeded(exhausted) => exhausted,
339 other => {
340 return Err(format!(
341 "cap did not report MaxDeliveriesExceeded: {other:?}"
342 ))
343 }
344 };
345 let dead_stream = InMemoryBus::new(InMemoryConfig::default());
346 let queue = DeadLetterObservedQueue {
347 queue: &bus,
348 dead_stream: &dead_stream,
349 };
350 let receipt = terminally_dispose(
351 &dead_stream,
352 &queue,
353 "box",
354 &exhausted,
355 digest("dead-record"),
356 )
357 .await
358 .map_err(debug)?;
359 require(!receipt.duplicate(), "dead letter was not stored")?;
360 require(
361 bus.events()
362 .iter()
363 .any(|event| matches!(event, BackendEvent::Terminated { .. })),
364 "exhausted delivery was not terminated",
365 )
366}
367
368struct DeadLetterObservedQueue<'a> {
371 queue: &'a InMemoryBus,
372 dead_stream: &'a InMemoryBus,
373}
374
375#[async_trait]
376impl WorkQueue for DeadLetterObservedQueue<'_> {
377 async fn claim(&self) -> cortexkit_bus_trait::BusResult<ClaimOutcome> {
378 self.queue.claim().await
379 }
380
381 async fn ack(
382 &self,
383 token: cortexkit_bus_trait::DeliveryToken,
384 ) -> cortexkit_bus_trait::BusResult<()> {
385 self.queue.ack(token).await
386 }
387
388 async fn nak(
389 &self,
390 token: cortexkit_bus_trait::DeliveryToken,
391 delay: Duration,
392 ) -> cortexkit_bus_trait::BusResult<()> {
393 self.queue.nak(token, delay).await
394 }
395
396 async fn term(
397 &self,
398 token: cortexkit_bus_trait::DeliveryToken,
399 ) -> cortexkit_bus_trait::BusResult<()> {
400 if !self.dead_stream.events().iter().any(|event| {
401 matches!(event, BackendEvent::Published { subject, .. } if subject == "ck.box.effect.dead")
402 }) {
403 return Err(BusError::denied("ck.box.effect.dead", "dead-letter publish did not precede term"));
404 }
405 self.queue.term(token).await
406 }
407}
408
409async fn full_queue() -> Result<(), String> {
410 let bus = queue_bus(5, 150);
411 let receipt = bus
412 .publish(
413 "ck.box.effect.a.s.intent",
414 "first",
415 digest("first"),
416 Headers::new(),
417 )
418 .await
419 .map_err(debug)?;
420 require(!receipt.duplicate(), "unexpected duplicate publish")?;
421 let error = bus
422 .publish(
423 "ck.box.effect.a.s.intent",
424 "second",
425 digest("second"),
426 Headers::new(),
427 )
428 .await
429 .expect_err("second publish should exceed max-bytes");
430 require(
431 matches!(error, BusError::Unavailable { exhausted_limit: Some(ref limit), .. } if limit == "max_bytes"),
432 "full queue did not name max_bytes in Unavailable",
433 )
434}
435
436async fn error_taxonomy() -> Result<(), String> {
437 let denied_subject = "ck.box.room.denied.post";
438 let bus = InMemoryBus::new(
439 InMemoryConfig::default()
440 .deny_subject(denied_subject)
441 .with_work_queue(WorkQueueConfig {
442 subject: "ck.box.effect.a.s.intent".into(),
443 max_bytes: 4_096,
444 max_deliveries: 5,
445 max_nak_delay: Duration::from_millis(1),
446 }),
447 );
448 require(
449 matches!(bus.publish(denied_subject, "m", digest("m"), Headers::new()).await, Err(BusError::Denied { subject, .. }) if subject == denied_subject),
450 "fixture denial was not Denied",
451 )?;
452 require(
453 matches!(bus.get("missing").await, Err(BusError::Absent { .. })),
454 "missing register key was not Absent",
455 )?;
456 let receipt = bus
457 .publish(
458 "ck.box.effect.a.s.intent",
459 "work",
460 digest("work"),
461 Headers::new(),
462 )
463 .await
464 .map_err(debug)?;
465 require(!receipt.duplicate(), "unexpected duplicate publish")?;
466 let claimed = item(bus.claim().await.map_err(debug)?)?;
467 require(
468 matches!(
469 bus.nak(claimed.token, Duration::from_secs(1)).await,
470 Err(BusError::Clamped {
471 retry_after: Some(_),
472 ..
473 })
474 ),
475 "oversized delay was not reported Clamped",
476 )?;
477 require(
478 matches!(
479 map_connection_failure(ConnectionFailure::WriteOnSeveredSocket),
480 BusError::Unavailable { .. }
481 ),
482 "severed write was not Unavailable",
483 )?;
484 require(
485 matches!(
486 map_connection_failure(ConnectionFailure::ServerClosedThenReauthenticationRefused {
487 subject: denied_subject.into(),
488 reason: "revoked".into()
489 }),
490 BusError::Denied { .. }
491 ),
492 "reconnect refusal was not Denied",
493 )?;
494 require(
495 matches!(
496 map_credential_sign_failure(CredentialSignFailure::RecordMissing {
497 record: "nats.module.g1.e0".into()
498 }),
499 BusError::Absent { .. }
500 ),
501 "missing signing record was not Absent",
502 )
503}
504
505fn queue_bus(max_deliveries: u32, max_bytes: usize) -> InMemoryBus {
506 InMemoryBus::new(InMemoryConfig::default().with_work_queue(WorkQueueConfig {
507 subject: "ck.box.effect.a.s.intent".into(),
508 max_bytes,
509 max_deliveries,
510 max_nak_delay: Duration::from_secs(30),
511 }))
512}
513
514fn item(outcome: ClaimOutcome) -> Result<cortexkit_bus_trait::WorkItem, String> {
515 match outcome {
516 ClaimOutcome::Item(item) => Ok(item),
517 other => Err(format!("expected work item, got {other:?}")),
518 }
519}
520
521fn digest(value: &str) -> ContentDigest {
522 ContentDigest::of_bytes(value.as_bytes())
523}
524
525fn require(condition: bool, reason: &str) -> Result<(), String> {
526 condition.then_some(()).ok_or_else(|| reason.into())
527}
528
529fn debug(error: impl std::fmt::Debug) -> String {
530 format!("{error:?}")
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use cortexkit_bus_trait::{
537 classify_properties, run_conformance, PropertyApplicability, PropertySet, SuiteBug,
538 CONFORMANCE_PROPERTIES,
539 };
540
541 struct WeakenedNatsFixture;
542
543 #[async_trait]
544 impl ConformanceBackend for WeakenedNatsFixture {
545 fn backend_kind(&self) -> BackendKind {
546 BackendKind::Nats
547 }
548
549 async fn probe(&self, property: PropertySpec) -> PropertyOutcome {
550 match property.set {
551 PropertySet::Parity => InMemoryConformance.probe(property).await,
552 PropertySet::NatsOnly => PropertyOutcome::Failed {
553 reason: "fixture deliberately has only process-local semantics".into(),
554 },
555 }
556 }
557 }
558
559 #[tokio::test]
560 async fn in_memory_passes_every_parity_property() {
561 let report = run_conformance(&InMemoryConformance, PropertySet::Parity)
562 .await
563 .expect("parity is applicable to in-memory");
564
565 assert_eq!(report.property_set_name, "parity");
566 for (property, outcome) in &report.outcomes {
567 assert_eq!(outcome, &PropertyOutcome::Passed, "{}", property.name);
568 }
569 assert!(report.passed());
570 }
571
572 #[tokio::test]
573 async fn asserting_nats_only_against_in_memory_is_a_suite_bug() {
574 let result = run_conformance(&InMemoryConformance, PropertySet::NatsOnly).await;
575 assert_eq!(result, Err(SuiteBug::NatsOnlyAssertedAgainstInMemory));
576 }
577
578 #[tokio::test(start_paused = true)]
579 async fn in_progress_probe_does_not_wait_for_wall_clock() {
580 stream_in_progress().await.unwrap();
581 }
582
583 #[tokio::test]
584 async fn weakened_backend_passes_real_parity_probes_but_fails_nats_only() {
585 let parity = run_conformance(&WeakenedNatsFixture, PropertySet::Parity)
586 .await
587 .expect("parity run is valid");
588 let nats_only = run_conformance(&WeakenedNatsFixture, PropertySet::NatsOnly)
589 .await
590 .expect("NATS-only run is valid for a NATS-labelled fixture");
591
592 assert!(parity.passed());
593 assert!(!nats_only.passed());
594 }
595
596 #[test]
597 fn every_nats_only_property_is_not_applicable_not_passed() {
598 let classifications = classify_properties(BackendKind::InMemory);
599 let nats_only = classifications
600 .iter()
601 .filter(|row| row.property.set == PropertySet::NatsOnly)
602 .collect::<Vec<_>>();
603
604 assert_eq!(
605 nats_only.len(),
606 CONFORMANCE_PROPERTIES
607 .iter()
608 .filter(|property| property.set == PropertySet::NatsOnly)
609 .count()
610 );
611 assert!(nats_only.iter().all(|row| matches!(
612 row.applicability,
613 PropertyApplicability::NotApplicable { .. }
614 )));
615 }
616}