Skip to main content

laminar_core/cluster/control/leader_lease/
subscription_replay.rs

1//! Bounded durable replay pins for committed cluster subscriptions.
2
3use 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
25/// Interval at which an active gateway must renew a durable replay pin.
26pub const SUBSCRIPTION_REPLAY_PIN_RENEW_INTERVAL: Duration = Duration::from_secs(30);
27
28/// Exact internal lease protecting one stream generation's replay start epoch.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct SubscriptionReplayPin {
31    id: Uuid,
32    stream_generation: StreamGeneration,
33    epoch: u64,
34}
35
36impl SubscriptionReplayPin {
37    /// Stream generation protected by this pin.
38    #[must_use]
39    pub const fn stream_generation(&self) -> StreamGeneration {
40        self.stream_generation
41    }
42
43    /// Oldest committed epoch required by this replay.
44    #[must_use]
45    pub const fn epoch(&self) -> u64 {
46        self.epoch
47    }
48}
49
50/// Result of atomically attaching a replay pin to the shared retention floor.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum SubscriptionReplayPinAcquire {
53    /// The requested epoch is now protected.
54    Acquired(SubscriptionReplayPin),
55    /// Cleanup authority had already made the requested epoch ineligible.
56    Pruned {
57        /// Epochs below this exclusive floor cannot acquire a new pin.
58        artifact_before_epoch: u64,
59    },
60    /// The fixed shared pin roster is full after expired leases were removed.
61    Capacity,
62    /// A leader-fenced cleanup floor is being committed; the caller may retry.
63    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    /// Acquire one bounded durable replay pin before loading an historical checkpoint.
245    ///
246    /// # Errors
247    ///
248    /// Returns an authority error when the identity is non-canonical or the durable
249    /// replay-pin registry cannot be validated or updated.
250    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    /// Renew an exact replay pin. `false` means it expired or was fenced by cleanup.
310    ///
311    /// # Errors
312    ///
313    /// Returns an authority error when the durable replay-pin registry cannot be
314    /// validated or updated.
315    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    /// Release an exact replay pin. Expired or already-released pins are idempotent.
336    ///
337    /// # Errors
338    ///
339    /// Returns an authority error when the durable replay-pin registry cannot be
340    /// validated or updated.
341    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    /// Advance cleanup authority without crossing an unexpired replay pin.
360    ///
361    /// # Errors
362    ///
363    /// Returns an authority error when the leader proof is invalid or stale, or the
364    /// durable replay-pin registry cannot be validated or updated.
365    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            // A pending slot is not a cleanup authority. Reconciliation removes it if this proof
388            // lost the authority CAS, while preserving it if the marker became durable.
389            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) = &registry.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(&registry)?.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(&registry).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}