1use crate::{
10 AsyncAuthorityStore, AsyncObjectStore, GenerationId, IdempotencyKey, OperationId, Workspace,
11 WorkspaceError, WorkspaceId, WorkspaceRebase,
12};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::future::Future;
16use std::sync::{Arc, Mutex};
17use thiserror::Error;
18
19use futures::StreamExt as _;
20
21const STATE_VERSION: u32 = 1;
22const MAXIMUM_CAS_ATTEMPTS: u8 = 32;
23
24#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct OperationLeaseId(OperationId);
28
29impl OperationLeaseId {
30 #[must_use]
32 pub fn new() -> Self {
33 Self(OperationId::new())
34 }
35
36 #[must_use]
38 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
39 Self(OperationId::from_bytes(bytes))
40 }
41
42 #[must_use]
44 pub const fn into_bytes(self) -> [u8; 16] {
45 self.0.into_bytes()
46 }
47}
48
49impl Default for OperationLeaseId {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57pub struct OperationLease {
58 pub id: OperationLeaseId,
60 pub owner: String,
62 pub expires_at_millis: u64,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
68pub enum OperationWindowPhase {
69 Idle,
71 Active {
73 pinned_parent: GenerationId,
75 leases: BTreeMap<OperationLeaseId, OperationLease>,
77 pending_parent: Option<GenerationId>,
79 },
80 Reconciling {
82 ticket: OperationId,
84 pinned_parent: GenerationId,
86 pending_parent: Option<GenerationId>,
88 #[serde(default)]
90 subsequent_parent: Option<GenerationId>,
91 },
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
96pub struct OperationWindowSnapshot {
97 pub version: u32,
99 pub revision: u64,
101 pub workspace_id: WorkspaceId,
103 pub phase: OperationWindowPhase,
105}
106
107impl OperationWindowSnapshot {
108 fn idle(workspace_id: WorkspaceId) -> Self {
109 Self {
110 version: STATE_VERSION,
111 revision: 0,
112 workspace_id,
113 phase: OperationWindowPhase::Idle,
114 }
115 }
116}
117
118fn snapshot_shape_is_valid(snapshot: &OperationWindowSnapshot) -> bool {
119 match &snapshot.phase {
120 OperationWindowPhase::Idle => true,
121 OperationWindowPhase::Reconciling { ticket, .. } => ticket.into_bytes() != [0; 16],
122 OperationWindowPhase::Active { leases, .. } => {
123 !leases.is_empty()
124 && leases.iter().all(|(lease_id, lease)| {
125 *lease_id == lease.id
126 && lease_id.into_bytes() != [0; 16]
127 && !lease.owner.trim().is_empty()
128 && lease.expires_at_millis > 0
129 })
130 }
131 }
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
136pub struct OperationWindowLease {
137 pub workspace_id: WorkspaceId,
139 pub lease_id: OperationLeaseId,
141 pub pinned_parent: GenerationId,
143 pub expires_at_millis: u64,
145}
146
147impl OperationWindowLease {
148 #[must_use]
151 pub const fn publication_permit(&self) -> crate::PublicationPermit {
152 crate::PublicationPermit::Lease {
153 authority_id: crate::kernel::volume_authority_id(self.workspace_id.volume_id())
154 .into_bytes(),
155 workspace_id: self.workspace_id.into_bytes(),
156 lease_id: self.lease_id.into_bytes(),
157 expires_at_millis: self.expires_at_millis,
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub struct OperationWindowReconcile {
165 pub ticket: OperationId,
167 pub pinned_parent: GenerationId,
169 pub pending_parent: Option<GenerationId>,
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub enum OperationWindowFinish {
176 StillActive {
178 remaining: u32,
180 },
181 Reconcile(OperationWindowReconcile),
183 AlreadyClosed,
185}
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189pub struct OperationReconcileLimits {
190 pub maximum_generations: u32,
192 pub maximum_changes: u32,
194 pub maximum_conflicts: u32,
196}
197
198impl Default for OperationReconcileLimits {
199 fn default() -> Self {
200 Self {
201 maximum_generations: 4_096,
202 maximum_changes: 65_536,
203 maximum_conflicts: 1_024,
204 }
205 }
206}
207
208pub enum WorkspaceOperationFinish<A, O> {
210 StillActive {
212 remaining: u32,
214 },
215 AlreadyClosed,
217 Reconciled(WorkspaceRebase<A, O>),
219}
220
221pub trait OperationWindowStore: Send + Sync {
223 type Error: std::error::Error + Send + Sync + 'static;
225
226 fn load(
228 &self,
229 workspace_id: WorkspaceId,
230 ) -> impl Future<Output = Result<Option<OperationWindowSnapshot>, Self::Error>> + Send;
231
232 fn compare_and_swap(
234 &self,
235 workspace_id: WorkspaceId,
236 expected_revision: u64,
237 replacement: OperationWindowSnapshot,
238 ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
239}
240
241#[derive(Clone)]
247pub struct StreamOperationWindowStore<P> {
248 provider: Arc<P>,
249}
250
251impl<P> StreamOperationWindowStore<P> {
252 #[must_use]
255 pub fn new(provider: Arc<P>) -> Self {
256 Self { provider }
257 }
258}
259
260#[derive(Debug, Error)]
262pub enum StreamOperationWindowStoreError {
263 #[error("operation-window stream failed: {0}")]
265 Stream(#[from] acyclic_stream::StreamError),
266 #[error("operation-window stream state is corrupt: {0}")]
268 Corrupt(String),
269}
270
271impl<P: acyclic_stream::StreamProvider> OperationWindowStore for StreamOperationWindowStore<P> {
272 type Error = StreamOperationWindowStoreError;
273
274 async fn load(
275 &self,
276 workspace_id: WorkspaceId,
277 ) -> Result<Option<OperationWindowSnapshot>, Self::Error> {
278 let path = stream_window_path(workspace_id)?;
279 let tail = match self.provider.tail(path.clone()).await {
280 Ok(tail) => tail,
281 Err(acyclic_stream::StreamError::NotFound) => return Ok(None),
282 Err(error) => return Err(error.into()),
283 };
284 if tail == 0 {
285 return Err(StreamOperationWindowStoreError::Corrupt(
286 "window stream is empty".to_owned(),
287 ));
288 }
289 let mut records = self
290 .provider
291 .read(acyclic_stream::ReadRequest {
292 path,
293 from: tail - 1,
294 limit: 1,
295 })
296 .await?;
297 let record = records.next().await.transpose()?.ok_or_else(|| {
298 StreamOperationWindowStoreError::Corrupt("window stream tail has no record".to_owned())
299 })?;
300 let snapshot: OperationWindowSnapshot = serde_json::from_slice(&record.value)
301 .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?;
302 if snapshot.revision != tail {
303 return Err(StreamOperationWindowStoreError::Corrupt(
304 "window revision does not match its stream tail".to_owned(),
305 ));
306 }
307 if !snapshot_shape_is_valid(&snapshot) {
308 return Err(StreamOperationWindowStoreError::Corrupt(
309 "window phase shape is invalid".to_owned(),
310 ));
311 }
312 Ok(Some(snapshot))
313 }
314
315 async fn compare_and_swap(
316 &self,
317 workspace_id: WorkspaceId,
318 expected_revision: u64,
319 replacement: OperationWindowSnapshot,
320 ) -> Result<bool, Self::Error> {
321 if replacement.workspace_id != workspace_id
322 || replacement.revision != expected_revision.saturating_add(1)
323 || !snapshot_shape_is_valid(&replacement)
324 {
325 return Err(StreamOperationWindowStoreError::Corrupt(
326 "replacement identity or revision is invalid".to_owned(),
327 ));
328 }
329 let current = self.load(workspace_id).await?;
330 if current.as_ref().map_or(0, |snapshot| snapshot.revision) != expected_revision {
331 return Ok(false);
332 }
333 let before = current
334 .as_ref()
335 .map_or_else(BTreeMap::new, |snapshot| phase_leases(&snapshot.phase));
336 let after = phase_leases(&replacement.phase);
337 let state_path = stream_window_path(workspace_id)?;
338 let mut conditions = vec![if expected_revision == 0 {
339 acyclic_stream::CommitCondition::Absent {
340 path: state_path.clone(),
341 }
342 } else {
343 acyclic_stream::CommitCondition::Tail {
344 path: state_path.clone(),
345 expected: expected_revision,
346 }
347 }];
348 let encoded = serde_json::to_vec(&replacement)
349 .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?;
350 let mut mutations = vec![acyclic_stream::CommitMutation::Append {
351 path: state_path,
352 records: vec![encoded.into()],
353 }];
354 for lease_id in after
355 .keys()
356 .filter(|lease_id| !before.contains_key(lease_id))
357 {
358 let path = stream_lease_path(workspace_id, *lease_id)?;
359 conditions.push(acyclic_stream::CommitCondition::Absent { path: path.clone() });
360 mutations.push(acyclic_stream::CommitMutation::Append {
361 path,
362 records: vec![bytes::Bytes::from_static(b"active")],
363 });
364 }
365 for lease_id in before
366 .keys()
367 .filter(|lease_id| !after.contains_key(lease_id))
368 {
369 let path = stream_lease_path(workspace_id, *lease_id)?;
370 conditions.push(acyclic_stream::CommitCondition::Tail {
371 path: path.clone(),
372 expected: 1,
373 });
374 mutations.push(acyclic_stream::CommitMutation::Append {
375 path,
376 records: vec![bytes::Bytes::from_static(b"fenced")],
377 });
378 }
379 let mut hasher = blake3::Hasher::new();
380 hasher.update(b"acyclic-operation-window-cas-v2\0");
381 hasher.update(&workspace_id.into_bytes());
382 hasher.update(&expected_revision.to_le_bytes());
383 hasher.update(
384 &serde_json::to_vec(&replacement)
385 .map_err(|error| StreamOperationWindowStoreError::Corrupt(error.to_string()))?,
386 );
387 let key = acyclic_stream::IdempotencyKey::new(hasher.finalize().as_bytes().to_vec())?;
388 match self
389 .provider
390 .commit(acyclic_stream::CommitRequest {
391 conditions,
392 mutations,
393 idempotency_key: key,
394 })
395 .await?
396 {
397 acyclic_stream::CommitOutcome::Committed(_) => Ok(true),
398 acyclic_stream::CommitOutcome::Conflict(_) => Ok(false),
399 }
400 }
401}
402
403fn phase_leases(phase: &OperationWindowPhase) -> BTreeMap<OperationLeaseId, OperationLease> {
404 match phase {
405 OperationWindowPhase::Active { leases, .. } => leases.clone(),
406 OperationWindowPhase::Idle | OperationWindowPhase::Reconciling { .. } => BTreeMap::new(),
407 }
408}
409
410fn stream_window_path(
411 workspace_id: WorkspaceId,
412) -> Result<acyclic_stream::StreamPath, acyclic_stream::StreamError> {
413 acyclic_stream::StreamPath::new(format!(
414 "fs/operation-windows-v2/{}/state",
415 hex::encode(workspace_id.into_bytes())
416 ))
417}
418
419pub(crate) fn stream_lease_path(
420 workspace_id: WorkspaceId,
421 lease_id: OperationLeaseId,
422) -> Result<acyclic_stream::StreamPath, acyclic_stream::StreamError> {
423 acyclic_stream::StreamPath::new(format!(
424 "fs/operation-windows-v2/{}/leases/{}",
425 hex::encode(workspace_id.into_bytes()),
426 hex::encode(lease_id.into_bytes())
427 ))
428}
429
430#[derive(Debug, Error)]
432pub enum OperationWindowError<E: std::error::Error + 'static> {
433 #[error("operation-window store failed: {0}")]
435 Store(E),
436 #[error("operation-window state is incompatible")]
438 IncompatibleState,
439 #[error("workspace operation window is reconciling")]
441 Reconciling,
442 #[error("operation lease must expire after the current time")]
444 InvalidExpiry,
445 #[error("operation lease identity is already bound to another owner or expiry")]
447 LeaseIdentityConflict,
448 #[error("operation lease is no longer active")]
450 StaleLease,
451 #[error("operation-window state remained contended")]
453 Contended,
454 #[error("operation-window reconciliation ticket is stale")]
456 StaleTicket,
457 #[error("workspace reconciliation failed: {0}")]
459 Workspace(#[source] WorkspaceError),
460}
461
462pub struct OperationWindowCoordinator<S> {
464 store: S,
465}
466
467impl<S> OperationWindowCoordinator<S> {
468 #[must_use]
470 pub const fn new(store: S) -> Self {
471 Self { store }
472 }
473
474 #[must_use]
476 pub const fn store(&self) -> &S {
477 &self.store
478 }
479}
480
481impl<S: OperationWindowStore> OperationWindowCoordinator<S> {
482 pub async fn begin(
484 &self,
485 workspace_id: WorkspaceId,
486 parent: GenerationId,
487 owner: impl Into<String>,
488 now_millis: u64,
489 expires_at_millis: u64,
490 ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
491 self.begin_with_lease_id(
492 workspace_id,
493 parent,
494 owner,
495 now_millis,
496 expires_at_millis,
497 OperationLeaseId::new(),
498 )
499 .await
500 }
501
502 pub async fn begin_with_lease_id(
504 &self,
505 workspace_id: WorkspaceId,
506 parent: GenerationId,
507 owner: impl Into<String>,
508 now_millis: u64,
509 expires_at_millis: u64,
510 lease_id: OperationLeaseId,
511 ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
512 if expires_at_millis <= now_millis {
513 return Err(OperationWindowError::InvalidExpiry);
514 }
515 let owner = owner.into();
516 if owner.trim().is_empty() || lease_id.into_bytes() == [0; 16] {
517 return Err(OperationWindowError::LeaseIdentityConflict);
518 }
519 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
520 let mut current = self.snapshot(workspace_id).await?;
521 if expire_to_reconcile(
522 &mut current.phase,
523 now_millis,
524 OperationId::from_bytes(lease_id.into_bytes()),
525 )
526 .is_some()
527 {
528 let expected = current.revision;
529 current.revision = expected.saturating_add(1);
530 if self.cas(workspace_id, expected, current).await? {
531 return Err(OperationWindowError::Reconciling);
532 }
533 continue;
534 }
535 let pinned_parent = match &mut current.phase {
536 OperationWindowPhase::Idle => {
537 let mut leases = BTreeMap::new();
538 leases.insert(
539 lease_id,
540 OperationLease {
541 id: lease_id,
542 owner: owner.clone(),
543 expires_at_millis,
544 },
545 );
546 current.phase = OperationWindowPhase::Active {
547 pinned_parent: parent,
548 leases,
549 pending_parent: None,
550 };
551 parent
552 }
553 OperationWindowPhase::Active {
554 pinned_parent,
555 leases,
556 ..
557 } => {
558 if let Some(existing) = leases.get(&lease_id) {
559 if existing.owner == owner
560 && existing.expires_at_millis == expires_at_millis
561 {
562 return Ok(OperationWindowLease {
563 workspace_id,
564 lease_id,
565 pinned_parent: *pinned_parent,
566 expires_at_millis,
567 });
568 }
569 return Err(OperationWindowError::LeaseIdentityConflict);
570 }
571 leases.insert(
572 lease_id,
573 OperationLease {
574 id: lease_id,
575 owner: owner.clone(),
576 expires_at_millis,
577 },
578 );
579 *pinned_parent
580 }
581 OperationWindowPhase::Reconciling { .. } => {
582 return Err(OperationWindowError::Reconciling);
583 }
584 };
585 let expected = current.revision;
586 current.revision = expected.saturating_add(1);
587 if self.cas(workspace_id, expected, current).await? {
588 return Ok(OperationWindowLease {
589 workspace_id,
590 lease_id,
591 pinned_parent,
592 expires_at_millis,
593 });
594 }
595 }
596 Err(OperationWindowError::Contended)
597 }
598
599 pub async fn observe_parent(
601 &self,
602 workspace_id: WorkspaceId,
603 parent: GenerationId,
604 ) -> Result<bool, OperationWindowError<S::Error>> {
605 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
606 let mut current = self.snapshot(workspace_id).await?;
607 match &mut current.phase {
608 OperationWindowPhase::Idle => return Ok(false),
609 OperationWindowPhase::Active { pending_parent, .. } => {
610 *pending_parent = Some(parent);
611 }
612 OperationWindowPhase::Reconciling {
613 subsequent_parent, ..
614 } => *subsequent_parent = Some(parent),
615 }
616 let expected = current.revision;
617 current.revision = expected.saturating_add(1);
618 if self.cas(workspace_id, expected, current).await? {
619 return Ok(true);
620 }
621 }
622 Err(OperationWindowError::Contended)
623 }
624
625 pub async fn parent_advanced_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
632 &self,
633 workspace: &Workspace<A, O>,
634 parent: GenerationId,
635 limits: OperationReconcileLimits,
636 ) -> Result<Option<WorkspaceRebase<A, O>>, OperationWindowError<S::Error>> {
637 let workspace_id = workspace.id();
638 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
639 let mut current = self.snapshot(workspace_id).await?;
640 let reconcile = match &mut current.phase {
641 OperationWindowPhase::Idle => {
642 let ticket = OperationId::new();
643 current.phase = OperationWindowPhase::Reconciling {
644 ticket,
645 pinned_parent: parent,
646 pending_parent: Some(parent),
647 subsequent_parent: None,
648 };
649 Some(OperationWindowReconcile {
650 ticket,
651 pinned_parent: parent,
652 pending_parent: Some(parent),
653 })
654 }
655 OperationWindowPhase::Active { pending_parent, .. } => {
656 *pending_parent = Some(parent);
657 None
658 }
659 OperationWindowPhase::Reconciling {
660 subsequent_parent, ..
661 } => {
662 *subsequent_parent = Some(parent);
663 None
664 }
665 };
666 let expected = current.revision;
667 current.revision = expected.saturating_add(1);
668 if self.cas(workspace_id, expected, current).await? {
669 return match reconcile {
670 Some(reconcile) => self
671 .reconcile_workspace(workspace, reconcile, limits)
672 .await
673 .map(Some),
674 None => Ok(None),
675 };
676 }
677 }
678 Err(OperationWindowError::Contended)
679 }
680
681 pub async fn renew(
685 &self,
686 lease: &OperationWindowLease,
687 now_millis: u64,
688 expires_at_millis: u64,
689 ) -> Result<OperationWindowLease, OperationWindowError<S::Error>> {
690 if expires_at_millis <= now_millis || expires_at_millis < lease.expires_at_millis {
691 return Err(OperationWindowError::InvalidExpiry);
692 }
693 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
694 let mut current = self.snapshot(lease.workspace_id).await?;
695 let OperationWindowPhase::Active {
696 pinned_parent,
697 leases,
698 ..
699 } = &mut current.phase
700 else {
701 return Err(OperationWindowError::StaleLease);
702 };
703 let Some(existing) = leases.get(&lease.lease_id) else {
704 return Err(OperationWindowError::StaleLease);
705 };
706 if existing.expires_at_millis != lease.expires_at_millis
707 || existing.expires_at_millis <= now_millis
708 {
709 return Err(OperationWindowError::StaleLease);
710 }
711 if existing.expires_at_millis == expires_at_millis {
712 return Ok(lease.clone());
713 }
714 let pinned_parent = *pinned_parent;
715 leases
716 .get_mut(&lease.lease_id)
717 .ok_or(OperationWindowError::StaleLease)?
718 .expires_at_millis = expires_at_millis;
719 let expected = current.revision;
720 current.revision = expected.saturating_add(1);
721 if self.cas(lease.workspace_id, expected, current).await? {
722 return Ok(OperationWindowLease {
723 workspace_id: lease.workspace_id,
724 lease_id: lease.lease_id,
725 pinned_parent,
726 expires_at_millis,
727 });
728 }
729 }
730 Err(OperationWindowError::Contended)
731 }
732
733 pub async fn finish(
735 &self,
736 lease: &OperationWindowLease,
737 now_millis: u64,
738 ) -> Result<OperationWindowFinish, OperationWindowError<S::Error>> {
739 self.finish_with_ticket(lease, now_millis, OperationId::new())
740 .await
741 }
742
743 pub async fn finish_with_ticket(
745 &self,
746 lease: &OperationWindowLease,
747 now_millis: u64,
748 reconciliation_ticket: OperationId,
749 ) -> Result<OperationWindowFinish, OperationWindowError<S::Error>> {
750 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
751 let mut current = self.snapshot(lease.workspace_id).await?;
752 let before = current.phase.clone();
753 let result = close_lease(
754 &mut current.phase,
755 lease.lease_id,
756 lease.expires_at_millis,
757 now_millis,
758 reconciliation_ticket,
759 );
760 if result.is_none() && current.phase == before {
761 return Ok(OperationWindowFinish::AlreadyClosed);
762 }
763 let expected = current.revision;
764 current.revision = expected.saturating_add(1);
765 if self.cas(lease.workspace_id, expected, current).await? {
766 return Ok(result.unwrap_or(OperationWindowFinish::AlreadyClosed));
767 }
768 }
769 Err(OperationWindowError::Contended)
770 }
771
772 pub async fn finish_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
778 &self,
779 workspace: &Workspace<A, O>,
780 lease: &OperationWindowLease,
781 now_millis: u64,
782 limits: OperationReconcileLimits,
783 ) -> Result<WorkspaceOperationFinish<A, O>, OperationWindowError<S::Error>> {
784 if workspace.id() != lease.workspace_id {
785 return Err(OperationWindowError::IncompatibleState);
786 }
787 let reconcile = match self.finish(lease, now_millis).await? {
788 OperationWindowFinish::StillActive { remaining } => {
789 return Ok(WorkspaceOperationFinish::StillActive { remaining });
790 }
791 OperationWindowFinish::AlreadyClosed => {
792 return Ok(WorkspaceOperationFinish::AlreadyClosed);
793 }
794 OperationWindowFinish::Reconcile(reconcile) => reconcile,
795 };
796 self.reconcile_workspace(workspace, reconcile, limits)
797 .await
798 .map(WorkspaceOperationFinish::Reconciled)
799 }
800
801 pub async fn reconcile_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
807 &self,
808 workspace: &Workspace<A, O>,
809 reconcile: OperationWindowReconcile,
810 limits: OperationReconcileLimits,
811 ) -> Result<WorkspaceRebase<A, O>, OperationWindowError<S::Error>> {
812 let snapshot = self.snapshot(workspace.id()).await?;
813 if !matches!(
814 snapshot.phase,
815 OperationWindowPhase::Reconciling { ticket, .. } if ticket == reconcile.ticket
816 ) {
817 return Err(OperationWindowError::StaleTicket);
818 }
819 let outcome = if reconcile.pending_parent.is_some() {
820 workspace
821 .live_rebase(
822 IdempotencyKey::from_bytes(reconcile.ticket.into_bytes()),
823 limits.maximum_generations,
824 limits.maximum_changes,
825 limits.maximum_conflicts,
826 )
827 .await
828 .map_err(OperationWindowError::Workspace)?
829 } else {
830 let generation = workspace
831 .head()
832 .await
833 .map_err(OperationWindowError::Workspace)?;
834 WorkspaceRebase::Current(generation)
835 };
836 if matches!(
837 outcome,
838 WorkspaceRebase::Rebased(_)
839 | WorkspaceRebase::AlreadyRebased(_)
840 | WorkspaceRebase::Current(_)
841 | WorkspaceRebase::Conflicted { .. }
842 ) {
843 self.complete_reconcile(workspace.id(), reconcile.ticket)
844 .await?;
845 }
846 Ok(outcome)
847 }
848
849 pub async fn recover_workspace<A: AsyncAuthorityStore, O: AsyncObjectStore>(
852 &self,
853 workspace: &Workspace<A, O>,
854 now_millis: u64,
855 limits: OperationReconcileLimits,
856 ) -> Result<Option<WorkspaceRebase<A, O>>, OperationWindowError<S::Error>> {
857 let reconcile = self.claim_reconcile(workspace.id(), now_millis).await?;
858 match reconcile {
859 Some(reconcile) => self
860 .reconcile_workspace(workspace, reconcile, limits)
861 .await
862 .map(Some),
863 None => Ok(None),
864 }
865 }
866
867 pub async fn claim_reconcile(
870 &self,
871 workspace_id: WorkspaceId,
872 now_millis: u64,
873 ) -> Result<Option<OperationWindowReconcile>, OperationWindowError<S::Error>> {
874 self.claim_reconcile_with_ticket(workspace_id, now_millis, OperationId::new())
875 .await
876 }
877
878 pub async fn claim_reconcile_with_ticket(
880 &self,
881 workspace_id: WorkspaceId,
882 now_millis: u64,
883 ticket: OperationId,
884 ) -> Result<Option<OperationWindowReconcile>, OperationWindowError<S::Error>> {
885 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
886 let mut current = self.snapshot(workspace_id).await?;
887 match current.phase {
888 OperationWindowPhase::Idle => return Ok(None),
889 OperationWindowPhase::Reconciling {
890 ticket,
891 pinned_parent,
892 pending_parent,
893 ..
894 } => {
895 return Ok(Some(OperationWindowReconcile {
896 ticket,
897 pinned_parent,
898 pending_parent,
899 }));
900 }
901 OperationWindowPhase::Active { .. } => {}
902 }
903 let Some(reconcile) = expire_to_reconcile(&mut current.phase, now_millis, ticket)
904 else {
905 return Ok(None);
906 };
907 let expected = current.revision;
908 current.revision = expected.saturating_add(1);
909 if self.cas(workspace_id, expected, current).await? {
910 return Ok(Some(reconcile));
911 }
912 }
913 Err(OperationWindowError::Contended)
914 }
915
916 pub async fn complete_reconcile(
918 &self,
919 workspace_id: WorkspaceId,
920 ticket: OperationId,
921 ) -> Result<(), OperationWindowError<S::Error>> {
922 self.complete_reconcile_with_next_ticket(workspace_id, ticket, OperationId::new())
923 .await
924 }
925
926 pub async fn complete_reconcile_with_next_ticket(
929 &self,
930 workspace_id: WorkspaceId,
931 ticket: OperationId,
932 next_ticket: OperationId,
933 ) -> Result<(), OperationWindowError<S::Error>> {
934 for _ in 0..MAXIMUM_CAS_ATTEMPTS {
935 let mut current = self.snapshot(workspace_id).await?;
936 match current.phase {
937 OperationWindowPhase::Reconciling {
938 ticket: current_ticket,
939 pinned_parent,
940 subsequent_parent,
941 ..
942 } if current_ticket == ticket => {
943 current.phase = match subsequent_parent {
944 Some(parent) => OperationWindowPhase::Reconciling {
945 ticket: next_ticket,
946 pinned_parent,
947 pending_parent: Some(parent),
948 subsequent_parent: None,
949 },
950 None => OperationWindowPhase::Idle,
951 };
952 }
953 _ => return Err(OperationWindowError::StaleTicket),
954 }
955 let expected = current.revision;
956 current.revision = expected.saturating_add(1);
957 if self.cas(workspace_id, expected, current).await? {
958 return Ok(());
959 }
960 }
961 Err(OperationWindowError::Contended)
962 }
963
964 pub async fn inspect(
966 &self,
967 workspace_id: WorkspaceId,
968 ) -> Result<OperationWindowSnapshot, OperationWindowError<S::Error>> {
969 self.snapshot(workspace_id).await
970 }
971
972 pub async fn snapshot(
974 &self,
975 workspace_id: WorkspaceId,
976 ) -> Result<OperationWindowSnapshot, OperationWindowError<S::Error>> {
977 let stored = self
978 .store
979 .load(workspace_id)
980 .await
981 .map_err(OperationWindowError::Store)?;
982 let state = stored
983 .clone()
984 .unwrap_or_else(|| OperationWindowSnapshot::idle(workspace_id));
985 if state.version != STATE_VERSION
986 || state.workspace_id != workspace_id
987 || stored.is_some() && state.revision == 0
988 || !snapshot_shape_is_valid(&state)
989 {
990 return Err(OperationWindowError::IncompatibleState);
991 }
992 Ok(state)
993 }
994
995 async fn cas(
996 &self,
997 workspace_id: WorkspaceId,
998 expected_revision: u64,
999 replacement: OperationWindowSnapshot,
1000 ) -> Result<bool, OperationWindowError<S::Error>> {
1001 if replacement.version != STATE_VERSION
1002 || replacement.workspace_id != workspace_id
1003 || replacement.revision != expected_revision.saturating_add(1)
1004 || !snapshot_shape_is_valid(&replacement)
1005 {
1006 return Err(OperationWindowError::IncompatibleState);
1007 }
1008 self.store
1009 .compare_and_swap(workspace_id, expected_revision, replacement)
1010 .await
1011 .map_err(OperationWindowError::Store)
1012 }
1013}
1014
1015fn expire_to_reconcile(
1016 phase: &mut OperationWindowPhase,
1017 now_millis: u64,
1018 ticket: OperationId,
1019) -> Option<OperationWindowReconcile> {
1020 let OperationWindowPhase::Active {
1021 pinned_parent,
1022 leases,
1023 pending_parent,
1024 } = phase
1025 else {
1026 return None;
1027 };
1028 leases.retain(|_, lease| lease.expires_at_millis > now_millis);
1029 if !leases.is_empty() {
1030 return None;
1031 }
1032 let reconcile = OperationWindowReconcile {
1033 ticket,
1034 pinned_parent: *pinned_parent,
1035 pending_parent: *pending_parent,
1036 };
1037 *phase = OperationWindowPhase::Reconciling {
1038 ticket: reconcile.ticket,
1039 pinned_parent: reconcile.pinned_parent,
1040 pending_parent: reconcile.pending_parent,
1041 subsequent_parent: None,
1042 };
1043 Some(reconcile)
1044}
1045
1046fn close_lease(
1047 phase: &mut OperationWindowPhase,
1048 lease_id: OperationLeaseId,
1049 expected_expiry_millis: u64,
1050 now_millis: u64,
1051 reconciliation_ticket: OperationId,
1052) -> Option<OperationWindowFinish> {
1053 let OperationWindowPhase::Active {
1054 pinned_parent,
1055 leases,
1056 pending_parent,
1057 } = phase
1058 else {
1059 return None;
1060 };
1061 let requested_live = leases.get(&lease_id).is_some_and(|lease| {
1062 lease.expires_at_millis == expected_expiry_millis && lease.expires_at_millis > now_millis
1063 });
1064 if leases
1065 .get(&lease_id)
1066 .is_some_and(|lease| lease.expires_at_millis != expected_expiry_millis)
1067 {
1068 return None;
1069 }
1070 leases.remove(&lease_id);
1071 leases.retain(|_, lease| lease.expires_at_millis > now_millis);
1072 if !leases.is_empty() {
1073 if !requested_live {
1074 return None;
1075 }
1076 return Some(OperationWindowFinish::StillActive {
1077 remaining: u32::try_from(leases.len()).unwrap_or(u32::MAX),
1078 });
1079 }
1080 let reconcile = OperationWindowReconcile {
1081 ticket: reconciliation_ticket,
1082 pinned_parent: *pinned_parent,
1083 pending_parent: *pending_parent,
1084 };
1085 *phase = OperationWindowPhase::Reconciling {
1086 ticket: reconcile.ticket,
1087 pinned_parent: reconcile.pinned_parent,
1088 pending_parent: reconcile.pending_parent,
1089 subsequent_parent: None,
1090 };
1091 requested_live.then_some(OperationWindowFinish::Reconcile(reconcile))
1092}
1093
1094#[derive(Clone, Default)]
1096pub struct MemoryOperationWindowStore {
1097 states: Arc<Mutex<BTreeMap<WorkspaceId, OperationWindowSnapshot>>>,
1098}
1099
1100impl MemoryOperationWindowStore {
1101 #[must_use]
1103 pub fn new() -> Self {
1104 Self::default()
1105 }
1106}
1107
1108#[derive(Debug, Error)]
1110#[error("operation-window memory store is unavailable")]
1111pub struct MemoryOperationWindowStoreError;
1112
1113impl OperationWindowStore for MemoryOperationWindowStore {
1114 type Error = MemoryOperationWindowStoreError;
1115
1116 async fn load(
1117 &self,
1118 workspace_id: WorkspaceId,
1119 ) -> Result<Option<OperationWindowSnapshot>, Self::Error> {
1120 self.states
1121 .lock()
1122 .map_err(|_| MemoryOperationWindowStoreError)
1123 .map(|states| states.get(&workspace_id).cloned())
1124 }
1125
1126 async fn compare_and_swap(
1127 &self,
1128 workspace_id: WorkspaceId,
1129 expected_revision: u64,
1130 replacement: OperationWindowSnapshot,
1131 ) -> Result<bool, Self::Error> {
1132 let mut states = self
1133 .states
1134 .lock()
1135 .map_err(|_| MemoryOperationWindowStoreError)?;
1136 let revision = states.get(&workspace_id).map_or(0, |state| state.revision);
1137 if revision != expected_revision {
1138 return Ok(false);
1139 }
1140 states.insert(workspace_id, replacement);
1141 Ok(true)
1142 }
1143}
1144
1145#[cfg(test)]
1146#[allow(clippy::expect_used, clippy::panic)]
1147mod tests {
1148 use super::*;
1149 use crate::{
1150 AsyncAuthorityStore, AuthorityId, CancellationToken, Digest, Epoch, ForkOptions, Fs,
1151 IdempotencyKey, OperationId, ProposedCommit, StreamAuthorityStore, WorkBudget,
1152 };
1153 use bytes::Bytes;
1154 use std::sync::atomic::{AtomicU64, Ordering};
1155
1156 #[derive(Default)]
1157 struct TestClock(AtomicU64);
1158
1159 impl acyclic_stream::UnixMillisClock for TestClock {
1160 fn now_unix_millis(&self) -> u64 {
1161 self.0.load(Ordering::SeqCst)
1162 }
1163 }
1164
1165 fn generation(byte: u8) -> GenerationId {
1166 GenerationId::new(Digest::from_bytes([byte; 32]))
1167 }
1168
1169 #[tokio::test]
1170 async fn persisted_active_windows_require_a_nonempty_identity_keyed_lease_set() {
1171 let workspace = WorkspaceId::from_bytes([6; 16]);
1172 let store = MemoryOperationWindowStore::new();
1173 store.states.lock().expect("state lock").insert(
1174 workspace,
1175 OperationWindowSnapshot {
1176 version: STATE_VERSION,
1177 revision: 1,
1178 workspace_id: workspace,
1179 phase: OperationWindowPhase::Active {
1180 pinned_parent: generation(1),
1181 leases: BTreeMap::new(),
1182 pending_parent: None,
1183 },
1184 },
1185 );
1186 let coordinator = OperationWindowCoordinator::new(store);
1187
1188 assert!(matches!(
1189 coordinator.inspect(workspace).await,
1190 Err(OperationWindowError::IncompatibleState)
1191 ));
1192 }
1193
1194 #[tokio::test]
1195 async fn overlapping_leases_coalesce_parent_and_reconcile_once() {
1196 let workspace = WorkspaceId::derive(
1197 [7; 16],
1198 &crate::WorkspaceName::new("agent").expect("valid test workspace"),
1199 );
1200 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1201 let first = coordinator
1202 .begin(workspace, generation(1), "tool-1", 10, 100)
1203 .await
1204 .expect("first lease");
1205 let second = coordinator
1206 .begin(workspace, generation(1), "tool-2", 11, 100)
1207 .await
1208 .expect("second lease");
1209 assert!(
1210 coordinator
1211 .observe_parent(workspace, generation(2))
1212 .await
1213 .expect("observe")
1214 );
1215 assert_eq!(
1216 coordinator.finish(&first, 20).await.expect("first close"),
1217 OperationWindowFinish::StillActive { remaining: 1 }
1218 );
1219 let OperationWindowFinish::Reconcile(reconcile) =
1220 coordinator.finish(&second, 21).await.expect("final close")
1221 else {
1222 panic!("final close must claim reconciliation");
1223 };
1224 assert_eq!(reconcile.pinned_parent, generation(1));
1225 assert_eq!(reconcile.pending_parent, Some(generation(2)));
1226 assert!(matches!(
1227 coordinator
1228 .begin(workspace, generation(2), "tool-3", 22, 100)
1229 .await,
1230 Err(OperationWindowError::Reconciling)
1231 ));
1232 coordinator
1233 .complete_reconcile(workspace, reconcile.ticket)
1234 .await
1235 .expect("complete reconciliation");
1236 coordinator
1237 .begin(workspace, generation(2), "tool-3", 23, 100)
1238 .await
1239 .expect("next window");
1240 }
1241
1242 #[tokio::test]
1243 async fn parent_advance_after_child_close_rebases_without_another_tool() {
1244 let fs = Fs::memory();
1245 let parent = fs.create_workspace("late-parent").await.expect("parent");
1246 let child = parent
1247 .fork(
1248 "late-child",
1249 ForkOptions::from_generation(
1250 parent.head().await.expect("base"),
1251 IdempotencyKey::new(),
1252 ),
1253 )
1254 .await
1255 .expect("child");
1256 child
1257 .write_text("/child", "local")
1258 .await
1259 .expect("child write");
1260 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1261 let lease = coordinator
1262 .begin(
1263 child.id(),
1264 parent.head().await.expect("parent head").id(),
1265 "tool",
1266 1,
1267 100,
1268 )
1269 .await
1270 .expect("lease");
1271 assert!(matches!(
1272 coordinator
1273 .finish_workspace(&child, &lease, 2, OperationReconcileLimits::default())
1274 .await
1275 .expect("child close"),
1276 WorkspaceOperationFinish::Reconciled(WorkspaceRebase::Current(_))
1277 ));
1278 parent
1279 .write_text("/parent", "upstream")
1280 .await
1281 .expect("parent write");
1282 assert!(matches!(
1283 coordinator
1284 .parent_advanced_workspace(
1285 &child,
1286 parent.head().await.expect("new parent head").id(),
1287 OperationReconcileLimits::default(),
1288 )
1289 .await
1290 .expect("parent advance"),
1291 Some(WorkspaceRebase::Rebased(_))
1292 ));
1293 assert_eq!(
1294 child.read("/parent", 16).await.expect("upstream"),
1295 Bytes::from_static(b"upstream")
1296 );
1297 assert_eq!(
1298 child.read("/child", 16).await.expect("local"),
1299 Bytes::from_static(b"local")
1300 );
1301 assert!(matches!(
1302 coordinator.inspect(child.id()).await.expect("window").phase,
1303 OperationWindowPhase::Idle
1304 ));
1305 }
1306
1307 #[tokio::test]
1308 async fn parent_advance_during_child_tool_rebases_at_final_close() {
1309 let fs = Fs::memory();
1310 let parent = fs.create_workspace("active-parent").await.expect("parent");
1311 let child = parent
1312 .fork(
1313 "active-child",
1314 ForkOptions::from_generation(
1315 parent.head().await.expect("base"),
1316 IdempotencyKey::new(),
1317 ),
1318 )
1319 .await
1320 .expect("child");
1321 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1322 let lease = coordinator
1323 .begin(
1324 child.id(),
1325 parent.head().await.expect("parent head").id(),
1326 "tool",
1327 1,
1328 100,
1329 )
1330 .await
1331 .expect("lease");
1332 parent
1333 .write_text("/parent", "new")
1334 .await
1335 .expect("parent write");
1336 assert!(
1337 coordinator
1338 .parent_advanced_workspace(
1339 &child,
1340 parent.head().await.expect("new parent head").id(),
1341 OperationReconcileLimits::default(),
1342 )
1343 .await
1344 .expect("coalesce")
1345 .is_none()
1346 );
1347 assert!(matches!(
1348 coordinator
1349 .finish_workspace(&child, &lease, 2, OperationReconcileLimits::default())
1350 .await
1351 .expect("final close"),
1352 WorkspaceOperationFinish::Reconciled(WorkspaceRebase::Rebased(_))
1353 ));
1354 assert_eq!(
1355 child.read("/parent", 16).await.expect("upstream").as_ref(),
1356 b"new"
1357 );
1358 }
1359
1360 #[tokio::test]
1361 async fn deterministic_lease_identity_is_idempotent_but_cannot_be_rebound() {
1362 let workspace = WorkspaceId::derive(
1363 [12; 16],
1364 &crate::WorkspaceName::new("lease-identity").expect("valid test workspace"),
1365 );
1366 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1367 let lease_id = OperationLeaseId::from_bytes([42; 16]);
1368 let first = coordinator
1369 .begin_with_lease_id(workspace, generation(1), "owner", 1, 10, lease_id)
1370 .await
1371 .expect("first lease");
1372 assert_eq!(
1373 coordinator
1374 .begin_with_lease_id(workspace, generation(1), "owner", 2, 10, lease_id)
1375 .await
1376 .expect("exact retry"),
1377 first
1378 );
1379 assert!(matches!(
1380 coordinator
1381 .begin_with_lease_id(workspace, generation(1), "other", 2, 10, lease_id)
1382 .await,
1383 Err(OperationWindowError::LeaseIdentityConflict)
1384 ));
1385 assert!(matches!(
1386 coordinator
1387 .begin_with_lease_id(workspace, generation(1), "owner", 2, 11, lease_id)
1388 .await,
1389 Err(OperationWindowError::LeaseIdentityConflict)
1390 ));
1391 }
1392
1393 #[tokio::test]
1394 async fn renewal_replaces_the_exact_live_lease_and_fences_stale_handles() {
1395 let workspace = WorkspaceId::derive(
1396 [13; 16],
1397 &crate::WorkspaceName::new("lease-renewal").expect("valid test workspace"),
1398 );
1399 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1400 let lease = coordinator
1401 .begin(workspace, generation(1), "owner", 1, 10)
1402 .await
1403 .expect("lease");
1404 let renewed = coordinator
1405 .renew(&lease, 2, 20)
1406 .await
1407 .expect("renew live lease");
1408 assert_eq!(renewed.expires_at_millis, 20);
1409 assert_eq!(renewed.pinned_parent, lease.pinned_parent);
1410 assert!(matches!(
1411 coordinator.renew(&lease, 3, 30).await,
1412 Err(OperationWindowError::StaleLease)
1413 ));
1414 assert!(matches!(
1415 coordinator.finish(&lease, 4).await,
1416 Ok(OperationWindowFinish::AlreadyClosed)
1417 ));
1418 assert!(matches!(
1419 coordinator.finish(&renewed, 4).await,
1420 Ok(OperationWindowFinish::Reconcile(_))
1421 ));
1422 }
1423
1424 #[tokio::test]
1425 async fn parent_observed_during_reconcile_is_not_lost() {
1426 let workspace = WorkspaceId::derive(
1427 [8; 16],
1428 &crate::WorkspaceName::new("agent").expect("valid test workspace"),
1429 );
1430 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1431 let lease = coordinator
1432 .begin(workspace, generation(1), "tool", 10, 100)
1433 .await
1434 .expect("lease");
1435 let OperationWindowFinish::Reconcile(reconcile) =
1436 coordinator.finish(&lease, 20).await.expect("finish")
1437 else {
1438 panic!("final close must claim reconciliation");
1439 };
1440 coordinator
1441 .observe_parent(workspace, generation(2))
1442 .await
1443 .expect("observe during reconcile");
1444 coordinator
1445 .complete_reconcile(workspace, reconcile.ticket)
1446 .await
1447 .expect("complete first reconcile");
1448 assert!(matches!(
1449 coordinator.inspect(workspace).await.expect("inspect").phase,
1450 OperationWindowPhase::Reconciling {
1451 pending_parent: Some(parent),
1452 ..
1453 } if parent == generation(2)
1454 ));
1455 }
1456
1457 #[tokio::test]
1458 async fn expired_lease_is_fenced_and_recovery_ticket_is_claimable() {
1459 let workspace = WorkspaceId::derive(
1460 [9; 16],
1461 &crate::WorkspaceName::new("expired").expect("valid test workspace"),
1462 );
1463 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1464 let lease = coordinator
1465 .begin(workspace, generation(1), "tool", 10, 20)
1466 .await
1467 .expect("lease");
1468 assert_eq!(
1469 coordinator.finish(&lease, 20).await.expect("expired close"),
1470 OperationWindowFinish::AlreadyClosed
1471 );
1472 let claimed = coordinator
1473 .claim_reconcile(workspace, 20)
1474 .await
1475 .expect("claim")
1476 .expect("recovery ticket");
1477 assert_eq!(claimed.pinned_parent, generation(1));
1478 assert!(matches!(
1479 coordinator
1480 .begin(workspace, generation(1), "late", 21, 30)
1481 .await,
1482 Err(OperationWindowError::Reconciling)
1483 ));
1484 }
1485
1486 #[tokio::test]
1487 async fn expired_close_is_persistently_fenced_while_another_lease_remains() {
1488 let workspace = WorkspaceId::derive(
1489 [10; 16],
1490 &crate::WorkspaceName::new("overlap-expiry").expect("valid test workspace"),
1491 );
1492 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1493 let expired = coordinator
1494 .begin(workspace, generation(1), "expired", 10, 20)
1495 .await
1496 .expect("expired lease");
1497 let active = coordinator
1498 .begin(workspace, generation(1), "active", 11, 40)
1499 .await
1500 .expect("active lease");
1501
1502 assert_eq!(
1503 coordinator
1504 .finish(&expired, 20)
1505 .await
1506 .expect("expired close"),
1507 OperationWindowFinish::AlreadyClosed
1508 );
1509 let snapshot = coordinator.inspect(workspace).await.expect("inspect");
1510 assert!(matches!(
1511 snapshot.phase,
1512 OperationWindowPhase::Active { ref leases, .. }
1513 if leases.len() == 1 && leases.contains_key(&active.lease_id)
1514 ));
1515 assert!(matches!(
1516 coordinator.finish(&active, 22).await.expect("active close"),
1517 OperationWindowFinish::Reconcile(_)
1518 ));
1519 }
1520
1521 #[tokio::test]
1522 async fn duplicate_close_persists_expiry_of_the_remaining_lease() {
1523 let workspace = WorkspaceId::derive(
1524 [11; 16],
1525 &crate::WorkspaceName::new("duplicate-expiry").expect("valid test workspace"),
1526 );
1527 let coordinator = OperationWindowCoordinator::new(MemoryOperationWindowStore::new());
1528 let first = coordinator
1529 .begin(workspace, generation(1), "first", 1, 10)
1530 .await
1531 .expect("first lease");
1532 coordinator
1533 .begin(workspace, generation(1), "second", 2, 40)
1534 .await
1535 .expect("second lease");
1536 assert_eq!(
1537 coordinator.finish(&first, 5).await.expect("first close"),
1538 OperationWindowFinish::StillActive { remaining: 1 }
1539 );
1540 assert_eq!(
1541 coordinator
1542 .finish(&first, 50)
1543 .await
1544 .expect("duplicate close"),
1545 OperationWindowFinish::AlreadyClosed
1546 );
1547 assert!(matches!(
1548 coordinator.inspect(workspace).await.expect("inspect").phase,
1549 OperationWindowPhase::Reconciling { .. }
1550 ));
1551 }
1552
1553 #[tokio::test]
1554 async fn stream_lease_gate_fences_one_writer_without_fencing_its_peer() {
1555 let clock = Arc::new(TestClock::default());
1556 clock.0.store(10, Ordering::SeqCst);
1557 let stream = Arc::new(acyclic_stream::MemoryStream::new_with_clock(
1558 acyclic_stream::MemoryLimits::default(),
1559 clock,
1560 ));
1561 let authority = StreamAuthorityStore::new(Arc::clone(&stream));
1562 let windows =
1563 OperationWindowCoordinator::new(StreamOperationWindowStore::new(Arc::clone(&stream)));
1564 let workspace = WorkspaceId::from_bytes([61; 16]);
1565 let authority_id = crate::kernel::volume_authority_id(workspace.volume_id());
1566 let unrelated_authority = AuthorityId::from_bytes([62; 16]);
1567 let cancellation = CancellationToken::new();
1568 AsyncAuthorityStore::create_authority(
1569 &authority,
1570 unrelated_authority,
1571 Epoch::GENESIS,
1572 WorkBudget::UNBOUNDED,
1573 &cancellation,
1574 )
1575 .await
1576 .expect("create unrelated authority");
1577 AsyncAuthorityStore::create_authority(
1578 &authority,
1579 authority_id,
1580 Epoch::GENESIS,
1581 WorkBudget::UNBOUNDED,
1582 &cancellation,
1583 )
1584 .await
1585 .expect("create authority");
1586 let first = windows
1587 .begin(workspace, generation(1), "first", 1, 100)
1588 .await
1589 .expect("first lease");
1590 let second = windows
1591 .begin(workspace, generation(1), "second", 2, 100)
1592 .await
1593 .expect("second lease");
1594 assert_eq!(
1595 windows.finish(&first, 3).await.expect("close first"),
1596 OperationWindowFinish::StillActive { remaining: 1 }
1597 );
1598
1599 let head = AsyncAuthorityStore::head(
1600 &authority,
1601 authority_id,
1602 WorkBudget::UNBOUNDED,
1603 &cancellation,
1604 )
1605 .await
1606 .expect("authority head")
1607 .value;
1608 let rejected = AsyncAuthorityStore::compare_and_append_guarded(
1609 &authority,
1610 crate::GuardedAppend {
1611 authority_id,
1612 epoch: head.epoch,
1613 expected: head,
1614 commit: ProposedCommit {
1615 operation_id: OperationId::from_bytes([71; 16]),
1616 fingerprint: Digest::from_bytes([72; 32]),
1617 payload: Bytes::from_static(b"first"),
1618 },
1619 permit: first.publication_permit(),
1620 },
1621 WorkBudget::UNBOUNDED,
1622 &cancellation,
1623 )
1624 .await
1625 .expect("guarded rejection")
1626 .value;
1627 assert!(matches!(rejected, crate::AppendOutcome::Fenced { .. }));
1628
1629 let committed = AsyncAuthorityStore::compare_and_append_guarded(
1630 &authority,
1631 crate::GuardedAppend {
1632 authority_id,
1633 epoch: head.epoch,
1634 expected: head,
1635 commit: ProposedCommit {
1636 operation_id: OperationId::from_bytes([73; 16]),
1637 fingerprint: Digest::from_bytes([74; 32]),
1638 payload: Bytes::from_static(b"second"),
1639 },
1640 permit: second.publication_permit(),
1641 },
1642 WorkBudget::UNBOUNDED,
1643 &cancellation,
1644 )
1645 .await
1646 .expect("guarded append")
1647 .value;
1648 assert!(matches!(committed, crate::AppendOutcome::Committed(_)));
1649
1650 let unrelated_head = AsyncAuthorityStore::head(
1651 &authority,
1652 unrelated_authority,
1653 WorkBudget::UNBOUNDED,
1654 &cancellation,
1655 )
1656 .await
1657 .expect("unrelated authority head")
1658 .value;
1659 let rejected = AsyncAuthorityStore::compare_and_append_guarded(
1660 &authority,
1661 crate::GuardedAppend {
1662 authority_id: unrelated_authority,
1663 epoch: unrelated_head.epoch,
1664 expected: unrelated_head,
1665 commit: ProposedCommit {
1666 operation_id: OperationId::from_bytes([75; 16]),
1667 fingerprint: Digest::from_bytes([76; 32]),
1668 payload: Bytes::from_static(b"cross-authority"),
1669 },
1670 permit: second.publication_permit(),
1671 },
1672 WorkBudget::UNBOUNDED,
1673 &cancellation,
1674 )
1675 .await
1676 .expect("cross-authority lease is a semantic fence")
1677 .value;
1678 assert!(matches!(rejected, crate::AppendOutcome::Fenced { .. }));
1679 assert_eq!(
1680 AsyncAuthorityStore::head(
1681 &authority,
1682 unrelated_authority,
1683 WorkBudget::UNBOUNDED,
1684 &cancellation,
1685 )
1686 .await
1687 .expect("unrelated authority remains unchanged")
1688 .value,
1689 unrelated_head
1690 );
1691 }
1692
1693 #[tokio::test]
1694 async fn provider_deadline_fences_without_a_separate_expiry_sweep() {
1695 let clock = Arc::new(TestClock::default());
1696 clock.0.store(10, Ordering::SeqCst);
1697 let stream = Arc::new(acyclic_stream::MemoryStream::new_with_clock(
1698 acyclic_stream::MemoryLimits::default(),
1699 clock.clone(),
1700 ));
1701 let authority = StreamAuthorityStore::new(Arc::clone(&stream));
1702 let windows =
1703 OperationWindowCoordinator::new(StreamOperationWindowStore::new(Arc::clone(&stream)));
1704 let workspace = WorkspaceId::from_bytes([81; 16]);
1705 let authority_id = crate::kernel::volume_authority_id(workspace.volume_id());
1706 let cancellation = CancellationToken::new();
1707 AsyncAuthorityStore::create_authority(
1708 &authority,
1709 authority_id,
1710 Epoch::GENESIS,
1711 WorkBudget::UNBOUNDED,
1712 &cancellation,
1713 )
1714 .await
1715 .expect("create authority");
1716 let lease = windows
1717 .begin(workspace, generation(1), "tool", 10, 20)
1718 .await
1719 .expect("lease");
1720 let head = AsyncAuthorityStore::head(
1721 &authority,
1722 authority_id,
1723 WorkBudget::UNBOUNDED,
1724 &cancellation,
1725 )
1726 .await
1727 .expect("authority head")
1728 .value;
1729 clock.0.store(20, Ordering::SeqCst);
1730 let outcome = AsyncAuthorityStore::compare_and_append_guarded(
1731 &authority,
1732 crate::GuardedAppend {
1733 authority_id,
1734 epoch: head.epoch,
1735 expected: head,
1736 commit: ProposedCommit {
1737 operation_id: OperationId::from_bytes([83; 16]),
1738 fingerprint: Digest::from_bytes([84; 32]),
1739 payload: Bytes::from_static(b"expired"),
1740 },
1741 permit: lease.publication_permit(),
1742 },
1743 WorkBudget::UNBOUNDED,
1744 &cancellation,
1745 )
1746 .await
1747 .expect("deadline is a semantic fence")
1748 .value;
1749 assert!(matches!(outcome, crate::AppendOutcome::Fenced { .. }));
1750 assert_eq!(
1751 AsyncAuthorityStore::head(
1752 &authority,
1753 authority_id,
1754 WorkBudget::UNBOUNDED,
1755 &cancellation,
1756 )
1757 .await
1758 .expect("unchanged authority")
1759 .value,
1760 head
1761 );
1762 }
1763}