laminar_core/cluster/control/catalog_manifest/
mod.rs1use std::fmt::Write;
4use std::sync::Arc;
5
6use ahash::AHashSet;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10use crate::checkpoint::LeaderProof;
11
12use super::{LeaderLeaseStore, LeaseError};
13
14pub use crate::catalog::CatalogObjectKind;
15
16pub(super) const CATALOG_MANIFEST_FORMAT_VERSION: u16 = 1;
17pub(super) const MAX_CATALOG_MANIFEST_ENTRIES: usize = 4_096;
18pub(super) const MAX_CATALOG_MANIFEST_BYTES: usize = 8 * 1024 * 1024;
19
20const CATALOG_MANIFEST_PREFIX: &str = "control/catalog-manifest/v1/";
21
22fn validate_entries(entries: &[CatalogManifestEntry]) -> Result<(), CatalogManifestError> {
23 if entries.len() > MAX_CATALOG_MANIFEST_ENTRIES {
24 return Err(CatalogManifestError::Invalid(format!(
25 "catalog manifest has {} entries; maximum is {MAX_CATALOG_MANIFEST_ENTRIES}",
26 entries.len()
27 )));
28 }
29 let mut names = AHashSet::with_capacity(entries.len());
30 for entry in entries {
31 if entry.canonical_name.is_empty() || entry.canonical_name.trim() != entry.canonical_name {
32 return Err(CatalogManifestError::Invalid(format!(
33 "catalog manifest has a non-canonical name {:?}",
34 entry.canonical_name
35 )));
36 }
37 if entry.ddl.trim().is_empty() {
38 return Err(CatalogManifestError::Invalid(format!(
39 "catalog manifest entry '{}' has empty DDL",
40 entry.canonical_name
41 )));
42 }
43 if entry.catalog_generation == 0 {
44 return Err(CatalogManifestError::Invalid(format!(
45 "catalog manifest entry '{}' has a zero object generation",
46 entry.canonical_name
47 )));
48 }
49 if let Some(binding) = &entry.schema_binding {
50 binding
51 .canonical_bytes()
52 .map_err(|error| CatalogManifestError::Invalid(error.to_string()))?;
53 let matches_direction = match binding.direction {
54 crate::schema_binding::SchemaDirection::Source => matches!(
55 entry.kind,
56 CatalogObjectKind::Source
57 | CatalogObjectKind::Table
58 | CatalogObjectKind::LookupTable
59 ),
60 crate::schema_binding::SchemaDirection::Sink => {
61 entry.kind == CatalogObjectKind::Sink
62 }
63 };
64 if !matches_direction {
65 return Err(CatalogManifestError::Invalid(
66 "schema binding has the wrong catalog direction".into(),
67 ));
68 }
69 }
70 if !names.insert(entry.canonical_name.as_str()) {
71 return Err(CatalogManifestError::Invalid(format!(
72 "catalog manifest repeats canonical name '{}'",
73 entry.canonical_name
74 )));
75 }
76 }
77 Ok(())
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct CatalogManifestEntry {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub schema_binding: Option<crate::schema_binding::SchemaBinding>,
87 pub canonical_name: String,
89 pub kind: CatalogObjectKind,
91 #[serde(
93 default = "initial_catalog_generation",
94 skip_serializing_if = "is_initial_catalog_generation"
95 )]
96 pub catalog_generation: u64,
97 pub ddl: String,
99}
100
101const fn initial_catalog_generation() -> u64 {
102 1
103}
104
105const fn is_initial_catalog_generation(generation: &u64) -> bool {
106 *generation == initial_catalog_generation()
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct CatalogManifest {
113 pub entries: Vec<CatalogManifestEntry>,
115}
116
117impl CatalogManifest {
118 pub fn reference(&self) -> Result<CatalogManifestRef, CatalogManifestError> {
123 self.encode_and_reference().map(|(_, reference)| reference)
124 }
125
126 pub fn new(entries: Vec<CatalogManifestEntry>) -> Result<Self, CatalogManifestError> {
132 let manifest = Self { entries };
133 manifest.encode_and_reference()?;
134 Ok(manifest)
135 }
136
137 pub(super) fn validate(&self) -> Result<(), CatalogManifestError> {
138 validate_entries(&self.entries)?;
139 Ok(())
140 }
141
142 pub(super) fn encode_and_reference(
143 &self,
144 ) -> Result<(Vec<u8>, CatalogManifestRef), CatalogManifestError> {
145 self.validate()?;
146 let encoded = if self
149 .entries
150 .iter()
151 .any(|entry| entry.schema_binding.is_some())
152 {
153 let mut value = serde_json::to_value(self)?;
154 value.sort_all_objects();
155 serde_json::to_vec(&value)?
156 } else {
157 serde_json::to_vec(self)?
158 };
159 if encoded.len() > MAX_CATALOG_MANIFEST_BYTES {
160 return Err(CatalogManifestError::Invalid(format!(
161 "encoded catalog manifest is {} bytes; maximum is {MAX_CATALOG_MANIFEST_BYTES}",
162 encoded.len()
163 )));
164 }
165 let encoded_len = u64::try_from(encoded.len()).map_err(|_| {
166 CatalogManifestError::Invalid("encoded catalog manifest length overflow".into())
167 })?;
168 let entry_count = u32::try_from(self.entries.len()).map_err(|_| {
169 CatalogManifestError::Invalid("catalog manifest entry count overflow".into())
170 })?;
171 let digest = Sha256::digest(&encoded);
172 let mut sha256 = String::with_capacity(64);
173 for byte in digest {
174 write!(&mut sha256, "{byte:02x}").expect("writing to a String cannot fail");
175 }
176 let reference = CatalogManifestRef {
177 version: CATALOG_MANIFEST_FORMAT_VERSION,
178 sha256,
179 encoded_len,
180 entry_count,
181 };
182 reference.validate()?;
183 Ok((encoded, reference))
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct CatalogManifestRef {
191 pub version: u16,
193 pub sha256: String,
195 pub encoded_len: u64,
197 pub entry_count: u32,
199}
200
201impl CatalogManifestRef {
202 pub(crate) fn validate(&self) -> Result<(), CatalogManifestError> {
203 if self.version != CATALOG_MANIFEST_FORMAT_VERSION {
204 return Err(CatalogManifestError::Invalid(format!(
205 "unsupported catalog manifest version {}",
206 self.version
207 )));
208 }
209 if self.sha256.len() != 64
210 || !self
211 .sha256
212 .bytes()
213 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
214 {
215 return Err(CatalogManifestError::Invalid(
216 "catalog manifest SHA-256 must be 64 lowercase hexadecimal characters".into(),
217 ));
218 }
219 if self.encoded_len == 0
220 || self.encoded_len
221 > u64::try_from(MAX_CATALOG_MANIFEST_BYTES)
222 .expect("catalog manifest byte limit fits u64")
223 {
224 return Err(CatalogManifestError::Invalid(format!(
225 "catalog manifest encoded length {} is outside 1..={MAX_CATALOG_MANIFEST_BYTES}",
226 self.encoded_len
227 )));
228 }
229 if !matches!(
230 usize::try_from(self.entry_count),
231 Ok(count) if count <= MAX_CATALOG_MANIFEST_ENTRIES
232 ) {
233 return Err(CatalogManifestError::Invalid(format!(
234 "catalog manifest entry count {} exceeds {MAX_CATALOG_MANIFEST_ENTRIES}",
235 self.entry_count
236 )));
237 }
238 Ok(())
239 }
240
241 pub(super) fn object_path(&self) -> object_store::path::Path {
242 object_store::path::Path::from(format!("{CATALOG_MANIFEST_PREFIX}{}.json", self.sha256))
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum CatalogSealOutcome {
249 Created,
251 ExistingIdentical,
253}
254
255pub struct CatalogManifestStore {
257 authority: Arc<LeaderLeaseStore>,
258}
259
260impl std::fmt::Debug for CatalogManifestStore {
261 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 formatter
263 .debug_struct("CatalogManifestStore")
264 .finish_non_exhaustive()
265 }
266}
267
268#[derive(Debug, thiserror::Error)]
270pub enum CatalogManifestError {
271 #[error("leader lease authority: {0}")]
273 Authority(#[from] LeaseError),
274 #[error("JSON: {0}")]
276 Json(#[from] serde_json::Error),
277 #[error("invalid sealed catalog manifest: {0}")]
279 Invalid(String),
280 #[error("cluster catalog is already sealed with a different inventory")]
282 Conflict,
283 #[error("catalog seal was fenced by a different durable leader term")]
285 Fenced,
286}
287
288impl CatalogManifestStore {
289 pub async fn operation_status(
294 &self,
295 operation_id: super::topology::TopologyOperationId,
296 ) -> Result<Option<super::topology::TopologyAdmissionStatus>, super::topology::TopologyError>
297 {
298 self.authority.topology_operation_status(operation_id).await
299 }
300
301 #[must_use]
303 pub fn new(authority: Arc<LeaderLeaseStore>) -> Self {
304 Self { authority }
305 }
306
307 pub async fn topology_state(
312 &self,
313 ) -> Result<super::topology::TopologyCatalogState, super::topology::TopologyError> {
314 self.authority.topology_catalog_state().await
315 }
316
317 pub async fn live_topology_commit(
322 &self,
323 ) -> Result<Option<super::topology::TopologyCommit>, super::topology::TopologyError> {
324 self.authority.live_topology_commit().await
325 }
326
327 pub async fn topology_journal_usage(
332 &self,
333 ) -> Result<super::topology::TopologyJournalUsage, super::topology::TopologyError> {
334 self.authority.topology_journal_usage().await
335 }
336
337 pub async fn committed_topology_subscriptions(
342 &self,
343 ) -> Result<Vec<crate::checkpoint::OutputDistributionCertificate>, super::topology::TopologyError>
344 {
345 self.authority.committed_topology_subscriptions().await
346 }
347
348 pub async fn load_with_topology(
353 &self,
354 ) -> Result<
355 Option<(CatalogManifest, super::topology::TopologyCatalogState)>,
356 super::topology::TopologyError,
357 > {
358 self.authority.catalog_with_topology().await
359 }
360
361 pub async fn parent_topology_catalog(
366 &self,
367 expected_current: &CatalogManifestRef,
368 ) -> Result<Option<CatalogManifest>, super::topology::TopologyError> {
369 self.authority
370 .parent_topology_catalog(expected_current)
371 .await
372 }
373
374 pub async fn original_topology_catalog(
380 &self,
381 expected_current: &CatalogManifestRef,
382 ) -> Result<Option<CatalogManifest>, super::topology::TopologyError> {
383 self.authority
384 .original_topology_catalog(expected_current)
385 .await
386 }
387
388 pub async fn retired_topology_names(
393 &self,
394 ) -> Result<std::collections::BTreeSet<String>, super::topology::TopologyError> {
395 self.authority.retired_topology_names().await
396 }
397
398 pub async fn retired_topology_generations(
403 &self,
404 ) -> Result<std::collections::BTreeMap<String, u64>, super::topology::TopologyError> {
405 self.authority.retired_topology_generations().await
406 }
407
408 pub async fn adopt_legacy_topology(
415 &self,
416 proof: &LeaderProof,
417 operation_id: super::topology::TopologyOperationId,
418 expected_manifest: &CatalogManifestRef,
419 expected_deployment: &str,
420 ) -> Result<super::topology::TopologyAdoptionOutcome, super::topology::TopologyError> {
421 self.authority
422 .adopt_legacy_topology(proof, operation_id, expected_manifest, expected_deployment)
423 .await
424 }
425
426 pub async fn load(&self) -> Result<Option<CatalogManifest>, CatalogManifestError> {
431 self.load_with_topology()
432 .await
433 .map(|snapshot| snapshot.map(|(manifest, _)| manifest))
434 .map_err(|error| match error {
435 super::topology::TopologyError::Authority(error) => {
436 CatalogManifestError::Authority(error)
437 }
438 super::topology::TopologyError::Catalog(error) => error,
439 error => CatalogManifestError::Invalid(error.to_string()),
440 })
441 }
442
443 pub async fn seal(
450 &self,
451 manifest: &CatalogManifest,
452 proof: &LeaderProof,
453 ) -> Result<CatalogSealOutcome, CatalogManifestError> {
454 Box::pin(self.authority.seal_catalog(proof, manifest)).await
455 }
456}
457
458#[cfg(test)]
459mod tests;