1use std::time::Duration;
4
5#[cfg(test)]
6use std::sync::Arc;
7
8use bytes::Bytes;
9use object_store::path::Path;
10use object_store::{ObjectStoreExt, PutMode, PutOptions, PutPayload, UpdateVersion};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14use crate::checkpoint::{LeaderProof, StreamGeneration};
15
16use super::{now_millis, ClusterCheckpointAuthorityError, LeaderLeaseStore, LeaseError};
17
18const REGISTRY_PATH: &str = "control/subscription-replay-pins/v1.json";
19const REGISTRY_VERSION: u16 = 1;
20const MAX_REGISTRY_BYTES: u64 = 256 * 1024;
21const MAX_REPLAY_PINS: usize = 1_024;
22const MAX_CAS_ATTEMPTS: usize = 16;
23const REPLAY_PIN_TTL_MS: i64 = 120_000;
24
25pub const SUBSCRIPTION_REPLAY_PIN_RENEW_INTERVAL: Duration = Duration::from_secs(30);
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct SubscriptionReplayPin {
31 id: Uuid,
32 stream_generation: StreamGeneration,
33 epoch: u64,
34}
35
36impl SubscriptionReplayPin {
37 #[must_use]
39 pub const fn stream_generation(&self) -> StreamGeneration {
40 self.stream_generation
41 }
42
43 #[must_use]
45 pub const fn epoch(&self) -> u64 {
46 self.epoch
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum SubscriptionReplayPinAcquire {
53 Acquired(SubscriptionReplayPin),
55 Pruned {
57 artifact_before_epoch: u64,
59 },
60 Capacity,
62 Contended,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68struct ReplayPinSlot {
69 id: Uuid,
70 stream_generation: StreamGeneration,
71 epoch: u64,
72 expires_at_ms: i64,
73}
74
75impl ReplayPinSlot {
76 fn matches(&self, pin: &SubscriptionReplayPin) -> bool {
77 self.id == pin.id
78 && self.stream_generation == pin.stream_generation
79 && self.epoch == pin.epoch
80 }
81
82 fn validate(&self) -> Result<(), LeaseError> {
83 if self.id.is_nil()
84 || self.stream_generation.digest().as_bytes() == &[0; 32]
85 || self.epoch == 0
86 || self.expires_at_ms <= 0
87 {
88 return Err(LeaseError::Invalid(
89 "subscription replay pin is not canonical".into(),
90 ));
91 }
92 Ok(())
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98struct PendingCleanupFloor {
99 id: Uuid,
100 leader_proof: LeaderProof,
101 selected_epoch: u64,
102}
103
104impl PendingCleanupFloor {
105 fn validate(&self) -> Result<(), LeaseError> {
106 if self.id.is_nil() || !self.leader_proof.is_canonical() || self.selected_epoch == 0 {
107 return Err(LeaseError::Invalid(
108 "pending subscription cleanup floor is not canonical".into(),
109 ));
110 }
111 Ok(())
112 }
113
114 fn committed(&self) -> SubscriptionCleanupCommit {
115 SubscriptionCleanupCommit {
116 id: self.id,
117 leader_proof: self.leader_proof.clone(),
118 selected_epoch: self.selected_epoch,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub(super) struct SubscriptionCleanupCommit {
126 id: Uuid,
127 leader_proof: LeaderProof,
128 selected_epoch: u64,
129}
130
131impl SubscriptionCleanupCommit {
132 pub(super) fn validate(&self) -> Result<(), LeaseError> {
133 if self.id.is_nil() || !self.leader_proof.is_canonical() || self.selected_epoch == 0 {
134 return Err(LeaseError::Invalid(
135 "subscription cleanup commit is not canonical".into(),
136 ));
137 }
138 Ok(())
139 }
140
141 fn matches(&self, pending: &PendingCleanupFloor) -> bool {
142 self.id == pending.id
143 && self.leader_proof == pending.leader_proof
144 && self.selected_epoch == pending.selected_epoch
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150struct ReplayPinRegistry {
151 version: u16,
152 revision: u64,
153 artifact_before_epoch: u64,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pending_cleanup: Option<PendingCleanupFloor>,
156 pins: Vec<ReplayPinSlot>,
157}
158
159impl ReplayPinRegistry {
160 fn empty() -> Self {
161 Self {
162 version: REGISTRY_VERSION,
163 revision: 0,
164 artifact_before_epoch: 0,
165 pending_cleanup: None,
166 pins: Vec::new(),
167 }
168 }
169
170 fn validate(&self, persisted: bool) -> Result<(), LeaseError> {
171 if self.version != REGISTRY_VERSION
172 || (persisted && self.revision == 0)
173 || self.pins.len() > MAX_REPLAY_PINS
174 || self.pins.windows(2).any(|pair| pair[0].id >= pair[1].id)
175 {
176 return Err(LeaseError::Invalid(
177 "subscription replay pin registry is not canonical".into(),
178 ));
179 }
180 if let Some(pending) = &self.pending_cleanup {
181 pending.validate()?;
182 if pending.selected_epoch <= self.artifact_before_epoch
183 || self
184 .pins
185 .iter()
186 .any(|pin| pin.epoch < pending.selected_epoch)
187 {
188 return Err(LeaseError::Invalid(
189 "pending subscription cleanup floor crosses committed state or a replay pin"
190 .into(),
191 ));
192 }
193 }
194 for pin in &self.pins {
195 pin.validate()?;
196 if pin.epoch < self.artifact_before_epoch {
197 return Err(LeaseError::Invalid(
198 "subscription replay pin lies below the cleanup floor".into(),
199 ));
200 }
201 }
202 Ok(())
203 }
204
205 fn prune_expired(&mut self, now_ms: i64) -> bool {
206 let before = self.pins.len();
207 self.pins.retain(|pin| pin.expires_at_ms > now_ms);
208 self.pins.len() != before
209 }
210}
211
212struct VersionedRegistry {
213 registry: ReplayPinRegistry,
214 update_version: Option<UpdateVersion>,
215}
216
217struct RegistryMutation<T> {
218 value: T,
219 changed: bool,
220}
221
222impl<T> RegistryMutation<T> {
223 const fn unchanged(value: T) -> Self {
224 Self {
225 value,
226 changed: false,
227 }
228 }
229
230 const fn changed(value: T) -> Self {
231 Self {
232 value,
233 changed: true,
234 }
235 }
236}
237
238enum CleanupFloorPreparation {
239 Committed(u64),
240 Pending(PendingCleanupFloor),
241}
242
243impl LeaderLeaseStore {
244 pub async fn acquire_subscription_replay_pin(
251 &self,
252 stream_generation: StreamGeneration,
253 epoch: u64,
254 ) -> Result<SubscriptionReplayPinAcquire, ClusterCheckpointAuthorityError> {
255 if stream_generation.digest().as_bytes() == &[0; 32] || epoch == 0 {
256 return Err(LeaseError::Invalid(
257 "subscription replay pin identity is not canonical".into(),
258 )
259 .into());
260 }
261 let pin = SubscriptionReplayPin {
262 id: Uuid::new_v4(),
263 stream_generation,
264 epoch,
265 };
266 self.mutate_subscription_replay_registry(|registry, now_ms| {
267 if epoch < registry.artifact_before_epoch {
268 return Ok(RegistryMutation::unchanged(
269 SubscriptionReplayPinAcquire::Pruned {
270 artifact_before_epoch: registry.artifact_before_epoch,
271 },
272 ));
273 }
274 if registry
275 .pending_cleanup
276 .as_ref()
277 .is_some_and(|pending| epoch < pending.selected_epoch)
278 {
279 return Ok(RegistryMutation::unchanged(
280 SubscriptionReplayPinAcquire::Contended,
281 ));
282 }
283 if registry.pins.iter().any(|slot| slot.matches(&pin)) {
284 return Ok(RegistryMutation::unchanged(
285 SubscriptionReplayPinAcquire::Acquired(pin.clone()),
286 ));
287 }
288 if registry.pins.len() == MAX_REPLAY_PINS {
289 return Ok(RegistryMutation::unchanged(
290 SubscriptionReplayPinAcquire::Capacity,
291 ));
292 }
293 let expires_at_ms = replay_pin_expiry(now_ms)?;
294 registry.pins.push(ReplayPinSlot {
295 id: pin.id,
296 stream_generation,
297 epoch,
298 expires_at_ms,
299 });
300 registry.pins.sort_unstable_by_key(|slot| slot.id);
301 Ok(RegistryMutation::changed(
302 SubscriptionReplayPinAcquire::Acquired(pin.clone()),
303 ))
304 })
305 .await
306 .map_err(Into::into)
307 }
308
309 pub async fn renew_subscription_replay_pin(
316 &self,
317 pin: &SubscriptionReplayPin,
318 ) -> Result<bool, ClusterCheckpointAuthorityError> {
319 let pin = pin.clone();
320 self.mutate_subscription_replay_registry(|registry, now_ms| {
321 if pin.epoch < registry.artifact_before_epoch {
322 registry.pins.retain(|slot| !slot.matches(&pin));
323 return Ok(RegistryMutation::changed(false));
324 }
325 let Some(slot) = registry.pins.iter_mut().find(|slot| slot.matches(&pin)) else {
326 return Ok(RegistryMutation::unchanged(false));
327 };
328 slot.expires_at_ms = replay_pin_expiry(now_ms)?;
329 Ok(RegistryMutation::changed(true))
330 })
331 .await
332 .map_err(Into::into)
333 }
334
335 pub async fn release_subscription_replay_pin(
342 &self,
343 pin: &SubscriptionReplayPin,
344 ) -> Result<(), ClusterCheckpointAuthorityError> {
345 let pin = pin.clone();
346 self.mutate_subscription_replay_registry(|registry, _| {
347 let before = registry.pins.len();
348 registry.pins.retain(|slot| !slot.matches(&pin));
349 Ok(if registry.pins.len() == before {
350 RegistryMutation::unchanged(())
351 } else {
352 RegistryMutation::changed(())
353 })
354 })
355 .await
356 .map_err(Into::into)
357 }
358
359 pub async fn reserve_subscription_cleanup_floor(
366 &self,
367 proof: &LeaderProof,
368 requested_epoch: u64,
369 ) -> Result<u64, ClusterCheckpointAuthorityError> {
370 if !proof.is_canonical() || requested_epoch == 0 {
371 return Err(LeaseError::Invalid(
372 "subscription cleanup authority is not canonical".into(),
373 )
374 .into());
375 }
376 let preparation = self
377 .prepare_subscription_cleanup_floor(proof, requested_epoch)
378 .await?;
379 let pending = match preparation {
380 CleanupFloorPreparation::Committed(floor) => return Ok(floor),
381 CleanupFloorPreparation::Pending(pending) => pending,
382 };
383 if let Err(error) = self
384 .commit_subscription_cleanup_floor(proof, &pending)
385 .await
386 {
387 let _ = self
390 .mutate_subscription_replay_registry(|_, _| Ok(RegistryMutation::unchanged(())))
391 .await;
392 return Err(error);
393 }
394 self.mutate_subscription_replay_registry(|registry, _| {
395 if registry.artifact_before_epoch < pending.selected_epoch {
396 return Err(LeaseError::Invalid(
397 "committed subscription cleanup marker was not applied to its registry".into(),
398 ));
399 }
400 Ok(RegistryMutation::unchanged(registry.artifact_before_epoch))
401 })
402 .await
403 .map_err(Into::into)
404 }
405
406 async fn prepare_subscription_cleanup_floor(
407 &self,
408 proof: &LeaderProof,
409 requested_epoch: u64,
410 ) -> Result<CleanupFloorPreparation, ClusterCheckpointAuthorityError> {
411 self.require_subscription_cleanup_leader(proof).await?;
412 let proof = proof.clone();
413 self.mutate_subscription_replay_registry(|registry, _| {
414 if let Some(pending) = ®istry.pending_cleanup {
415 if pending.leader_proof != proof {
416 return Err(LeaseError::Fenced(
417 "another leader term owns the pending subscription cleanup floor".into(),
418 ));
419 }
420 return Ok(RegistryMutation::unchanged(
421 CleanupFloorPreparation::Pending(pending.clone()),
422 ));
423 }
424 if registry.artifact_before_epoch >= requested_epoch {
425 return Ok(RegistryMutation::unchanged(
426 CleanupFloorPreparation::Committed(registry.artifact_before_epoch),
427 ));
428 }
429 let pending = {
430 let pinned = registry.pins.iter().map(|pin| pin.epoch).min();
431 let selected = pinned.map_or(requested_epoch, |epoch| requested_epoch.min(epoch));
432 let selected = registry.artifact_before_epoch.max(selected);
433 if selected == registry.artifact_before_epoch {
434 return Ok(RegistryMutation::unchanged(
435 CleanupFloorPreparation::Committed(selected),
436 ));
437 }
438 PendingCleanupFloor {
439 id: Uuid::new_v4(),
440 leader_proof: proof.clone(),
441 selected_epoch: selected,
442 }
443 };
444 pending.validate()?;
445 registry.pending_cleanup = Some(pending.clone());
446 Ok(RegistryMutation::changed(CleanupFloorPreparation::Pending(
447 pending,
448 )))
449 })
450 .await
451 .map_err(|error| match error {
452 LeaseError::Fenced(_) => ClusterCheckpointAuthorityError::Fenced,
453 error => error.into(),
454 })
455 }
456
457 async fn commit_subscription_cleanup_floor(
458 &self,
459 proof: &LeaderProof,
460 pending: &PendingCleanupFloor,
461 ) -> Result<(), ClusterCheckpointAuthorityError> {
462 pending.validate()?;
463 if pending.leader_proof != *proof {
464 return Err(ClusterCheckpointAuthorityError::Fenced);
465 }
466 for _ in 0..MAX_CAS_ATTEMPTS {
467 let published = self
468 .load_published_authority_head()
469 .await?
470 .ok_or(ClusterCheckpointAuthorityError::Fenced)?;
471 let current = &published.record;
472 if current
473 .subscription_cleanup_commit
474 .as_ref()
475 .is_some_and(|commit| commit.matches(pending))
476 {
477 return Ok(());
478 }
479 if !current.lease.matches_proof(proof) {
480 return Err(ClusterCheckpointAuthorityError::Fenced);
481 }
482 let registry = read_registry(self.store.as_ref()).await?;
483 if registry.registry.pending_cleanup.as_ref() != Some(pending) {
484 return Err(ClusterCheckpointAuthorityError::Fenced);
485 }
486 let mut lease = current.lease.clone();
487 lease.seq = lease
488 .seq
489 .checked_add(1)
490 .ok_or_else(|| LeaseError::Invalid("leader authority sequence exhausted".into()))?;
491 let mut next = current.preserve_with_lease(lease);
492 next.subscription_cleanup_commit = Some(pending.committed());
493 next.validate()?;
494 match self
495 .create_authority_record(Some(&published), &next)
496 .await?
497 {
498 super::AuthorityCreateOutcome::Created
499 | super::AuthorityCreateOutcome::ExistingIdentical => return Ok(()),
500 super::AuthorityCreateOutcome::Contended(winner)
501 if winner
502 .subscription_cleanup_commit
503 .as_ref()
504 .is_some_and(|commit| commit.matches(pending)) =>
505 {
506 return Ok(());
507 }
508 super::AuthorityCreateOutcome::Contended(winner)
509 if !winner.lease.matches_proof(proof) =>
510 {
511 return Err(ClusterCheckpointAuthorityError::Fenced);
512 }
513 super::AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
514 }
515 }
516 Err(LeaseError::Io(format!(
517 "subscription cleanup authority update exceeded {MAX_CAS_ATTEMPTS} attempts"
518 ))
519 .into())
520 }
521
522 async fn require_subscription_cleanup_leader(
523 &self,
524 proof: &LeaderProof,
525 ) -> Result<(), ClusterCheckpointAuthorityError> {
526 let current = self
527 .load_record()
528 .await?
529 .ok_or(ClusterCheckpointAuthorityError::Fenced)?;
530 if !current.lease.matches_proof(proof) {
531 return Err(ClusterCheckpointAuthorityError::Fenced);
532 }
533 Ok(())
534 }
535
536 async fn mutate_subscription_replay_registry<T>(
537 &self,
538 mut mutation: impl FnMut(&mut ReplayPinRegistry, i64) -> Result<RegistryMutation<T>, LeaseError>,
539 ) -> Result<T, LeaseError> {
540 let mut last_error = None;
541 for _ in 0..MAX_CAS_ATTEMPTS {
542 let mut versioned = read_registry(self.store.as_ref()).await?;
543 let reconciled = self
544 .reconcile_subscription_cleanup(&mut versioned.registry)
545 .await?;
546 let now_ms = now_millis();
547 if now_ms == i64::MAX {
548 return Err(LeaseError::Invalid(
549 "subscription replay pin clock is unavailable".into(),
550 ));
551 }
552 let pruned = versioned.registry.prune_expired(now_ms);
553 let result = mutation(&mut versioned.registry, now_ms)?;
554 if !reconciled && !pruned && !result.changed {
555 return Ok(result.value);
556 }
557 versioned.registry.revision = versioned
558 .registry
559 .revision
560 .checked_add(1)
561 .ok_or_else(|| LeaseError::Invalid("replay pin revision overflowed".into()))?;
562 versioned.registry.validate(false)?;
563 match write_registry(
564 self.store.as_ref(),
565 &versioned.registry,
566 versioned.update_version.as_ref(),
567 )
568 .await
569 {
570 Ok(()) => return Ok(result.value),
571 Err(error) => {
572 last_error = Some(error);
573 tokio::task::yield_now().await;
574 }
575 }
576 }
577 Err(LeaseError::Io(format!(
578 "subscription replay pin update exceeded {MAX_CAS_ATTEMPTS} attempts: {}",
579 last_error.map_or_else(|| "unknown conflict".into(), |error| error.to_string())
580 )))
581 }
582
583 async fn reconcile_subscription_cleanup(
584 &self,
585 registry: &mut ReplayPinRegistry,
586 ) -> Result<bool, LeaseError> {
587 let Some(pending) = registry.pending_cleanup.clone() else {
588 return Ok(false);
589 };
590 let current = self.load_record().await?.ok_or_else(|| {
591 LeaseError::Invalid(
592 "pending subscription cleanup floor lost the leader authority head".into(),
593 )
594 })?;
595 if current
596 .subscription_cleanup_commit
597 .as_ref()
598 .is_some_and(|commit| commit.matches(&pending))
599 {
600 registry.artifact_before_epoch = pending.selected_epoch;
601 registry.pending_cleanup = None;
602 return Ok(true);
603 }
604 if current.lease.matches_proof(&pending.leader_proof) {
605 return Ok(false);
606 }
607 registry.pending_cleanup = None;
608 Ok(true)
609 }
610}
611
612fn replay_pin_expiry(now_ms: i64) -> Result<i64, LeaseError> {
613 now_ms
614 .checked_add(REPLAY_PIN_TTL_MS)
615 .ok_or_else(|| LeaseError::Invalid("subscription replay pin expiry overflowed".into()))
616}
617
618async fn read_registry(
619 store: &dyn object_store::ObjectStore,
620) -> Result<VersionedRegistry, LeaseError> {
621 let result = match store.get(&Path::from(REGISTRY_PATH)).await {
622 Ok(result) => result,
623 Err(object_store::Error::NotFound { .. }) => {
624 return Ok(VersionedRegistry {
625 registry: ReplayPinRegistry::empty(),
626 update_version: None,
627 });
628 }
629 Err(error) => return Err(LeaseError::Io(error.to_string())),
630 };
631 if result.meta.size == 0 || result.meta.size > MAX_REGISTRY_BYTES {
632 return Err(LeaseError::Invalid(format!(
633 "subscription replay pin registry is {} bytes; maximum is {MAX_REGISTRY_BYTES}",
634 result.meta.size
635 )));
636 }
637 let update_version = UpdateVersion {
638 e_tag: result.meta.e_tag.clone(),
639 version: result.meta.version.clone(),
640 };
641 if update_version.e_tag.is_none() && update_version.version.is_none() {
642 return Err(LeaseError::Invalid(
643 "subscription replay pin store omitted its conditional update version".into(),
644 ));
645 }
646 let bytes = result
647 .bytes()
648 .await
649 .map_err(|error| LeaseError::Io(error.to_string()))?;
650 let registry: ReplayPinRegistry = serde_json::from_slice(&bytes)?;
651 registry.validate(true)?;
652 if encode_registry(®istry)?.as_ref() != bytes.as_ref() {
653 return Err(LeaseError::Invalid(
654 "subscription replay pin registry does not use its canonical body".into(),
655 ));
656 }
657 Ok(VersionedRegistry {
658 registry,
659 update_version: Some(update_version),
660 })
661}
662
663async fn write_registry(
664 store: &dyn object_store::ObjectStore,
665 registry: &ReplayPinRegistry,
666 expected: Option<&UpdateVersion>,
667) -> Result<(), LeaseError> {
668 let mode = expected.map_or(PutMode::Create, |version| PutMode::Update(version.clone()));
669 store
670 .put_opts(
671 &Path::from(REGISTRY_PATH),
672 PutPayload::from(encode_registry(registry)?),
673 PutOptions {
674 mode,
675 ..PutOptions::default()
676 },
677 )
678 .await
679 .map(|_| ())
680 .map_err(|error| LeaseError::Io(error.to_string()))
681}
682
683fn encode_registry(registry: &ReplayPinRegistry) -> Result<Bytes, LeaseError> {
684 registry.validate(false)?;
685 let bytes = serde_json::to_vec(registry)?;
686 let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
687 if bytes.is_empty() || length > MAX_REGISTRY_BYTES {
688 return Err(LeaseError::Invalid(format!(
689 "encoded subscription replay pin registry is {length} bytes; maximum is {MAX_REGISTRY_BYTES}"
690 )));
691 }
692 Ok(Bytes::from(bytes))
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::checkpoint::SubscriptionDigest;
699 use crate::cluster::control::{LeaderLeaseOwner, LeaseOutcome};
700 use crate::cluster::discovery::NodeId;
701 use std::time::Instant;
702
703 fn generation(byte: u8) -> StreamGeneration {
704 StreamGeneration::from_digest(SubscriptionDigest::from_bytes([byte; 32]))
705 }
706
707 fn owner(node: u64) -> LeaderLeaseOwner {
708 LeaderLeaseOwner {
709 node: NodeId(node),
710 boot: Uuid::from_u128(u128::from(node)),
711 process_term: 1,
712 }
713 }
714
715 async fn takeover(
716 authority: &LeaderLeaseStore,
717 lease: &super::super::LeaderLease,
718 successor: &LeaderLeaseOwner,
719 ) -> super::super::LeaderLease {
720 let observation = super::super::LeaderLeaseObservation {
721 lease: lease.clone(),
722 started: Instant::now()
723 .checked_sub(Duration::from_millis(2))
724 .unwrap(),
725 };
726 let LeaseOutcome::Acquired(takeover) = authority
727 .try_takeover(successor, &observation, 2)
728 .await
729 .unwrap()
730 else {
731 panic!("elapsed rival observation must permit takeover");
732 };
733 takeover
734 }
735
736 #[tokio::test]
737 async fn active_pin_serializes_with_cleanup_floor() {
738 let authority =
739 LeaderLeaseStore::new(Arc::new(object_store::memory::InMemory::new()), 30_000);
740 let owner = owner(1);
741 let LeaseOutcome::Acquired(lease) = authority.begin_new_term(&owner, 0).await.unwrap()
742 else {
743 panic!("empty authority must grant the leader term");
744 };
745 let SubscriptionReplayPinAcquire::Acquired(pin) = authority
746 .acquire_subscription_replay_pin(generation(1), 5)
747 .await
748 .unwrap()
749 else {
750 panic!("empty replay registry must accept a pin");
751 };
752
753 assert_eq!(
754 authority
755 .reserve_subscription_cleanup_floor(&lease.proof(), 9)
756 .await
757 .unwrap(),
758 5
759 );
760 authority
761 .release_subscription_replay_pin(&pin)
762 .await
763 .unwrap();
764 assert_eq!(
765 authority
766 .reserve_subscription_cleanup_floor(&lease.proof(), 9)
767 .await
768 .unwrap(),
769 9
770 );
771 assert!(matches!(
772 authority
773 .acquire_subscription_replay_pin(generation(2), 8)
774 .await
775 .unwrap(),
776 SubscriptionReplayPinAcquire::Pruned {
777 artifact_before_epoch: 9
778 }
779 ));
780 }
781
782 #[tokio::test]
783 async fn concurrent_gateways_preserve_both_pins() {
784 let authority = Arc::new(LeaderLeaseStore::new(
785 Arc::new(object_store::memory::InMemory::new()),
786 30_000,
787 ));
788 let (left, right) = tokio::join!(
789 authority.acquire_subscription_replay_pin(generation(1), 4),
790 authority.acquire_subscription_replay_pin(generation(2), 7),
791 );
792 assert!(matches!(
793 left.unwrap(),
794 SubscriptionReplayPinAcquire::Acquired(_)
795 ));
796 assert!(matches!(
797 right.unwrap(),
798 SubscriptionReplayPinAcquire::Acquired(_)
799 ));
800
801 let registry = read_registry(authority.store.as_ref()).await.unwrap();
802 assert_eq!(registry.registry.pins.len(), 2);
803 assert_eq!(
804 registry.registry.pins.iter().map(|pin| pin.epoch).min(),
805 Some(4)
806 );
807 }
808
809 #[test]
810 fn legacy_registry_without_a_pending_floor_round_trips_canonically() {
811 let legacy = Bytes::from_static(
812 br#"{"version":1,"revision":1,"artifact_before_epoch":0,"pins":[]}"#,
813 );
814 let registry: ReplayPinRegistry = serde_json::from_slice(&legacy).unwrap();
815 assert!(registry.pending_cleanup.is_none());
816 assert_eq!(encode_registry(®istry).unwrap(), legacy);
817 }
818
819 #[tokio::test]
820 async fn takeover_before_cleanup_marker_discards_the_uncommitted_floor() {
821 let authority = LeaderLeaseStore::new(Arc::new(object_store::memory::InMemory::new()), 1);
822 let incumbent = owner(1);
823 let LeaseOutcome::Acquired(lease) = authority.begin_new_term(&incumbent, 0).await.unwrap()
824 else {
825 panic!("empty authority must grant the leader term");
826 };
827 let CleanupFloorPreparation::Pending(pending) = authority
828 .prepare_subscription_cleanup_floor(&lease.proof(), 9)
829 .await
830 .unwrap()
831 else {
832 panic!("a new cleanup floor must prepare a pending slot");
833 };
834
835 let successor = owner(2);
836 takeover(&authority, &lease, &successor).await;
837 assert!(matches!(
838 authority
839 .commit_subscription_cleanup_floor(&lease.proof(), &pending)
840 .await,
841 Err(ClusterCheckpointAuthorityError::Fenced)
842 ));
843 assert!(matches!(
844 authority
845 .acquire_subscription_replay_pin(generation(2), 8)
846 .await
847 .unwrap(),
848 SubscriptionReplayPinAcquire::Acquired(_)
849 ));
850 let registry = read_registry(authority.store.as_ref()).await.unwrap();
851 assert_eq!(registry.registry.artifact_before_epoch, 0);
852 assert!(registry.registry.pending_cleanup.is_none());
853 }
854
855 #[tokio::test]
856 async fn committed_cleanup_marker_survives_takeover_and_is_helped_to_completion() {
857 let authority = LeaderLeaseStore::new(Arc::new(object_store::memory::InMemory::new()), 1);
858 let incumbent = owner(1);
859 let LeaseOutcome::Acquired(lease) = authority.begin_new_term(&incumbent, 0).await.unwrap()
860 else {
861 panic!("empty authority must grant the leader term");
862 };
863 let CleanupFloorPreparation::Pending(pending) = authority
864 .prepare_subscription_cleanup_floor(&lease.proof(), 9)
865 .await
866 .unwrap()
867 else {
868 panic!("a new cleanup floor must prepare a pending slot");
869 };
870 assert_eq!(
871 authority
872 .acquire_subscription_replay_pin(generation(2), 8)
873 .await
874 .unwrap(),
875 SubscriptionReplayPinAcquire::Contended
876 );
877 authority
878 .commit_subscription_cleanup_floor(&lease.proof(), &pending)
879 .await
880 .unwrap();
881
882 let successor = owner(2);
883 takeover(&authority, &lease, &successor).await;
884 assert!(matches!(
885 authority
886 .acquire_subscription_replay_pin(generation(3), 8)
887 .await
888 .unwrap(),
889 SubscriptionReplayPinAcquire::Pruned {
890 artifact_before_epoch: 9
891 }
892 ));
893 let registry = read_registry(authority.store.as_ref()).await.unwrap();
894 assert_eq!(registry.registry.artifact_before_epoch, 9);
895 assert!(registry.registry.pending_cleanup.is_none());
896 }
897}