laminar_core/checkpoint_decision/
allocation.rs1use super::{
4 Bytes, CheckpointDecisionStore, DecisionError, DecisionStoreUpdateMode, DeploymentIdentity,
5 ObjectStore, PutMode, PutOptions, PutPayload, UpdateVersion, VersionedCheckpointIdHead,
6 DEPLOYMENT_IDENTITY_MAX_BYTES, DEPLOYMENT_IDENTITY_VERSION, LOCAL_RESERVATION_SIZE,
7};
8
9impl CheckpointDecisionStore {
10 fn consume_local_reservation(&self, minimum: u64) -> Option<u64> {
11 let mut reservation = self.local_reservation.lock();
12 let checkpoint_id = reservation.next_id?.max(minimum);
13 if checkpoint_id > reservation.end {
14 reservation.next_id = None;
15 return None;
16 }
17 reservation.next_id = checkpoint_id
18 .checked_add(1)
19 .filter(|next| *next <= reservation.end);
20 Some(checkpoint_id)
21 }
22
23 fn install_local_reservation(&self, first: u64, end: u64) {
24 let mut reservation = self.local_reservation.lock();
25 reservation.end = end;
26 reservation.next_id = first.checked_add(1).filter(|next| *next <= end);
27 }
28
29 async fn allocate_local_checkpoint_id_at_least(
30 &self,
31 deployment_id: &str,
32 minimum: u64,
33 ) -> Result<u64, DecisionError> {
34 if let Some(checkpoint_id) = self.consume_local_reservation(minimum) {
35 return Ok(checkpoint_id);
36 }
37 let lock = self.local_metadata_rmw_lock.as_ref().ok_or_else(|| {
38 DecisionError::Conflict(
39 "local decision store is missing its namespace write lock".into(),
40 )
41 })?;
42 let _guard = lock.lock().await;
43 let current = self
44 .read_checkpoint_id_head(deployment_id)
45 .await?
46 .ok_or_else(|| {
47 DecisionError::Conflict(format!(
48 "checkpoint ID authority for deployment {deployment_id} disappeared"
49 ))
50 })?;
51 let checkpoint_id = current
52 .head
53 .checkpoint_id
54 .checked_add(1)
55 .map(|next| next.max(minimum))
56 .ok_or_else(|| DecisionError::Conflict("checkpoint ID space exhausted u64".into()))?;
57 let reservation_end = checkpoint_id.saturating_add(LOCAL_RESERVATION_SIZE - 1);
58 let head = DeploymentIdentity {
59 version: DEPLOYMENT_IDENTITY_VERSION,
60 id: deployment_id.to_owned(),
61 allocator_mode: self.update_mode,
62 checkpoint_id: reservation_end,
63 allocation_id: uuid::Uuid::now_v7().to_string(),
64 };
65 let payload = Self::encode_control_record(
66 "deployment identity",
67 &head,
68 DEPLOYMENT_IDENTITY_MAX_BYTES,
69 )?;
70 let result = self
71 .store
72 .put_opts(
73 &Self::deployment_identity_path(),
74 PutPayload::from(payload),
75 PutOptions {
76 mode: PutMode::Overwrite,
77 ..PutOptions::default()
78 },
79 )
80 .await;
81 match result {
82 Ok(put_result) => {
83 self.cache_checkpoint_id_head(Some(VersionedCheckpointIdHead {
84 head,
85 update_version: put_result.into(),
86 }));
87 self.install_local_reservation(checkpoint_id, reservation_end);
88 Ok(checkpoint_id)
89 }
90 Err(error) => {
91 let observed = self.read_checkpoint_id_head(deployment_id).await?;
92 if observed.as_ref().is_some_and(|value| value.head == head) {
93 self.cache_checkpoint_id_head(observed);
94 self.install_local_reservation(checkpoint_id, reservation_end);
95 Ok(checkpoint_id)
96 } else {
97 self.cache_checkpoint_id_head(observed);
98 Err(DecisionError::Io(error.to_string()))
99 }
100 }
101 }
102 }
103
104 async fn read_checkpoint_id_head(
105 &self,
106 deployment_id: &str,
107 ) -> Result<Option<VersionedCheckpointIdHead>, DecisionError> {
108 let observed = self.read_deployment_identity().await?;
109 if let Some(observed) = observed.as_ref() {
110 if observed.head.id != deployment_id {
111 return Err(DecisionError::Conflict(format!(
112 "checkpoint ID head belongs to deployment {}, current deployment is {deployment_id}",
113 observed.head.id
114 )));
115 }
116 }
117 Ok(observed)
118 }
119
120 pub(super) fn cache_checkpoint_id_head(&self, head: Option<VersionedCheckpointIdHead>) {
121 *self.checkpoint_id_head.lock() = head;
122 }
123
124 fn validate_checkpoint_id_head_progress(
125 prior: Option<&VersionedCheckpointIdHead>,
126 observed: Option<&VersionedCheckpointIdHead>,
127 ) -> Result<(), DecisionError> {
128 match (prior, observed) {
129 (Some(prior), None) => Err(DecisionError::Conflict(format!(
130 "checkpoint ID head disappeared after durable ID {}",
131 prior.head.checkpoint_id
132 ))),
133 (Some(prior), Some(observed))
134 if observed.head.checkpoint_id < prior.head.checkpoint_id =>
135 {
136 Err(DecisionError::Conflict(format!(
137 "checkpoint ID head regressed from {} to {}",
138 prior.head.checkpoint_id, observed.head.checkpoint_id
139 )))
140 }
141 (Some(prior), Some(observed))
142 if observed.head.checkpoint_id == prior.head.checkpoint_id
143 && observed.head != prior.head =>
144 {
145 Err(DecisionError::Conflict(format!(
146 "checkpoint ID {} changed allocation identity without advancing",
147 prior.head.checkpoint_id
148 )))
149 }
150 _ => Ok(()),
151 }
152 }
153
154 async fn allocate_shared_checkpoint_id_at_least(
155 &self,
156 deployment_id: &str,
157 minimum: u64,
158 ) -> Result<u64, DecisionError> {
159 let allocation_id = uuid::Uuid::now_v7().to_string();
160 let mut current = self.checkpoint_id_head.lock().clone();
161 if current.is_none() {
162 current = self.read_checkpoint_id_head(deployment_id).await?;
163 if current.is_none() {
164 return Err(DecisionError::Conflict(format!(
165 "checkpoint ID authority for deployment {deployment_id} disappeared"
166 )));
167 }
168 self.cache_checkpoint_id_head(current.clone());
169 }
170
171 loop {
172 let versioned = current.as_ref().ok_or_else(|| {
173 DecisionError::Conflict(format!(
174 "checkpoint ID authority for deployment {deployment_id} disappeared"
175 ))
176 })?;
177 let checkpoint_id = versioned
178 .head
179 .checkpoint_id
180 .checked_add(1)
181 .map(|next| next.max(minimum))
182 .ok_or_else(|| {
183 DecisionError::Conflict("checkpoint ID space exhausted u64".to_owned())
184 })?;
185 let head = DeploymentIdentity {
186 version: DEPLOYMENT_IDENTITY_VERSION,
187 id: deployment_id.to_owned(),
188 allocator_mode: self.update_mode,
189 checkpoint_id,
190 allocation_id: allocation_id.clone(),
191 };
192 let payload = serde_json::to_vec(&head)
193 .map(Bytes::from)
194 .map_err(|error| DecisionError::Conflict(error.to_string()))?;
195 let mode = PutMode::Update(versioned.update_version.clone());
196 let result = self
197 .store
198 .put_opts(
199 &Self::deployment_identity_path(),
200 PutPayload::from(payload),
201 PutOptions {
202 mode,
203 ..PutOptions::default()
204 },
205 )
206 .await;
207 match result {
208 Ok(put_result) => {
209 let update_version: UpdateVersion = put_result.into();
210 if update_version.e_tag.is_none() && update_version.version.is_none() {
211 self.cache_checkpoint_id_head(None);
214 } else {
215 self.cache_checkpoint_id_head(Some(VersionedCheckpointIdHead {
216 head,
217 update_version,
218 }));
219 }
220 return Ok(checkpoint_id);
221 }
222 Err(
223 object_store::Error::Precondition { .. }
224 | object_store::Error::AlreadyExists { .. }
225 | object_store::Error::NotFound { .. },
226 ) => {
227 let observed = self.read_checkpoint_id_head(deployment_id).await?;
228 Self::validate_checkpoint_id_head_progress(
229 current.as_ref(),
230 observed.as_ref(),
231 )?;
232 current = observed;
233 self.cache_checkpoint_id_head(current.clone());
234 tokio::task::yield_now().await;
235 }
236 Err(error) => {
237 let observed = self.read_checkpoint_id_head(deployment_id).await?;
238 Self::validate_checkpoint_id_head_progress(
239 current.as_ref(),
240 observed.as_ref(),
241 )?;
242 if observed.as_ref().is_some_and(|value| value.head == head) {
243 self.cache_checkpoint_id_head(observed);
244 return Ok(checkpoint_id);
245 }
246 if observed
247 .as_ref()
248 .is_some_and(|value| value.head.checkpoint_id >= checkpoint_id)
249 {
250 current = observed;
251 self.cache_checkpoint_id_head(current.clone());
252 tokio::task::yield_now().await;
253 continue;
254 }
255 self.cache_checkpoint_id_head(observed);
256 return Err(DecisionError::Io(error.to_string()));
257 }
258 }
259 }
260 }
261
262 pub async fn allocate_checkpoint_id_at_least(
272 &self,
273 minimum: u64,
274 ) -> Result<u64, DecisionError> {
275 if minimum == 0 {
276 return Err(DecisionError::Conflict(
277 "minimum checkpoint ID must be nonzero".to_owned(),
278 ));
279 }
280
281 let deployment_id = self.load_or_create_deployment_id().await?;
282 let _guard = self.metadata_write_lock.lock().await;
283 match self.update_mode {
284 DecisionStoreUpdateMode::NativeCas => {
285 self.allocate_shared_checkpoint_id_at_least(&deployment_id, minimum)
286 .await
287 }
288 DecisionStoreUpdateMode::LocalSingleWriter => {
289 self.allocate_local_checkpoint_id_at_least(&deployment_id, minimum)
290 .await
291 }
292 }
293 }
294
295 #[cfg(test)]
296 pub(super) async fn checkpoint_id_reservation_high_watermark(
297 &self,
298 ) -> Result<u64, DecisionError> {
299 let deployment_id = self.load_or_create_deployment_id().await?;
300 self.read_checkpoint_id_head(&deployment_id)
301 .await?
302 .map(|head| head.head.checkpoint_id)
303 .ok_or_else(|| {
304 DecisionError::Conflict(format!(
305 "checkpoint ID authority for deployment {deployment_id} disappeared"
306 ))
307 })
308 }
309
310 #[cfg(test)]
311 pub async fn allocate_checkpoint_id(&self) -> Result<u64, DecisionError> {
317 self.allocate_checkpoint_id_at_least(1).await
318 }
319}