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}