1use crate::{
10 AsyncAuthorityStore, AsyncObjectStore, ForkOptions, GenerationId, IdempotencyKey, Workspace,
11 WorkspaceError, WorkspaceId,
12};
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, BTreeSet};
15use std::future::Future;
16use std::sync::Mutex;
17use thiserror::Error;
18
19const LINEAGE_VERSION: u32 = 1;
20const MAXIMUM_LINEAGE_CAS_ATTEMPTS: usize = 16;
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
24pub struct WorkspaceLineageRecord {
25 pub version: u32,
27 pub revision: u64,
29 pub ready: bool,
31 pub workspace_id: WorkspaceId,
33 pub workspace_name: String,
35 pub parent_workspace_id: Option<WorkspaceId>,
37 pub parent_workspace_name: Option<String>,
39 pub fork_generation: GenerationId,
41 pub initial_generation: GenerationId,
43}
44
45fn lineage_shape_is_valid(record: &WorkspaceLineageRecord) -> bool {
46 let parent_shape = match (
47 record.parent_workspace_id,
48 record.parent_workspace_name.as_deref(),
49 ) {
50 (None, None) => record.fork_generation == record.initial_generation,
51 (Some(parent_id), Some(parent_name)) => {
52 parent_id != record.workspace_id && crate::WorkspaceName::new(parent_name).is_ok()
53 }
54 _ => false,
55 };
56 record.version == LINEAGE_VERSION
57 && record.revision > 0
58 && crate::WorkspaceName::new(&record.workspace_name).is_ok()
59 && parent_shape
60}
61
62pub trait WorkspaceLineageStore: Send + Sync {
64 type Error: std::error::Error + Send + Sync + 'static;
66
67 fn load(
69 &self,
70 workspace_id: WorkspaceId,
71 ) -> impl Future<Output = Result<Option<WorkspaceLineageRecord>, Self::Error>> + Send;
72
73 fn compare_and_swap(
75 &self,
76 workspace_id: WorkspaceId,
77 expected_revision: u64,
78 replacement: WorkspaceLineageRecord,
79 ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
80}
81
82#[derive(Debug, Error)]
84pub enum WorkspaceLineageError<E: std::error::Error + 'static> {
85 #[error("workspace-lineage store failed: {0}")]
87 Store(E),
88 #[error(transparent)]
90 Workspace(#[from] WorkspaceError),
91 #[error("workspace-lineage state is incompatible")]
93 IncompatibleState,
94 #[error("workspace lineage conflicts with its durable registration")]
96 ConflictingRegistration,
97 #[error("workspace lineage registration remained contended")]
99 Contended,
100 #[error("only the direct parent may receive workspace publication")]
102 UnauthorizedJoin,
103 #[error("workspace lineage traversal exceeded its safe bound")]
105 TraversalLimit,
106}
107
108pub struct WorkspaceGraph<S> {
110 store: S,
111}
112
113impl<S> WorkspaceGraph<S> {
114 #[must_use]
116 pub const fn new(store: S) -> Self {
117 Self { store }
118 }
119
120 #[must_use]
122 pub const fn store(&self) -> &S {
123 &self.store
124 }
125}
126
127impl<S: WorkspaceLineageStore> WorkspaceGraph<S> {
128 pub async fn register_root<A: AsyncAuthorityStore, O: AsyncObjectStore>(
130 &self,
131 workspace: &Workspace<A, O>,
132 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
133 if let Some(existing) = self
134 .store
135 .load(workspace.id())
136 .await
137 .map_err(WorkspaceLineageError::Store)?
138 {
139 if lineage_shape_is_valid(&existing)
140 && existing.ready
141 && existing.workspace_id == workspace.id()
142 && existing.workspace_name == workspace.name().as_str()
143 && existing.parent_workspace_id.is_none()
144 && existing.parent_workspace_name.is_none()
145 {
146 return Ok(existing);
147 }
148 return Err(WorkspaceLineageError::ConflictingRegistration);
149 }
150 let generation = workspace.head().await?;
151 self.register(WorkspaceLineageRecord {
152 version: LINEAGE_VERSION,
153 revision: 1,
154 ready: true,
155 workspace_id: workspace.id(),
156 workspace_name: workspace.name().as_str().to_owned(),
157 parent_workspace_id: None,
158 parent_workspace_name: None,
159 fork_generation: generation.id(),
160 initial_generation: generation.id(),
161 })
162 .await
163 }
164
165 pub async fn fork<A: AsyncAuthorityStore, O: AsyncObjectStore>(
167 &self,
168 parent: &Workspace<A, O>,
169 destination: impl AsRef<str>,
170 idempotency_key: IdempotencyKey,
171 ) -> Result<Workspace<A, O>, WorkspaceLineageError<S::Error>> {
172 let destination =
173 crate::WorkspaceName::new(destination.as_ref()).map_err(WorkspaceError::from)?;
174 let generation = parent.head().await?;
175 let child_id = parent
176 .fork_workspace_id(destination.as_str())
177 .map_err(WorkspaceError::from)?;
178 let planned = WorkspaceLineageRecord {
179 version: LINEAGE_VERSION,
180 revision: 1,
181 ready: false,
182 workspace_id: child_id,
183 workspace_name: destination.as_str().to_owned(),
184 parent_workspace_id: Some(parent.id()),
185 parent_workspace_name: Some(parent.name().as_str().to_owned()),
186 fork_generation: generation.id(),
187 initial_generation: generation.id(),
188 };
189 let planned = self.register(planned).await?;
190 let child = parent
191 .fork(
192 destination.as_str(),
193 ForkOptions::from_generation(generation.clone(), idempotency_key),
194 )
195 .await?;
196 let initial_generation = child.head().await?;
197 if child.id() != child_id {
198 return Err(WorkspaceLineageError::ConflictingRegistration);
199 }
200 if planned.ready && planned.initial_generation != initial_generation.id() {
201 return Err(WorkspaceLineageError::ConflictingRegistration);
202 }
203 let mut ready = planned;
204 ready.ready = true;
205 ready.initial_generation = initial_generation.id();
206 self.promote_ready(ready).await?;
207 Ok(child)
208 }
209
210 pub async fn register_existing_child<A: AsyncAuthorityStore, O: AsyncObjectStore>(
216 &self,
217 parent: &Workspace<A, O>,
218 child: &Workspace<A, O>,
219 fork_generation: crate::GenerationId,
220 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
221 let initial_generation = child.head().await?.id();
222 self.register_existing_child_at_generation(
223 parent,
224 child,
225 fork_generation,
226 initial_generation,
227 )
228 .await
229 }
230
231 pub async fn register_existing_child_at_generation<
234 A: AsyncAuthorityStore,
235 O: AsyncObjectStore,
236 >(
237 &self,
238 parent: &Workspace<A, O>,
239 child: &Workspace<A, O>,
240 fork_generation: crate::GenerationId,
241 initial_generation: crate::GenerationId,
242 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
243 parent.generation(fork_generation).await?;
247 let initial = child.generation(initial_generation).await?;
248 let expected_child = parent
249 .fork_workspace_id(child.name().as_str())
250 .map_err(WorkspaceError::from)?;
251 if child.id() != expected_child {
252 return Err(WorkspaceLineageError::UnauthorizedJoin);
253 }
254 if initial.parents().await?.as_slice() != [fork_generation] {
255 return Err(WorkspaceLineageError::UnauthorizedJoin);
256 }
257 self.register(WorkspaceLineageRecord {
258 version: LINEAGE_VERSION,
259 revision: 1,
260 ready: true,
261 workspace_id: child.id(),
262 workspace_name: child.name().as_str().to_owned(),
263 parent_workspace_id: Some(parent.id()),
264 parent_workspace_name: Some(parent.name().as_str().to_owned()),
265 fork_generation,
266 initial_generation,
267 })
268 .await
269 }
270
271 pub async fn authorize_join(
273 &self,
274 child: WorkspaceId,
275 parent: WorkspaceId,
276 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
277 let record = self.required(child).await?;
278 if record.parent_workspace_id != Some(parent) {
279 return Err(WorkspaceLineageError::UnauthorizedJoin);
280 }
281 Ok(record)
282 }
283
284 pub async fn resolve(
289 &self,
290 workspace: WorkspaceId,
291 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
292 self.required(workspace).await
293 }
294
295 pub async fn ancestors(
297 &self,
298 workspace: WorkspaceId,
299 maximum: u32,
300 ) -> Result<Vec<WorkspaceLineageRecord>, WorkspaceLineageError<S::Error>> {
301 let mut next = Some(workspace);
302 let mut seen = BTreeSet::new();
303 let mut records = Vec::new();
304 while let Some(id) = next {
305 if !seen.insert(id) || records.len() >= maximum as usize {
306 return Err(WorkspaceLineageError::TraversalLimit);
307 }
308 let record = self.required(id).await?;
309 next = record.parent_workspace_id;
310 if id != workspace {
311 records.push(record);
312 }
313 }
314 Ok(records)
315 }
316
317 async fn required(
318 &self,
319 workspace_id: WorkspaceId,
320 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
321 let record = self
322 .store
323 .load(workspace_id)
324 .await
325 .map_err(WorkspaceLineageError::Store)?
326 .ok_or(WorkspaceLineageError::IncompatibleState)?;
327 if record.workspace_id != workspace_id || !lineage_shape_is_valid(&record) {
328 return Err(WorkspaceLineageError::IncompatibleState);
329 }
330 if !record.ready {
331 return Err(WorkspaceLineageError::IncompatibleState);
332 }
333 Ok(record)
334 }
335
336 async fn register(
337 &self,
338 record: WorkspaceLineageRecord,
339 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
340 if !lineage_shape_is_valid(&record) {
341 return Err(WorkspaceLineageError::IncompatibleState);
342 }
343 if let Some(existing) = self
344 .store
345 .load(record.workspace_id)
346 .await
347 .map_err(WorkspaceLineageError::Store)?
348 {
349 if lineage_shape_is_valid(&existing) && same_lineage(&existing, &record) {
350 if record.ready && !existing.ready {
351 return self.promote_ready(record).await;
352 }
353 return Ok(existing);
354 }
355 return Err(WorkspaceLineageError::ConflictingRegistration);
356 }
357 if self
358 .store
359 .compare_and_swap(record.workspace_id, 0, record.clone())
360 .await
361 .map_err(WorkspaceLineageError::Store)?
362 {
363 return Ok(record);
364 }
365 let existing = self
366 .store
367 .load(record.workspace_id)
368 .await
369 .map_err(WorkspaceLineageError::Store)?
370 .ok_or(WorkspaceLineageError::IncompatibleState)?;
371 if lineage_shape_is_valid(&existing)
372 && same_lineage(&existing, &record)
373 && existing.ready == record.ready
374 {
375 Ok(existing)
376 } else if lineage_shape_is_valid(&existing)
377 && same_lineage(&existing, &record)
378 && record.ready
379 && !existing.ready
380 {
381 self.promote_ready(record).await
382 } else {
383 Err(WorkspaceLineageError::ConflictingRegistration)
384 }
385 }
386
387 async fn promote_ready(
388 &self,
389 ready: WorkspaceLineageRecord,
390 ) -> Result<WorkspaceLineageRecord, WorkspaceLineageError<S::Error>> {
391 if !ready.ready || !lineage_shape_is_valid(&ready) {
392 return Err(WorkspaceLineageError::IncompatibleState);
393 }
394 for _ in 0..MAXIMUM_LINEAGE_CAS_ATTEMPTS {
395 let existing = self
396 .store
397 .load(ready.workspace_id)
398 .await
399 .map_err(WorkspaceLineageError::Store)?
400 .ok_or(WorkspaceLineageError::IncompatibleState)?;
401 if !lineage_shape_is_valid(&existing) || !same_lineage(&existing, &ready) {
402 return Err(WorkspaceLineageError::ConflictingRegistration);
403 }
404 if existing.ready {
405 return Ok(existing);
406 }
407 let mut replacement = ready.clone();
408 replacement.revision = existing
409 .revision
410 .checked_add(1)
411 .ok_or(WorkspaceLineageError::IncompatibleState)?;
412 if self
413 .store
414 .compare_and_swap(ready.workspace_id, existing.revision, replacement.clone())
415 .await
416 .map_err(WorkspaceLineageError::Store)?
417 {
418 return Ok(replacement);
419 }
420 }
421 Err(WorkspaceLineageError::Contended)
422 }
423}
424
425fn same_lineage(left: &WorkspaceLineageRecord, right: &WorkspaceLineageRecord) -> bool {
426 left.version == right.version
427 && left.workspace_id == right.workspace_id
428 && left.workspace_name == right.workspace_name
429 && left.parent_workspace_id == right.parent_workspace_id
430 && left.parent_workspace_name == right.parent_workspace_name
431 && left.fork_generation == right.fork_generation
432 && (!left.ready || !right.ready || left.initial_generation == right.initial_generation)
433}
434
435#[derive(Default)]
437pub struct MemoryWorkspaceLineageStore {
438 records: Mutex<BTreeMap<WorkspaceId, WorkspaceLineageRecord>>,
439}
440
441impl MemoryWorkspaceLineageStore {
442 #[must_use]
444 pub fn new() -> Self {
445 Self::default()
446 }
447}
448
449#[derive(Debug, Error)]
451#[error("workspace-lineage memory store is unavailable")]
452pub struct MemoryWorkspaceLineageStoreError;
453
454impl WorkspaceLineageStore for MemoryWorkspaceLineageStore {
455 type Error = MemoryWorkspaceLineageStoreError;
456
457 async fn load(
458 &self,
459 workspace_id: WorkspaceId,
460 ) -> Result<Option<WorkspaceLineageRecord>, Self::Error> {
461 self.records
462 .lock()
463 .map_err(|_| MemoryWorkspaceLineageStoreError)
464 .map(|records| records.get(&workspace_id).cloned())
465 }
466
467 async fn compare_and_swap(
468 &self,
469 workspace_id: WorkspaceId,
470 expected_revision: u64,
471 replacement: WorkspaceLineageRecord,
472 ) -> Result<bool, Self::Error> {
473 let mut records = self
474 .records
475 .lock()
476 .map_err(|_| MemoryWorkspaceLineageStoreError)?;
477 let revision = records
478 .get(&workspace_id)
479 .map_or(0, |record| record.revision);
480 if revision != expected_revision {
481 return Ok(false);
482 }
483 records.insert(workspace_id, replacement);
484 Ok(true)
485 }
486}
487
488#[cfg(test)]
489#[allow(clippy::expect_used)]
490mod tests {
491 use super::*;
492 use crate::{Digest, Fs, WorkspaceName};
493
494 fn workspace(name: &str) -> WorkspaceId {
495 WorkspaceId::derive([9; 16], &WorkspaceName::new(name).expect("valid name"))
496 }
497
498 fn generation(byte: u8) -> GenerationId {
499 GenerationId::new(Digest::from_bytes([byte; 32]))
500 }
501
502 #[tokio::test]
503 async fn malformed_lineage_shape_never_enters_the_graph() {
504 let graph = WorkspaceGraph::new(MemoryWorkspaceLineageStore::new());
505 let child = workspace("child");
506 let malformed = WorkspaceLineageRecord {
507 version: LINEAGE_VERSION,
508 revision: 1,
509 ready: true,
510 workspace_id: child,
511 workspace_name: "child".to_owned(),
512 parent_workspace_id: Some(child),
513 parent_workspace_name: None,
514 fork_generation: generation(1),
515 initial_generation: generation(2),
516 };
517
518 assert!(matches!(
519 graph.register(malformed).await,
520 Err(WorkspaceLineageError::IncompatibleState)
521 ));
522 }
523
524 #[tokio::test]
525 async fn direct_parent_authorization_and_recursive_walk_are_durable() {
526 let graph = WorkspaceGraph::new(MemoryWorkspaceLineageStore::new());
527 let root = workspace("root");
528 let child = workspace("child");
529 let grandchild = workspace("grandchild");
530 graph
531 .register(WorkspaceLineageRecord {
532 version: LINEAGE_VERSION,
533 revision: 1,
534 ready: true,
535 workspace_id: root,
536 workspace_name: "root".to_owned(),
537 parent_workspace_id: None,
538 parent_workspace_name: None,
539 fork_generation: generation(1),
540 initial_generation: generation(1),
541 })
542 .await
543 .expect("root registration");
544 for (workspace_id, parent_workspace_id, byte) in [(child, root, 2), (grandchild, child, 3)]
545 {
546 graph
547 .register(WorkspaceLineageRecord {
548 version: LINEAGE_VERSION,
549 revision: 1,
550 ready: true,
551 workspace_id,
552 workspace_name: if workspace_id == child {
553 "child".to_owned()
554 } else {
555 "grandchild".to_owned()
556 },
557 parent_workspace_id: Some(parent_workspace_id),
558 parent_workspace_name: Some(if parent_workspace_id == root {
559 "root".to_owned()
560 } else {
561 "child".to_owned()
562 }),
563 fork_generation: generation(byte),
564 initial_generation: generation(byte),
565 })
566 .await
567 .expect("child registration");
568 }
569 graph
570 .authorize_join(grandchild, child)
571 .await
572 .expect("direct parent");
573 assert!(matches!(
574 graph.authorize_join(grandchild, root).await,
575 Err(WorkspaceLineageError::UnauthorizedJoin)
576 ));
577 let ancestors = graph
578 .ancestors(grandchild, 3)
579 .await
580 .expect("bounded ancestry");
581 assert_eq!(
582 ancestors
583 .iter()
584 .map(|record| record.workspace_id)
585 .collect::<Vec<_>>(),
586 vec![child, root]
587 );
588 }
589
590 #[tokio::test]
591 async fn a_durable_planned_child_is_completed_by_retry_after_a_crash() {
592 let fs = Fs::memory();
593 let parent = fs.create_workspace("root").await.expect("root");
594 let graph = WorkspaceGraph::new(MemoryWorkspaceLineageStore::new());
595 graph.register_root(&parent).await.expect("register root");
596 let parent_head = parent.head().await.expect("parent head");
597 let child_id = parent
598 .fork_workspace_id("child")
599 .expect("derived child identity");
600 let planned = WorkspaceLineageRecord {
601 version: LINEAGE_VERSION,
602 revision: 1,
603 ready: false,
604 workspace_id: child_id,
605 workspace_name: "child".to_owned(),
606 parent_workspace_id: Some(parent.id()),
607 parent_workspace_name: Some(parent.name().as_str().to_owned()),
608 fork_generation: parent_head.id(),
609 initial_generation: parent_head.id(),
610 };
611 graph.register(planned).await.expect("persist fork intent");
612 assert!(matches!(
613 graph.resolve(child_id).await,
614 Err(WorkspaceLineageError::IncompatibleState)
615 ));
616
617 let child = graph
618 .fork(&parent, "child", IdempotencyKey::from_bytes([31; 16]))
619 .await
620 .expect("retry completes child");
621 assert_eq!(child.id(), child_id);
622 let recovered = graph.resolve(child_id).await.expect("ready lineage");
623 assert!(recovered.ready);
624 assert_eq!(recovered.parent_workspace_id, Some(parent.id()));
625 assert_eq!(
626 recovered.initial_generation,
627 child.head().await.expect("head").id()
628 );
629 }
630}