Skip to main content

lenso_app_plan/
policy.rs

1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    DEFAULT_EVENT_QUEUE_CAPACITY, DEFAULT_REQUEST_MAX_CONCURRENCY, DEFAULT_REQUEST_QUEUE_CAPACITY,
7    PlanResolutionError,
8};
9
10/// The cardinality of one Plugin's Capability requirement.
11#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum CapabilityCardinality {
14    /// Exactly one provider must be bound.
15    One,
16    /// Zero or one provider may be bound.
17    Optional,
18    /// Zero or more providers may be bound in deterministic order.
19    Many,
20}
21
22/// The transport-independent interaction semantics of one Capability Operation.
23#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum CapabilityOperationKind {
26    /// One request produces one response or Domain Error.
27    Request,
28    /// One open establishes an ordered, bidirectional stream session.
29    Stream,
30    /// One publication is delivered to zero or more subscribers.
31    Event,
32}
33
34/// The bounded admission policy materialized for one request Operation.
35///
36/// `queue_capacity` counts requests waiting for one of the
37/// `max_concurrency` execution slots. A zero queue capacity is valid and
38/// makes admission fail immediately while all execution slots are occupied.
39#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
40pub struct RequestAdmissionPlan {
41    queue_capacity: usize,
42    max_concurrency: usize,
43}
44
45impl RequestAdmissionPlan {
46    /// Creates a bounded request admission policy.
47    pub const fn new(queue_capacity: usize, max_concurrency: usize) -> Self {
48        Self {
49            queue_capacity,
50            max_concurrency,
51        }
52    }
53
54    /// Returns the maximum number of requests waiting for an execution slot.
55    pub const fn queue_capacity(self) -> usize {
56        self.queue_capacity
57    }
58
59    /// Returns the maximum number of requests executing concurrently.
60    pub const fn max_concurrency(self) -> usize {
61        self.max_concurrency
62    }
63
64    pub(super) fn validate(
65        self,
66        capability_id: &str,
67        operation: &str,
68    ) -> Result<(), PlanResolutionError> {
69        if self.max_concurrency == 0 {
70            return Err(PlanResolutionError::InvalidRequestAdmission {
71                capability_id: capability_id.to_owned(),
72                operation: operation.to_owned(),
73                queue_capacity: self.queue_capacity,
74                max_concurrency: self.max_concurrency,
75            });
76        }
77        Ok(())
78    }
79}
80
81impl Default for RequestAdmissionPlan {
82    fn default() -> Self {
83        Self::new(
84            DEFAULT_REQUEST_QUEUE_CAPACITY,
85            DEFAULT_REQUEST_MAX_CONCURRENCY,
86        )
87    }
88}
89
90/// The bounded volatile mailbox policy materialized for one Event binding.
91///
92/// Capacity counts all accepted Events that have not completed handling. Zero
93/// is valid and makes every publication to the binding report exhausted.
94#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
95pub struct EventAdmissionPlan {
96    capacity: usize,
97}
98
99impl EventAdmissionPlan {
100    /// Creates one Event mailbox policy.
101    pub const fn new(capacity: usize) -> Self {
102        Self { capacity }
103    }
104
105    /// Returns the maximum number of accepted Events retained by the binding.
106    pub const fn capacity(self) -> usize {
107        self.capacity
108    }
109}
110
111impl Default for EventAdmissionPlan {
112    fn default() -> Self {
113        Self::new(DEFAULT_EVENT_QUEUE_CAPACITY)
114    }
115}
116
117/// The finite restart mode selected for one Plugin Instance.
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119#[serde(rename_all = "snake_case")]
120pub enum RestartMode {
121    /// Do not recreate a failed generation.
122    Never,
123    /// Recreate failed generations within a bounded attempt window.
124    OnFailure,
125}
126
127/// Bounded supervision settings materialized in the Resolved App Plan.
128#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
129pub struct RestartPolicy {
130    mode: RestartMode,
131    max_attempts: usize,
132    window: Duration,
133    backoff: Duration,
134    jitter: Duration,
135    stability: Duration,
136}
137
138impl RestartPolicy {
139    /// Creates a policy that never recreates a failed generation.
140    pub const fn never() -> Self {
141        Self {
142            mode: RestartMode::Never,
143            max_attempts: 0,
144            window: Duration::ZERO,
145            backoff: Duration::ZERO,
146            jitter: Duration::ZERO,
147            stability: Duration::ZERO,
148        }
149    }
150
151    /// Creates a finite on-failure policy.
152    pub const fn on_failure(
153        max_attempts: usize,
154        window: Duration,
155        backoff: Duration,
156        jitter: Duration,
157        stability: Duration,
158    ) -> Self {
159        Self {
160            mode: RestartMode::OnFailure,
161            max_attempts,
162            window,
163            backoff,
164            jitter,
165            stability,
166        }
167    }
168
169    /// Returns the selected restart mode.
170    pub const fn mode(self) -> RestartMode {
171        self.mode
172    }
173
174    /// Returns the maximum number of recreation attempts in one window.
175    pub const fn max_attempts(self) -> usize {
176        self.max_attempts
177    }
178
179    /// Returns the rolling attempt window.
180    pub const fn window(self) -> Duration {
181        self.window
182    }
183
184    /// Returns the initial exponential backoff duration.
185    pub const fn backoff(self) -> Duration {
186        self.backoff
187    }
188
189    /// Returns the maximum jitter requested from the Runtime Driver.
190    pub const fn jitter(self) -> Duration {
191        self.jitter
192    }
193
194    /// Returns the ready period after which the attempt budget becomes stable again.
195    pub const fn stability(self) -> Duration {
196        self.stability
197    }
198
199    pub(super) fn validate(&self, instance_key: &str) -> Result<(), PlanResolutionError> {
200        if self.mode == RestartMode::OnFailure && (self.max_attempts == 0 || self.window.is_zero())
201        {
202            return Err(PlanResolutionError::InvalidRestartPolicy {
203                instance_key: instance_key.to_owned(),
204                max_attempts: self.max_attempts,
205                window: self.window,
206            });
207        }
208        Ok(())
209    }
210}
211
212impl Default for RestartPolicy {
213    fn default() -> Self {
214        Self::never()
215    }
216}
217
218/// Whether a failed Plugin Instance is allowed to remain unavailable.
219#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
220#[serde(rename_all = "snake_case")]
221pub enum PluginCriticality {
222    /// Exhaustion leaves this Plugin unavailable when it is not required by a `one` binding.
223    #[default]
224    NonCritical,
225    /// Exhaustion fails the App even when no `one` binding reaches this Plugin.
226    Critical,
227}
228
229impl PluginCriticality {
230    /// Returns whether this criticality requires a terminal App outcome on exhaustion.
231    pub const fn is_critical(self) -> bool {
232        matches!(self, Self::Critical)
233    }
234}