Skip to main content

zerodds_durability_store/
contract.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! The retention **contract** — the only thing that bounds retention
5//! (ADR 0009, invariant 1). Derived from `DurabilityServiceQosPolicy`.
6
7use core::time::Duration;
8
9use zerodds_qos::policies::durability_service::DurabilityServiceQosPolicy;
10use zerodds_qos::policies::history::HistoryKind;
11use zerodds_qos::policies::resource_limits::LENGTH_UNLIMITED;
12
13/// Retention contract for a topic, derived from its
14/// `DurabilityServiceQosPolicy`. The cold store MUST enforce exactly this and
15/// nothing tighter (the memory budget is a separate, non-retention knob).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Contract {
18    /// `KEEP_LAST` (ring per instance at `history_depth`) or `KEEP_ALL`
19    /// (bounded only by the `max_*` caps).
20    pub history_kind: HistoryKind,
21    /// `KEEP_LAST` depth per instance (`<= 0` is normalized to 1).
22    pub history_depth: i32,
23    /// Global sample cap (`LENGTH_UNLIMITED` = unbounded).
24    pub max_samples: i32,
25    /// Instance-count cap (`LENGTH_UNLIMITED` = unbounded).
26    pub max_instances: i32,
27    /// Per-instance sample cap under `KEEP_ALL` (`LENGTH_UNLIMITED` = unbounded).
28    pub max_samples_per_instance: i32,
29    /// Delay after an instance is unregistered before its samples are purged.
30    pub cleanup_delay: Duration,
31}
32
33impl Contract {
34    /// Derives the contract from the durability-service QoS policy.
35    #[must_use]
36    pub fn from_qos(qos: &DurabilityServiceQosPolicy) -> Self {
37        Self {
38            history_kind: qos.history_kind,
39            history_depth: qos.history_depth,
40            max_samples: qos.max_samples,
41            max_instances: qos.max_instances,
42            max_samples_per_instance: qos.max_samples_per_instance,
43            cleanup_delay: cleanup_delay(qos),
44        }
45    }
46
47    /// A `KEEP_ALL` contract with all caps unlimited and no cleanup delay — the
48    /// sensible default for a durability service that should retain full
49    /// history (the `DurabilityServiceQosPolicy` default is `KEEP_LAST(1)`,
50    /// which is rarely what a service wants).
51    #[must_use]
52    pub fn keep_all() -> Self {
53        Self {
54            history_kind: HistoryKind::KeepAll,
55            history_depth: 0,
56            max_samples: LENGTH_UNLIMITED,
57            max_instances: LENGTH_UNLIMITED,
58            max_samples_per_instance: LENGTH_UNLIMITED,
59            cleanup_delay: core::time::Duration::ZERO,
60        }
61    }
62
63    /// Effective `KEEP_LAST` depth (`<= 0` normalized to 1, per DDS default).
64    #[must_use]
65    pub fn effective_depth(&self) -> usize {
66        if self.history_depth <= 0 {
67            1
68        } else {
69            self.history_depth as usize
70        }
71    }
72
73    /// `true` if `max_samples` is bounded.
74    #[must_use]
75    pub fn samples_bounded(&self) -> bool {
76        self.max_samples != LENGTH_UNLIMITED
77    }
78
79    /// `true` if `max_instances` is bounded.
80    #[must_use]
81    pub fn instances_bounded(&self) -> bool {
82        self.max_instances != LENGTH_UNLIMITED
83    }
84
85    /// `true` if `max_samples_per_instance` is bounded.
86    #[must_use]
87    pub fn per_instance_bounded(&self) -> bool {
88        self.max_samples_per_instance != LENGTH_UNLIMITED
89    }
90}
91
92impl Default for Contract {
93    /// `KEEP_LAST(1)`, all caps unlimited, zero cleanup delay — matches the
94    /// `DurabilityServiceQosPolicy` default.
95    fn default() -> Self {
96        Self::from_qos(&DurabilityServiceQosPolicy::default())
97    }
98}
99
100/// Service-cleanup-delay (QoS Duration) → `core::time::Duration`. The QoS
101/// fraction is `2^-32 s`, not nanoseconds.
102fn cleanup_delay(qos: &DurabilityServiceQosPolicy) -> Duration {
103    let secs = u64::try_from(qos.service_cleanup_delay.seconds.max(0)).unwrap_or(0);
104    let frac = u64::from(qos.service_cleanup_delay.fraction);
105    let nanos = (frac.saturating_mul(1_000_000_000) >> 32) as u32;
106    Duration::new(secs, nanos)
107}