laminar_core/checkpoint_decision/
deployment.rs1use super::{
4 CheckpointDecisionStore, DecisionError, DecisionStoreUpdateMode, DeploymentIdentity,
5 ObjectStore, PutMode, PutOptions, PutPayload, UpdateVersion, VersionedCheckpointIdHead,
6 DEPLOYMENT_IDENTITY_MAX_BYTES, DEPLOYMENT_IDENTITY_VERSION,
7};
8
9impl CheckpointDecisionStore {
10 fn validate_deployment_identity(
11 identity: &DeploymentIdentity,
12 ) -> Result<String, DecisionError> {
13 if identity.version != DEPLOYMENT_IDENTITY_VERSION {
14 return Err(DecisionError::Conflict(format!(
15 "deployment identity version {} is unsupported (expected \
16 {DEPLOYMENT_IDENTITY_VERSION})",
17 identity.version
18 )));
19 }
20 let parsed = uuid::Uuid::parse_str(&identity.id).map_err(|error| {
21 DecisionError::Conflict(format!("deployment identity is not a UUID: {error}"))
22 })?;
23 let canonical = parsed.to_string();
24 if canonical != identity.id || parsed.is_nil() {
25 return Err(DecisionError::Conflict(
26 "deployment identity must be a canonical non-nil UUID".into(),
27 ));
28 }
29 let allocation_id = uuid::Uuid::parse_str(&identity.allocation_id).map_err(|error| {
30 DecisionError::Conflict(format!(
31 "deployment identity has invalid allocation identity: {error}"
32 ))
33 })?;
34 if allocation_id.is_nil() || allocation_id.to_string() != identity.allocation_id {
35 return Err(DecisionError::Conflict(
36 "deployment identity must use a canonical non-nil allocation identity".into(),
37 ));
38 }
39 Ok(canonical)
40 }
41
42 pub(super) async fn read_deployment_identity(
43 &self,
44 ) -> Result<Option<VersionedCheckpointIdHead>, DecisionError> {
45 let Some(result) = self
46 .get_control_record(
47 &Self::deployment_identity_path(),
48 "deployment identity",
49 DEPLOYMENT_IDENTITY_MAX_BYTES,
50 )
51 .await?
52 else {
53 return Ok(None);
54 };
55 let update_version = UpdateVersion {
56 e_tag: result.meta.e_tag.clone(),
57 version: result.meta.version.clone(),
58 };
59 self.require_native_cas_token("deployment identity", &update_version)?;
60 let bytes = Self::read_control_record_bytes(
61 result,
62 "deployment identity",
63 DEPLOYMENT_IDENTITY_MAX_BYTES,
64 None,
65 )
66 .await?;
67 let identity: DeploymentIdentity = serde_json::from_slice(&bytes)
68 .map_err(|error| DecisionError::Conflict(format!("deployment identity: {error}")))?;
69 Self::validate_deployment_identity(&identity)?;
70 if identity.allocator_mode != self.update_mode {
71 return Err(DecisionError::Conflict(format!(
72 "deployment identity allocator mode {:?} cannot be opened as {:?}",
73 identity.allocator_mode, self.update_mode
74 )));
75 }
76 let canonical = serde_json::to_vec(&identity)
77 .map_err(|error| DecisionError::Conflict(error.to_string()))?;
78 if canonical.as_slice() != bytes.as_ref() {
79 return Err(DecisionError::Conflict(
80 "deployment identity does not use its canonical body".into(),
81 ));
82 }
83 Ok(Some(VersionedCheckpointIdHead {
84 head: identity,
85 update_version,
86 }))
87 }
88
89 pub async fn load_deployment_id(&self) -> Result<Option<String>, DecisionError> {
94 self.read_deployment_identity()
95 .await
96 .map(|identity| identity.map(|stored| stored.head.id))
97 }
98
99 pub async fn load_or_create_deployment_id(&self) -> Result<String, DecisionError> {
105 if let Some(identity) = self.deployment_id.get() {
106 return Ok(identity.clone());
107 }
108 let _guard = self.metadata_write_lock.lock().await;
109 if let Some(identity) = self.deployment_id.get() {
110 return Ok(identity.clone());
111 }
112 if let Some(stored) = self.read_deployment_identity().await? {
113 let identity = stored.head.id.clone();
114 self.cache_checkpoint_id_head(Some(stored));
115 let _ = self.deployment_id.set(identity.clone());
116 return Ok(identity);
117 }
118
119 let identity = DeploymentIdentity {
120 version: DEPLOYMENT_IDENTITY_VERSION,
121 id: uuid::Uuid::now_v7().to_string(),
122 allocator_mode: self.update_mode,
123 checkpoint_id: 0,
124 allocation_id: uuid::Uuid::now_v7().to_string(),
125 };
126 let payload = Self::encode_control_record(
127 "deployment identity",
128 &identity,
129 DEPLOYMENT_IDENTITY_MAX_BYTES,
130 )?;
131 let options = PutOptions {
132 mode: PutMode::Create,
133 ..PutOptions::default()
134 };
135 match self
136 .store
137 .put_opts(
138 &Self::deployment_identity_path(),
139 PutPayload::from(payload),
140 options,
141 )
142 .await
143 {
144 Ok(put_result) => {
145 let update_version: UpdateVersion = put_result.into();
146 if self.update_mode == DecisionStoreUpdateMode::NativeCas
147 && update_version.e_tag.is_none()
148 && update_version.version.is_none()
149 {
150 self.cache_checkpoint_id_head(None);
151 } else {
152 self.cache_checkpoint_id_head(Some(VersionedCheckpointIdHead {
153 head: identity.clone(),
154 update_version,
155 }));
156 }
157 let _ = self.deployment_id.set(identity.id.clone());
158 Ok(identity.id)
159 }
160 Err(
161 object_store::Error::Precondition { .. }
162 | object_store::Error::AlreadyExists { .. },
163 ) => {
164 let stored = self.read_deployment_identity().await?.ok_or_else(|| {
165 DecisionError::Conflict(
166 "deployment identity disappeared after create conflict".into(),
167 )
168 })?;
169 let identity = stored.head.id.clone();
170 self.cache_checkpoint_id_head(Some(stored));
171 let _ = self.deployment_id.set(identity.clone());
172 Ok(identity)
173 }
174 Err(error) => match self.read_deployment_identity().await? {
175 Some(stored) => {
176 let identity = stored.head.id.clone();
177 self.cache_checkpoint_id_head(Some(stored));
178 let _ = self.deployment_id.set(identity.clone());
179 Ok(identity)
180 }
181 None => Err(DecisionError::Io(error.to_string())),
182 },
183 }
184 }
185}