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_or_create_deployment_id(&self) -> Result<String, DecisionError> {
95 if let Some(identity) = self.deployment_id.get() {
96 return Ok(identity.clone());
97 }
98 let _guard = self.metadata_write_lock.lock().await;
99 if let Some(identity) = self.deployment_id.get() {
100 return Ok(identity.clone());
101 }
102 if let Some(stored) = self.read_deployment_identity().await? {
103 let identity = stored.head.id.clone();
104 self.cache_checkpoint_id_head(Some(stored));
105 let _ = self.deployment_id.set(identity.clone());
106 return Ok(identity);
107 }
108
109 let identity = DeploymentIdentity {
110 version: DEPLOYMENT_IDENTITY_VERSION,
111 id: uuid::Uuid::now_v7().to_string(),
112 allocator_mode: self.update_mode,
113 checkpoint_id: 0,
114 allocation_id: uuid::Uuid::now_v7().to_string(),
115 };
116 let payload = Self::encode_control_record(
117 "deployment identity",
118 &identity,
119 DEPLOYMENT_IDENTITY_MAX_BYTES,
120 )?;
121 let options = PutOptions {
122 mode: PutMode::Create,
123 ..PutOptions::default()
124 };
125 match self
126 .store
127 .put_opts(
128 &Self::deployment_identity_path(),
129 PutPayload::from(payload),
130 options,
131 )
132 .await
133 {
134 Ok(put_result) => {
135 let update_version: UpdateVersion = put_result.into();
136 if self.update_mode == DecisionStoreUpdateMode::NativeCas
137 && update_version.e_tag.is_none()
138 && update_version.version.is_none()
139 {
140 self.cache_checkpoint_id_head(None);
141 } else {
142 self.cache_checkpoint_id_head(Some(VersionedCheckpointIdHead {
143 head: identity.clone(),
144 update_version,
145 }));
146 }
147 let _ = self.deployment_id.set(identity.id.clone());
148 Ok(identity.id)
149 }
150 Err(
151 object_store::Error::Precondition { .. }
152 | object_store::Error::AlreadyExists { .. },
153 ) => {
154 let stored = self.read_deployment_identity().await?.ok_or_else(|| {
155 DecisionError::Conflict(
156 "deployment identity disappeared after create conflict".into(),
157 )
158 })?;
159 let identity = stored.head.id.clone();
160 self.cache_checkpoint_id_head(Some(stored));
161 let _ = self.deployment_id.set(identity.clone());
162 Ok(identity)
163 }
164 Err(error) => match self.read_deployment_identity().await? {
165 Some(stored) => {
166 let identity = stored.head.id.clone();
167 self.cache_checkpoint_id_head(Some(stored));
168 let _ = self.deployment_id.set(identity.clone());
169 Ok(identity)
170 }
171 None => Err(DecisionError::Io(error.to_string())),
172 },
173 }
174 }
175}