1use crate::cancellation::CancellationToken;
4use crate::foundation::{
5 AuthorityId, Digest, DurableCommit, Epoch, GenerationId, Head, OperationId, ProposedCommit,
6 Sequence, authority_commit_digest,
7};
8use crate::kernel::{decode_published_generation, decode_volume_created, volume_authority_id};
9use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
10use crate::storage::{
11 AppendOutcome as FsAppendOutcome, AuthorityReceipt, AuthorityResult, AuthorityStoreError,
12 CreateAuthorityOutcome, FenceOutcome, ObjectId, ObjectRead, ObjectReadRequest,
13 ObjectReadRetention, ObjectReceipt, ObjectResult, ObjectStoreError, ObjectWrite,
14 PublicationPermit, PublicationReservation, ReplayLimit, ReservationOutcome, object_digest,
15};
16use crate::streams_record::StreamsDurableRecord;
17use crate::{AsyncAuthorityStore, AsyncObjectStore};
18use acyclic_objects::{
19 Condition, GetRequest, ObjectsError, ObjectsProvider, PutRequest, ReadTarget, wire,
20};
21use bytes::Bytes;
22use futures::StreamExt;
23use std::collections::BTreeMap;
24use std::sync::Arc;
25
26const STREAM_RECORD_LIMIT: u64 = acyclic_stream::MAX_RECORD_BYTES as u64;
27const GENESIS_DOMAIN: &[u8] = b"acyclic-fs-stream-genesis-v1\0";
28const EPOCH_DOMAIN: &[u8] = b"acyclic-fs-stream-epoch-v1\0";
29const LINEAGE_DOMAIN: &[u8] = b"acyclic-fs-stream-lineage-v1\0";
30const LINEAGE_TAIL_DOMAIN: &[u8] = b"acyclic-fs-stream-lineage-tail-v1\0";
31const AUTHORITY_MARKER: &[u8] = b"acyclic-fs-authority-v1\0";
32const PUBLICATION_GATE_FREE: &[u8] = b"acyclic-fs-publication-gate-v1\0free";
33const PUBLICATION_GATE_ACTIVE: &[u8] = b"acyclic-fs-publication-gate-v1\0active\0";
34
35#[derive(Clone)]
41pub struct StreamAuthorityStore<P> {
42 provider: Arc<P>,
43}
44
45impl<P> StreamAuthorityStore<P> {
46 #[must_use]
48 pub fn new(provider: Arc<P>) -> Self {
49 Self { provider }
50 }
51
52 #[must_use]
59 pub fn provider(&self) -> Arc<P> {
60 Arc::clone(&self.provider)
61 }
62}
63
64#[derive(Clone, Copy)]
65struct AuthoritySnapshot {
66 head: Head,
67 record_tail: u64,
68 epoch_tail: u64,
69}
70
71impl<P: acyclic_stream::StreamProvider> StreamAuthorityStore<P> {
72 pub async fn authorities(&self, maximum: u32) -> Result<Vec<AuthorityId>, AuthorityStoreError> {
76 let request_limit = maximum.checked_add(1).ok_or_else(|| {
77 AuthorityStoreError::Rejected("authority listing bound is too large".to_owned())
78 })?;
79 let parent = acyclic_stream::StreamPath::new("fs/authorities").map_err(map_stream_error)?;
80 let mut children = self
81 .provider
82 .children(acyclic_stream::ChildrenRequest {
83 parent: Some(parent),
84 limit: request_limit,
85 })
86 .await
87 .map_err(map_stream_error)?;
88 let mut authorities = Vec::new();
89 while let Some(child) = children.next().await {
90 let child = child.map_err(map_stream_error)?;
91 let encoded = child
92 .path
93 .as_str()
94 .strip_prefix("fs/authorities/")
95 .ok_or_else(|| {
96 AuthorityStoreError::Corrupt(
97 "Stream authority child escaped its parent".to_owned(),
98 )
99 })?;
100 if encoded.len() != 32 || encoded.contains('/') {
101 return Err(AuthorityStoreError::Corrupt(
102 "Stream authority child has a noncanonical identity".to_owned(),
103 ));
104 }
105 let mut bytes = [0_u8; 16];
106 hex::decode_to_slice(encoded, &mut bytes).map_err(|_| {
107 AuthorityStoreError::Corrupt(
108 "Stream authority child has a noncanonical identity".to_owned(),
109 )
110 })?;
111 authorities.push(AuthorityId::from_bytes(bytes));
112 }
113 if authorities.len() > maximum as usize {
114 return Err(AuthorityStoreError::Rejected(
115 "authority listing bound exceeded".to_owned(),
116 ));
117 }
118 Ok(authorities)
119 }
120
121 async fn snapshot(
122 &self,
123 authority_id: AuthorityId,
124 ) -> Result<AuthoritySnapshot, AuthorityStoreError> {
125 let records = records_path(authority_id)?;
126 let epochs = epochs_path(authority_id)?;
127 let record_tail = self
128 .provider
129 .tail(records.clone())
130 .await
131 .map_err(map_stream_error)?;
132 let epoch_tail = self
133 .provider
134 .tail(epochs.clone())
135 .await
136 .map_err(map_stream_error)?;
137 if record_tail == 0 || epoch_tail == 0 {
138 return Err(AuthorityStoreError::Corrupt(
139 "authority genesis paths are empty".to_owned(),
140 ));
141 }
142 let epoch_record = read_one(self.provider.as_ref(), epochs, epoch_tail - 1).await?;
143 let epoch = decode_epoch(&epoch_record.value, EPOCH_DOMAIN)?;
144 let head = if record_tail == 1 {
145 let genesis = read_one(self.provider.as_ref(), records, 0).await?;
146 let _genesis_epoch = decode_epoch(&genesis.value, GENESIS_DOMAIN)?;
147 Head::genesis(epoch)
148 } else {
149 let record = read_one(self.provider.as_ref(), records, record_tail - 1).await?;
150 let durable = decode_durable(authority_id, &record.value)?;
151 if durable.sequence.get().saturating_add(1) != record_tail {
152 return Err(AuthorityStoreError::Corrupt(
153 "Stream sequence does not match filesystem authority sequence".to_owned(),
154 ));
155 }
156 Head {
157 epoch,
158 sequence: durable.sequence,
159 digest: durable.digest,
160 }
161 };
162 Ok(AuthoritySnapshot {
163 head,
164 record_tail,
165 epoch_tail,
166 })
167 }
168
169 async fn publication_gate(
170 &self,
171 authority_id: AuthorityId,
172 ) -> Result<(u64, Option<OperationId>), AuthorityStoreError> {
173 let path = publication_gate_path(authority_id)?;
174 let tail = self
175 .provider
176 .tail(path.clone())
177 .await
178 .map_err(map_stream_error)?;
179 if tail == 0 {
180 return Err(AuthorityStoreError::Corrupt(
181 "authority publication gate is empty".to_owned(),
182 ));
183 }
184 let record = read_one(self.provider.as_ref(), path, tail - 1).await?;
185 Ok((tail, decode_publication_gate(&record.value)?))
186 }
187
188 async fn ensure_publication_gate(
189 &self,
190 authority_id: AuthorityId,
191 snapshot: AuthoritySnapshot,
192 mut work: WorkCounters,
193 budget: WorkBudget,
194 ) -> Result<WorkCounters, OperationFailure<AuthorityStoreError>> {
195 let path = publication_gate_path(authority_id).map_err(OperationFailure::before_work)?;
196 work = work
197 .checked_add(authority_read_work(1))
198 .map_err(|error| OperationFailure::new(error.into(), work))?;
199 admit_authority(work, budget)?;
200 let tail = match self.provider.tail(path.clone()).await {
201 Ok(tail) => tail,
202 Err(acyclic_stream::StreamError::NotFound) => 0,
203 Err(error) => {
204 return Err(OperationFailure::new(map_stream_error(error), work));
205 }
206 };
207 if tail != 0 {
208 work = work
209 .checked_add(authority_read_work(1))
210 .map_err(|error| OperationFailure::new(error.into(), work))?;
211 admit_authority(work, budget)?;
212 let record = read_one(self.provider.as_ref(), path, tail - 1)
213 .await
214 .map_err(|error| OperationFailure::new(error, work))?;
215 decode_publication_gate(&record.value)
216 .map_err(|error| OperationFailure::new(error, work))?;
217 return Ok(work);
218 }
219 let records =
220 records_path(authority_id).map_err(|error| OperationFailure::new(error, work))?;
221 let epochs =
222 epochs_path(authority_id).map_err(|error| OperationFailure::new(error, work))?;
223 let mut migration_identity = Vec::with_capacity(32);
224 migration_identity.extend_from_slice(&authority_id.into_bytes());
225 migration_identity.extend_from_slice(&snapshot.record_tail.to_le_bytes());
226 migration_identity.extend_from_slice(&snapshot.epoch_tail.to_le_bytes());
227 let request = acyclic_stream::CommitRequest {
228 conditions: vec![
229 acyclic_stream::CommitCondition::Tail {
230 path: records,
231 expected: snapshot.record_tail,
232 },
233 acyclic_stream::CommitCondition::Tail {
234 path: epochs,
235 expected: snapshot.epoch_tail,
236 },
237 acyclic_stream::CommitCondition::Absent { path: path.clone() },
238 ],
239 mutations: vec![acyclic_stream::CommitMutation::Append {
240 path: path.clone(),
241 records: vec![Bytes::from_static(PUBLICATION_GATE_FREE)],
242 }],
243 idempotency_key: stream_key(b"publication-gate", &migration_identity)
244 .map_err(|error| OperationFailure::new(error, work))?,
245 };
246 let mutation_work = work
247 .checked_add(authority_write_work(
248 1,
249 u64::try_from(PUBLICATION_GATE_FREE.len()).unwrap_or(u64::MAX),
250 ))
251 .map_err(|error| OperationFailure::new(error.into(), work))?;
252 let completed_work = mutation_work
253 .checked_add(authority_read_work(2))
254 .map_err(|error| OperationFailure::new(error.into(), mutation_work))?;
255 admit_authority(completed_work, budget)?;
256 match self.provider.commit(request).await {
257 Ok(
258 acyclic_stream::CommitOutcome::Committed(_)
259 | acyclic_stream::CommitOutcome::Conflict(_),
260 ) => {}
261 Err(error) => {
262 return Err(OperationFailure::new(
263 map_stream_error(error),
264 mutation_work,
265 ));
266 }
267 }
268 let tail = self
269 .provider
270 .tail(path.clone())
271 .await
272 .map_err(|error| OperationFailure::new(map_stream_error(error), completed_work))?;
273 if tail == 0 {
274 return Err(OperationFailure::new(
275 AuthorityStoreError::Rejected(
276 "authority publication gate migration conflicted".to_owned(),
277 ),
278 completed_work,
279 ));
280 }
281 let record = read_one(self.provider.as_ref(), path, tail - 1)
282 .await
283 .map_err(|error| OperationFailure::new(error, completed_work))?;
284 decode_publication_gate(&record.value)
285 .map_err(|error| OperationFailure::new(error, completed_work))?;
286 Ok(completed_work)
287 }
288
289 async fn operation(
290 &self,
291 authority_id: AuthorityId,
292 operation_id: OperationId,
293 ) -> Result<Option<DurableCommit>, AuthorityStoreError> {
294 let key = operation_key(authority_id, operation_id)?;
295 let Some(observation) = self
296 .provider
297 .inspect_idempotency(key)
298 .await
299 .map_err(map_stream_error)?
300 else {
301 return Ok(None);
302 };
303 let acyclic_stream::IdempotencyOutcome::Commit(outcome) = observation.outcome else {
304 return Err(AuthorityStoreError::Corrupt(
305 "operation identity retained another Stream mutation family".to_owned(),
306 ));
307 };
308 let acyclic_stream::CommitOutcome::Committed(envelope) = outcome else {
309 return Ok(None);
310 };
311 durable_from_operation_envelope(authority_id, operation_id, envelope).map(Some)
312 }
313
314 async fn existing_fork_destination(
315 &self,
316 destination_authority: AuthorityId,
317 source_generation: GenerationId,
318 forked_at: u64,
319 ) -> Result<Option<AuthoritySnapshot>, AuthorityStoreError> {
320 let snapshot = match self.snapshot(destination_authority).await {
321 Ok(snapshot) => snapshot,
322 Err(AuthorityStoreError::Missing) => return Ok(None),
323 Err(error) => return Err(error),
324 };
325 let locator = generation_path(destination_authority, source_generation)?;
326 let record = read_one(self.provider.as_ref(), locator, 0).await?;
327 let located_tail = decode_lineage_tail(&record.value)?;
328 if located_tail == 0 {
329 return Err(AuthorityStoreError::Corrupt(
330 "destination generation locator selected an empty lineage".to_owned(),
331 ));
332 }
333 let lineage = lineage_path(destination_authority)?;
334 let selected = read_one(self.provider.as_ref(), lineage, located_tail - 1).await?;
335 if located_tail == forked_at
336 && decode_lineage_generation(&selected.value)? == source_generation
337 {
338 Ok(Some(snapshot))
339 } else {
340 Err(AuthorityStoreError::Rejected(
341 "destination generation fork conflicts with existing state".to_owned(),
342 ))
343 }
344 }
345
346 async fn resolve_source_fork_point(
348 &self,
349 source_authority: AuthorityId,
350 source_generation: GenerationId,
351 ) -> Result<(acyclic_stream::StreamPath, u64), OperationFailure<AuthorityStoreError>> {
352 let source_lineage =
353 lineage_path(source_authority).map_err(OperationFailure::before_work)?;
354 let source_locator = generation_path(source_authority, source_generation)
355 .map_err(OperationFailure::before_work)?;
356 let locator_record = match read_one(self.provider.as_ref(), source_locator, 0).await {
357 Ok(record) => record,
358 Err(AuthorityStoreError::Missing) => {
359 return Err(OperationFailure::before_work(
360 AuthorityStoreError::Rejected(
361 "source authority has no generation lineage locator".to_owned(),
362 ),
363 ));
364 }
365 Err(error) => return Err(OperationFailure::before_work(error)),
366 };
367 let forked_at =
368 decode_lineage_tail(&locator_record.value).map_err(OperationFailure::before_work)?;
369 if forked_at == 0 {
370 return Err(OperationFailure::before_work(AuthorityStoreError::Corrupt(
371 "generation locator selected an empty lineage".to_owned(),
372 )));
373 }
374 let selected = read_one(
375 self.provider.as_ref(),
376 source_lineage.clone(),
377 forked_at - 1,
378 )
379 .await
380 .map_err(OperationFailure::before_work)?;
381 if decode_lineage_generation(&selected.value).map_err(OperationFailure::before_work)?
382 != source_generation
383 {
384 return Err(OperationFailure::before_work(AuthorityStoreError::Corrupt(
385 "generation locator and lineage disagree".to_owned(),
386 )));
387 }
388 Ok((source_lineage, forked_at))
389 }
390}
391
392impl<P: acyclic_stream::StreamProvider> AsyncAuthorityStore for StreamAuthorityStore<P> {
393 fn supports_generation_lineage_prefix(&self) -> bool {
394 true
395 }
396
397 async fn fork_generation_authority(
398 &self,
399 source: crate::GenerationForkSource,
400 destination_authority: AuthorityId,
401 operation_id: OperationId,
402 budget: WorkBudget,
403 cancellation: &CancellationToken,
404 ) -> AuthorityResult<CreateAuthorityOutcome> {
405 cancellation
406 .check()
407 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
408 if source.lineage == crate::GenerationFork::Independent {
409 return self
410 .create_authority(destination_authority, Epoch::GENESIS, budget, cancellation)
411 .await;
412 }
413 let (source_lineage, forked_at) = self
414 .resolve_source_fork_point(source.authority, source.generation)
415 .await?;
416 let destination_records =
417 records_path(destination_authority).map_err(OperationFailure::before_work)?;
418 let destination_root =
419 authority_path(destination_authority).map_err(OperationFailure::before_work)?;
420 let destination_epochs =
421 epochs_path(destination_authority).map_err(OperationFailure::before_work)?;
422 let destination_lineage =
423 lineage_path(destination_authority).map_err(OperationFailure::before_work)?;
424 let destination_gate =
425 publication_gate_path(destination_authority).map_err(OperationFailure::before_work)?;
426 let destination_locator = generation_path(destination_authority, source.generation)
427 .map_err(OperationFailure::before_work)?;
428 if let Some(snapshot) = self
429 .existing_fork_destination(destination_authority, source.generation, forked_at)
430 .await
431 .map_err(OperationFailure::before_work)?
432 {
433 let work = self
434 .ensure_publication_gate(
435 destination_authority,
436 snapshot,
437 authority_read_work(8),
438 budget,
439 )
440 .await?;
441 return authority_success(
442 CreateAuthorityOutcome::Existing(snapshot.head),
443 work,
444 budget,
445 );
446 }
447 let request = fork_generation_commit_request(
448 ForkDestinationPaths {
449 root: destination_root,
450 records: destination_records,
451 epochs: destination_epochs,
452 locator: destination_locator,
453 lineage: destination_lineage,
454 gate: destination_gate,
455 },
456 source_lineage,
457 forked_at,
458 operation_id,
459 )?;
460 let mut work = authority_write_work(4, 96);
461 work.authority_records_read = 3;
462 work.backend_read_operations = 3;
463 admit_authority(work, budget)?;
464 match self.provider.commit(request).await {
465 Ok(acyclic_stream::CommitOutcome::Committed(_)) => authority_success(
466 CreateAuthorityOutcome::Created(Head::genesis(Epoch::GENESIS)),
467 work,
468 budget,
469 ),
470 Ok(acyclic_stream::CommitOutcome::Conflict(_)) => {
471 work = work
472 .checked_add(authority_read_work(6))
473 .map_err(|error| OperationFailure::new(error.into(), work))?;
474 let snapshot = self
475 .existing_fork_destination(destination_authority, source.generation, forked_at)
476 .await
477 .map_err(|error| OperationFailure::new(error, work))?
478 .ok_or_else(|| {
479 OperationFailure::new(
480 AuthorityStoreError::Rejected(
481 "generation fork conflicted without creating its destination"
482 .to_owned(),
483 ),
484 work,
485 )
486 })?;
487 work = self
488 .ensure_publication_gate(destination_authority, snapshot, work, budget)
489 .await?;
490 authority_success(
491 CreateAuthorityOutcome::Existing(snapshot.head),
492 work,
493 budget,
494 )
495 }
496 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
497 }
498 }
499
500 async fn create_authority(
501 &self,
502 authority_id: AuthorityId,
503 genesis_epoch: Epoch,
504 budget: WorkBudget,
505 cancellation: &CancellationToken,
506 ) -> AuthorityResult<CreateAuthorityOutcome> {
507 cancellation
508 .check()
509 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
510 match self.snapshot(authority_id).await {
511 Ok(snapshot) => {
512 let work = self
513 .ensure_publication_gate(authority_id, snapshot, authority_read_work(2), budget)
514 .await?;
515 return authority_success(
516 CreateAuthorityOutcome::Existing(snapshot.head),
517 work,
518 budget,
519 );
520 }
521 Err(AuthorityStoreError::Missing) => {}
522 Err(error) => return Err(OperationFailure::before_work(error)),
523 }
524 let records = records_path(authority_id).map_err(OperationFailure::before_work)?;
525 let epochs = epochs_path(authority_id).map_err(OperationFailure::before_work)?;
526 let root = authority_path(authority_id).map_err(OperationFailure::before_work)?;
527 let gate = publication_gate_path(authority_id).map_err(OperationFailure::before_work)?;
528 let request = acyclic_stream::CommitRequest {
529 conditions: vec![
530 acyclic_stream::CommitCondition::Absent { path: root.clone() },
531 acyclic_stream::CommitCondition::Absent {
532 path: records.clone(),
533 },
534 acyclic_stream::CommitCondition::Absent {
535 path: epochs.clone(),
536 },
537 acyclic_stream::CommitCondition::Absent { path: gate.clone() },
538 ],
539 mutations: vec![
540 acyclic_stream::CommitMutation::Append {
541 path: root,
542 records: vec![Bytes::from_static(AUTHORITY_MARKER)],
543 },
544 acyclic_stream::CommitMutation::Append {
545 path: records,
546 records: vec![encode_epoch(GENESIS_DOMAIN, genesis_epoch)],
547 },
548 acyclic_stream::CommitMutation::Append {
549 path: epochs,
550 records: vec![encode_epoch(EPOCH_DOMAIN, genesis_epoch)],
551 },
552 acyclic_stream::CommitMutation::Append {
553 path: gate,
554 records: vec![Bytes::from_static(PUBLICATION_GATE_FREE)],
555 },
556 ],
557 idempotency_key: stream_key(b"create", &authority_id.into_bytes())
558 .map_err(OperationFailure::before_work)?,
559 };
560 let work = authority_write_work(2, 0);
561 admit_authority(work, budget)?;
562 match self.provider.commit(request).await {
563 Ok(acyclic_stream::CommitOutcome::Committed(_)) => authority_success(
564 CreateAuthorityOutcome::Created(Head::genesis(genesis_epoch)),
565 work,
566 budget,
567 ),
568 Ok(acyclic_stream::CommitOutcome::Conflict(_)) => {
569 let snapshot = self
570 .snapshot(authority_id)
571 .await
572 .map_err(|error| OperationFailure::new(error, work))?;
573 authority_success(
574 CreateAuthorityOutcome::Existing(snapshot.head),
575 work,
576 budget,
577 )
578 }
579 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
580 }
581 }
582
583 async fn head(
584 &self,
585 authority_id: AuthorityId,
586 budget: WorkBudget,
587 cancellation: &CancellationToken,
588 ) -> AuthorityResult<Head> {
589 cancellation
590 .check()
591 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
592 let snapshot = self
593 .snapshot(authority_id)
594 .await
595 .map_err(OperationFailure::before_work)?;
596 authority_success(snapshot.head, authority_read_work(2), budget)
597 }
598
599 async fn compare_and_append(
600 &self,
601 authority_id: AuthorityId,
602 epoch: Epoch,
603 expected: Head,
604 commit: ProposedCommit,
605 budget: WorkBudget,
606 cancellation: &CancellationToken,
607 ) -> AuthorityResult<FsAppendOutcome> {
608 self.compare_and_append_guarded(
609 crate::GuardedAppend {
610 authority_id,
611 epoch,
612 expected,
613 commit,
614 permit: PublicationPermit::Unrestricted,
615 },
616 budget,
617 cancellation,
618 )
619 .await
620 }
621
622 #[allow(
623 clippy::too_many_lines,
624 reason = "one atomic Stream transaction keeps fencing, idempotency, lineage, and terminal mapping auditable together"
625 )]
626 async fn compare_and_append_guarded(
627 &self,
628 request: crate::GuardedAppend,
629 budget: WorkBudget,
630 cancellation: &CancellationToken,
631 ) -> AuthorityResult<FsAppendOutcome> {
632 let crate::GuardedAppend {
633 authority_id,
634 epoch,
635 expected,
636 commit,
637 permit,
638 } = request;
639 cancellation
640 .check()
641 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
642 if let Some(existing) = self
643 .operation(authority_id, commit.operation_id)
644 .await
645 .map_err(OperationFailure::before_work)?
646 {
647 let value = if existing.fingerprint == commit.fingerprint {
648 FsAppendOutcome::AlreadyCommitted(existing)
649 } else {
650 FsAppendOutcome::IdempotencyConflict {
651 committed_fingerprint: existing.fingerprint,
652 }
653 };
654 return authority_success(value, authority_read_work(1), budget);
655 }
656 let snapshot = self
657 .snapshot(authority_id)
658 .await
659 .map_err(OperationFailure::before_work)?;
660 if snapshot.head.epoch != epoch {
661 return authority_success(
662 FsAppendOutcome::Fenced {
663 actual_epoch: snapshot.head.epoch,
664 },
665 authority_read_work(2),
666 budget,
667 );
668 }
669 if snapshot.head != expected {
670 return authority_success(
671 FsAppendOutcome::Conflict {
672 actual: snapshot.head,
673 },
674 authority_read_work(2),
675 budget,
676 );
677 }
678 let (gate_tail, active_reservation) = self
679 .publication_gate(authority_id)
680 .await
681 .map_err(OperationFailure::before_work)?;
682 let reservation_admitted = match permit {
683 PublicationPermit::Reservation {
684 operation_id,
685 gate_tail: permitted_tail,
686 expected: reserved_head,
687 } => {
688 permitted_tail == gate_tail
689 && active_reservation == Some(OperationId::from_bytes(operation_id))
690 && reserved_head == expected
691 }
692 PublicationPermit::Unrestricted => active_reservation.is_none(),
693 PublicationPermit::Lease {
694 authority_id: permitted_authority,
695 workspace_id,
696 ..
697 } => {
698 let permitted_workspace = crate::WorkspaceId::from_bytes(workspace_id);
699 let derived_authority =
700 crate::kernel::volume_authority_id(permitted_workspace.volume_id());
701 active_reservation.is_none()
702 && AuthorityId::from_bytes(permitted_authority) == authority_id
703 && derived_authority == authority_id
704 }
705 };
706 if !reservation_admitted {
707 return authority_success(
708 FsAppendOutcome::Fenced {
709 actual_epoch: snapshot.head.epoch,
710 },
711 authority_read_work(3),
712 budget,
713 );
714 }
715 let sequence = expected
716 .sequence
717 .checked_next()
718 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::SequenceExhausted))?;
719 let durable = DurableCommit {
720 epoch,
721 sequence,
722 operation_id: commit.operation_id,
723 fingerprint: commit.fingerprint,
724 previous_digest: expected.digest,
725 digest: authority_commit_digest(
726 authority_id,
727 epoch,
728 sequence,
729 commit.operation_id,
730 commit.fingerprint,
731 expected.digest,
732 &commit.payload,
733 ),
734 payload: commit.payload,
735 };
736 let encoded =
737 StreamsDurableRecord::encode(&durable, STREAM_RECORD_LIMIT).map_err(|error| {
738 OperationFailure::before_work(AuthorityStoreError::Rejected(error.to_string()))
739 })?;
740 let records = records_path(authority_id).map_err(OperationFailure::before_work)?;
741 let epochs = epochs_path(authority_id).map_err(OperationFailure::before_work)?;
742 let gate = publication_gate_path(authority_id).map_err(OperationFailure::before_work)?;
743 let mut conditions = vec![
744 acyclic_stream::CommitCondition::Tail {
745 path: records.clone(),
746 expected: snapshot.record_tail,
747 },
748 acyclic_stream::CommitCondition::Tail {
749 path: epochs,
750 expected: snapshot.epoch_tail,
751 },
752 acyclic_stream::CommitCondition::Tail {
753 path: gate.clone(),
754 expected: gate_tail,
755 },
756 ];
757 let mut lease_deadline = None;
758 let lease_path = match permit {
759 PublicationPermit::Unrestricted | PublicationPermit::Reservation { .. } => None,
760 PublicationPermit::Lease {
761 authority_id: _,
762 workspace_id,
763 lease_id,
764 expires_at_millis,
765 } => {
766 lease_deadline = Some(expires_at_millis);
767 let path = crate::operation_window::stream_lease_path(
768 crate::WorkspaceId::from_bytes(workspace_id),
769 crate::OperationLeaseId::from_bytes(lease_id),
770 )
771 .map_err(|error| OperationFailure::before_work(map_stream_error(error)))?;
772 conditions.push(acyclic_stream::CommitCondition::Tail {
773 path: path.clone(),
774 expected: 1,
775 });
776 Some(path)
777 }
778 };
779 let mut mutations = vec![acyclic_stream::CommitMutation::Append {
780 path: records,
781 records: vec![encoded],
782 }];
783 let mut authority_records = 1_u64;
784 let mut authority_bytes = u64::try_from(durable.payload.len()).unwrap_or(u64::MAX);
785 if let Some(generation) = generation_from_payload(authority_id, sequence, &durable.payload)
786 .map_err(OperationFailure::before_work)?
787 {
788 let lineage = lineage_path(authority_id).map_err(OperationFailure::before_work)?;
789 let locator =
790 generation_path(authority_id, generation).map_err(OperationFailure::before_work)?;
791 match self.provider.tail(locator.clone()).await {
792 Ok(1) => {
793 let record = read_one(self.provider.as_ref(), locator, 0)
794 .await
795 .map_err(OperationFailure::before_work)?;
796 let located_tail = decode_lineage_tail(&record.value)
797 .map_err(OperationFailure::before_work)?;
798 if located_tail == 0 {
799 return Err(OperationFailure::before_work(AuthorityStoreError::Corrupt(
800 "generation locator selected an empty lineage".to_owned(),
801 )));
802 }
803 let lineage_record =
804 read_one(self.provider.as_ref(), lineage, located_tail - 1)
805 .await
806 .map_err(OperationFailure::before_work)?;
807 if decode_lineage_generation(&lineage_record.value)
808 .map_err(OperationFailure::before_work)?
809 != generation
810 {
811 return Err(OperationFailure::before_work(AuthorityStoreError::Corrupt(
812 "generation locator does not identify its lineage record".to_owned(),
813 )));
814 }
815 }
816 Err(acyclic_stream::StreamError::NotFound) => {
817 let lineage_tail = match self.provider.tail(lineage.clone()).await {
818 Ok(tail) => tail,
819 Err(acyclic_stream::StreamError::NotFound) => 0,
820 Err(error) => {
821 return Err(OperationFailure::before_work(map_stream_error(error)));
822 }
823 };
824 conditions.push(if lineage_tail == 0 {
825 acyclic_stream::CommitCondition::Absent {
826 path: lineage.clone(),
827 }
828 } else {
829 acyclic_stream::CommitCondition::Tail {
830 path: lineage.clone(),
831 expected: lineage_tail,
832 }
833 });
834 conditions.push(acyclic_stream::CommitCondition::Absent {
835 path: locator.clone(),
836 });
837 let lineage_record = encode_lineage_generation(generation);
838 let locator_record = encode_lineage_tail(lineage_tail.saturating_add(1));
839 authority_bytes = authority_bytes
840 .saturating_add(u64::try_from(lineage_record.len()).unwrap_or(u64::MAX))
841 .saturating_add(u64::try_from(locator_record.len()).unwrap_or(u64::MAX));
842 mutations.push(acyclic_stream::CommitMutation::Append {
843 path: lineage,
844 records: vec![lineage_record],
845 });
846 mutations.push(acyclic_stream::CommitMutation::Append {
847 path: locator,
848 records: vec![locator_record],
849 });
850 authority_records = authority_records.saturating_add(2);
851 }
852 Ok(_) => {
853 return Err(OperationFailure::before_work(AuthorityStoreError::Corrupt(
854 "generation locator contains an invalid record count".to_owned(),
855 )));
856 }
857 Err(error) => {
858 return Err(OperationFailure::before_work(map_stream_error(error)));
859 }
860 }
861 }
862 let request = acyclic_stream::CommitRequest {
863 conditions,
864 mutations,
865 idempotency_key: operation_key(authority_id, durable.operation_id)
866 .map_err(OperationFailure::before_work)?,
867 };
868 let work = authority_write_work(authority_records, authority_bytes);
869 admit_authority(work, budget)?;
870 let commit_result = if let Some(deadline) = lease_deadline {
871 self.provider.commit_before(request, deadline).await
872 } else {
873 self.provider.commit(request).await
874 };
875 match commit_result {
876 Ok(acyclic_stream::CommitOutcome::Committed(_)) => {
877 authority_success(FsAppendOutcome::Committed(durable), work, budget)
878 }
879 Ok(acyclic_stream::CommitOutcome::Conflict(conflicts)) => {
880 if let Some(existing) = self
881 .operation(authority_id, durable.operation_id)
882 .await
883 .map_err(|error| OperationFailure::new(error, work))?
884 {
885 let value = if existing.fingerprint == durable.fingerprint {
886 FsAppendOutcome::AlreadyCommitted(existing)
887 } else {
888 FsAppendOutcome::IdempotencyConflict {
889 committed_fingerprint: existing.fingerprint,
890 }
891 };
892 return authority_success(value, work, budget);
893 }
894 let actual = self
895 .snapshot(authority_id)
896 .await
897 .map_err(|error| OperationFailure::new(error, work))?
898 .head;
899 let gate_rejected = conflicts.iter().any(|conflict| match conflict {
900 acyclic_stream::CommitConflict::Tail { path, .. }
901 | acyclic_stream::CommitConflict::Exists { path }
902 | acyclic_stream::CommitConflict::Retired { path } => path == &gate,
903 });
904 let permit_rejected = gate_rejected
905 || lease_path.as_ref().is_some_and(|lease_path| {
906 conflicts.iter().any(|conflict| match conflict {
907 acyclic_stream::CommitConflict::Tail { path, .. }
908 | acyclic_stream::CommitConflict::Exists { path }
909 | acyclic_stream::CommitConflict::Retired { path } => {
910 path == lease_path
911 }
912 })
913 });
914 let value = if permit_rejected {
915 FsAppendOutcome::Fenced {
916 actual_epoch: actual.epoch,
917 }
918 } else if actual.epoch == epoch {
919 FsAppendOutcome::Conflict { actual }
920 } else {
921 FsAppendOutcome::Fenced {
922 actual_epoch: actual.epoch,
923 }
924 };
925 authority_success(value, work, budget)
926 }
927 Err(acyclic_stream::StreamError::IdempotencyMismatch) => {
928 let existing = self
929 .operation(authority_id, durable.operation_id)
930 .await
931 .map_err(|error| OperationFailure::new(error, work))?
932 .ok_or_else(|| {
933 OperationFailure::new(
934 AuthorityStoreError::Corrupt(
935 "Stream idempotency mismatch has no operation record".to_owned(),
936 ),
937 work,
938 )
939 })?;
940 authority_success(
941 FsAppendOutcome::IdempotencyConflict {
942 committed_fingerprint: existing.fingerprint,
943 },
944 work,
945 budget,
946 )
947 }
948 Err(acyclic_stream::StreamError::DeadlineElapsed) => authority_success(
949 FsAppendOutcome::Fenced {
950 actual_epoch: epoch,
951 },
952 work,
953 budget,
954 ),
955 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
956 }
957 }
958
959 async fn reserve_publication(
960 &self,
961 authority_id: AuthorityId,
962 expected: Head,
963 operation_id: OperationId,
964 budget: WorkBudget,
965 cancellation: &CancellationToken,
966 ) -> AuthorityResult<ReservationOutcome> {
967 cancellation
968 .check()
969 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
970 let snapshot = self
971 .snapshot(authority_id)
972 .await
973 .map_err(OperationFailure::before_work)?;
974 if snapshot.head != expected {
975 return authority_success(
976 ReservationOutcome::Conflict {
977 actual: snapshot.head,
978 },
979 authority_read_work(2),
980 budget,
981 );
982 }
983 let gate = publication_gate_path(authority_id).map_err(OperationFailure::before_work)?;
984 let (gate_tail, active) = self
985 .publication_gate(authority_id)
986 .await
987 .map_err(OperationFailure::before_work)?;
988 if active == Some(operation_id) {
989 return authority_success(
990 ReservationOutcome::AlreadyReserved(PublicationReservation {
991 authority_id,
992 operation_id,
993 expected,
994 gate_tail,
995 }),
996 authority_read_work(3),
997 budget,
998 );
999 }
1000 if active.is_some() {
1001 return authority_success(
1002 ReservationOutcome::Conflict {
1003 actual: snapshot.head,
1004 },
1005 authority_read_work(3),
1006 budget,
1007 );
1008 }
1009 let records = records_path(authority_id).map_err(OperationFailure::before_work)?;
1010 let epochs = epochs_path(authority_id).map_err(OperationFailure::before_work)?;
1011 let request = acyclic_stream::CommitRequest {
1012 conditions: vec![
1013 acyclic_stream::CommitCondition::Tail {
1014 path: records,
1015 expected: snapshot.record_tail,
1016 },
1017 acyclic_stream::CommitCondition::Tail {
1018 path: epochs,
1019 expected: snapshot.epoch_tail,
1020 },
1021 acyclic_stream::CommitCondition::Tail {
1022 path: gate.clone(),
1023 expected: gate_tail,
1024 },
1025 ],
1026 mutations: vec![acyclic_stream::CommitMutation::Append {
1027 path: gate,
1028 records: vec![encode_active_reservation(operation_id)],
1029 }],
1030 idempotency_key: publication_reservation_key(
1031 b"reserve-publication",
1032 authority_id,
1033 operation_id,
1034 gate_tail,
1035 )
1036 .map_err(OperationFailure::before_work)?,
1037 };
1038 let work = authority_write_work(1, 16);
1039 admit_authority(work, budget)?;
1040 match self.provider.commit(request).await {
1041 Ok(acyclic_stream::CommitOutcome::Committed(_)) => authority_success(
1042 ReservationOutcome::Reserved(PublicationReservation {
1043 authority_id,
1044 operation_id,
1045 expected,
1046 gate_tail: gate_tail + 1,
1047 }),
1048 work,
1049 budget,
1050 ),
1051 Ok(acyclic_stream::CommitOutcome::Conflict(_)) => {
1052 let actual = self
1053 .snapshot(authority_id)
1054 .await
1055 .map_err(|error| OperationFailure::new(error, work))?
1056 .head;
1057 authority_success(ReservationOutcome::Conflict { actual }, work, budget)
1058 }
1059 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
1060 }
1061 }
1062
1063 async fn release_publication(
1064 &self,
1065 reservation: PublicationReservation,
1066 budget: WorkBudget,
1067 cancellation: &CancellationToken,
1068 ) -> AuthorityResult<()> {
1069 cancellation
1070 .check()
1071 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
1072 let release_key = publication_reservation_key(
1073 b"release-publication",
1074 reservation.authority_id,
1075 reservation.operation_id,
1076 reservation.gate_tail,
1077 )
1078 .map_err(OperationFailure::before_work)?;
1079 let mut work = authority_read_work(1);
1083 admit_authority(work, budget)?;
1084 if let Some(observation) = self
1085 .provider
1086 .inspect_idempotency(release_key.clone())
1087 .await
1088 .map_err(|error| OperationFailure::new(map_stream_error(error), work))?
1089 {
1090 return match observation.outcome {
1091 acyclic_stream::IdempotencyOutcome::Commit(
1092 acyclic_stream::CommitOutcome::Committed(_),
1093 ) => authority_success((), work, budget),
1094 acyclic_stream::IdempotencyOutcome::Commit(
1095 acyclic_stream::CommitOutcome::Conflict(_),
1096 ) => Err(OperationFailure::new(
1097 AuthorityStoreError::Rejected(
1098 "publication reservation is no longer active".to_owned(),
1099 ),
1100 work,
1101 )),
1102 _ => Err(OperationFailure::new(
1103 AuthorityStoreError::Corrupt(
1104 "publication release identity retained another Stream mutation family"
1105 .to_owned(),
1106 ),
1107 work,
1108 )),
1109 };
1110 }
1111 let gate = publication_gate_path(reservation.authority_id)
1112 .map_err(|error| OperationFailure::new(error, work))?;
1113 work = work
1114 .checked_add(authority_read_work(2))
1115 .map_err(|error| OperationFailure::new(error.into(), work))?;
1116 admit_authority(work, budget)?;
1117 let (current_tail, active) = self
1118 .publication_gate(reservation.authority_id)
1119 .await
1120 .map_err(|error| OperationFailure::new(error, work))?;
1121 if current_tail != reservation.gate_tail || active != Some(reservation.operation_id) {
1122 return Err(OperationFailure::new(
1123 AuthorityStoreError::Rejected(
1124 "publication reservation is no longer owned by this operation".to_owned(),
1125 ),
1126 work,
1127 ));
1128 }
1129 let request = acyclic_stream::CommitRequest {
1130 conditions: vec![acyclic_stream::CommitCondition::Tail {
1131 path: gate.clone(),
1132 expected: reservation.gate_tail,
1133 }],
1134 mutations: vec![acyclic_stream::CommitMutation::Append {
1135 path: gate,
1136 records: vec![Bytes::from_static(PUBLICATION_GATE_FREE)],
1137 }],
1138 idempotency_key: release_key,
1139 };
1140 work = work
1141 .checked_add(authority_write_work(1, 0))
1142 .map_err(|error| OperationFailure::new(error.into(), work))?;
1143 admit_authority(work, budget)?;
1144 match self.provider.commit(request).await {
1145 Ok(acyclic_stream::CommitOutcome::Committed(_)) => authority_success((), work, budget),
1146 Ok(acyclic_stream::CommitOutcome::Conflict(_)) => Err(OperationFailure::new(
1147 AuthorityStoreError::Rejected(
1148 "publication reservation is no longer active".to_owned(),
1149 ),
1150 work,
1151 )),
1152 Err(acyclic_stream::StreamError::IdempotencyMismatch) => Err(OperationFailure::new(
1153 AuthorityStoreError::Rejected(
1154 "publication reservation release identity was reused".to_owned(),
1155 ),
1156 work,
1157 )),
1158 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
1159 }
1160 }
1161
1162 async fn replay(
1163 &self,
1164 authority_id: AuthorityId,
1165 after: Sequence,
1166 limit: ReplayLimit,
1167 budget: WorkBudget,
1168 cancellation: &CancellationToken,
1169 ) -> AuthorityResult<Vec<DurableCommit>> {
1170 cancellation
1171 .check()
1172 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
1173 if limit.records == 0 || limit.payload_bytes == 0 {
1174 return Err(OperationFailure::before_work(
1175 AuthorityStoreError::InvalidReplayLimit,
1176 ));
1177 }
1178 let path = records_path(authority_id).map_err(OperationFailure::before_work)?;
1179 let mut stream = self
1180 .provider
1181 .read(acyclic_stream::ReadRequest {
1182 path,
1183 from: after.get().saturating_add(1),
1184 limit: limit.records,
1185 })
1186 .await
1187 .map_err(|error| OperationFailure::before_work(map_stream_error(error)))?;
1188 let mut commits = Vec::new();
1189 let mut payload_bytes = 0_u64;
1190 while let Some(record) = stream.next().await {
1191 let record =
1192 record.map_err(|error| OperationFailure::before_work(map_stream_error(error)))?;
1193 let durable = decode_durable(authority_id, &record.value)
1194 .map_err(OperationFailure::before_work)?;
1195 payload_bytes = payload_bytes
1196 .checked_add(u64::try_from(durable.payload.len()).unwrap_or(u64::MAX))
1197 .ok_or_else(|| {
1198 OperationFailure::before_work(AuthorityStoreError::ReplayRecordTooLarge {
1199 observed: u64::MAX,
1200 maximum: limit.payload_bytes,
1201 })
1202 })?;
1203 if payload_bytes > limit.payload_bytes {
1204 if commits.is_empty() {
1205 return Err(OperationFailure::before_work(
1206 AuthorityStoreError::ReplayRecordTooLarge {
1207 observed: payload_bytes,
1208 maximum: limit.payload_bytes,
1209 },
1210 ));
1211 }
1212 break;
1213 }
1214 commits.push(durable);
1215 }
1216 let work = WorkCounters {
1217 authority_records_read: u64::try_from(commits.len()).unwrap_or(u64::MAX),
1218 authority_bytes_read: payload_bytes,
1219 backend_read_operations: 1,
1220 ..WorkCounters::default()
1221 };
1222 authority_success(commits, work, budget)
1223 }
1224
1225 async fn fence(
1226 &self,
1227 authority_id: AuthorityId,
1228 expected: Head,
1229 budget: WorkBudget,
1230 cancellation: &CancellationToken,
1231 ) -> AuthorityResult<FenceOutcome> {
1232 cancellation
1233 .check()
1234 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
1235 let snapshot = self
1236 .snapshot(authority_id)
1237 .await
1238 .map_err(OperationFailure::before_work)?;
1239 if snapshot.head != expected {
1240 return authority_success(
1241 FenceOutcome::Conflict {
1242 actual: snapshot.head,
1243 },
1244 authority_read_work(2),
1245 budget,
1246 );
1247 }
1248 let next =
1249 expected.epoch.get().checked_add(1).ok_or_else(|| {
1250 OperationFailure::before_work(AuthorityStoreError::EpochExhausted)
1251 })?;
1252 let epoch = Epoch::new(next)
1253 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::EpochExhausted))?;
1254 let records = records_path(authority_id).map_err(OperationFailure::before_work)?;
1255 let epochs = epochs_path(authority_id).map_err(OperationFailure::before_work)?;
1256 let mut fence_identity = Vec::with_capacity(64);
1257 fence_identity.extend_from_slice(&authority_id.into_bytes());
1258 fence_identity.extend_from_slice(&expected.epoch.get().to_le_bytes());
1259 fence_identity.extend_from_slice(&expected.sequence.get().to_le_bytes());
1260 fence_identity.extend_from_slice(expected.digest.as_bytes());
1261 let request = acyclic_stream::CommitRequest {
1262 conditions: vec![
1263 acyclic_stream::CommitCondition::Tail {
1264 path: records,
1265 expected: snapshot.record_tail,
1266 },
1267 acyclic_stream::CommitCondition::Tail {
1268 path: epochs.clone(),
1269 expected: snapshot.epoch_tail,
1270 },
1271 ],
1272 mutations: vec![acyclic_stream::CommitMutation::Append {
1273 path: epochs,
1274 records: vec![encode_epoch(EPOCH_DOMAIN, epoch)],
1275 }],
1276 idempotency_key: stream_key(b"fence", &fence_identity)
1277 .map_err(OperationFailure::before_work)?,
1278 };
1279 let work = authority_write_work(1, 0);
1280 admit_authority(work, budget)?;
1281 match self.provider.commit(request).await {
1282 Ok(acyclic_stream::CommitOutcome::Committed(_)) => authority_success(
1283 FenceOutcome::Advanced(Head { epoch, ..expected }),
1284 work,
1285 budget,
1286 ),
1287 Ok(acyclic_stream::CommitOutcome::Conflict(_)) => {
1288 let actual = self
1289 .snapshot(authority_id)
1290 .await
1291 .map_err(|error| OperationFailure::new(error, work))?
1292 .head;
1293 authority_success(FenceOutcome::Conflict { actual }, work, budget)
1294 }
1295 Err(error) => Err(OperationFailure::new(map_stream_error(error), work)),
1296 }
1297 }
1298
1299 async fn find_operation(
1300 &self,
1301 authority_id: AuthorityId,
1302 operation_id: OperationId,
1303 budget: WorkBudget,
1304 cancellation: &CancellationToken,
1305 ) -> AuthorityResult<Option<DurableCommit>> {
1306 cancellation
1307 .check()
1308 .map_err(|_| OperationFailure::before_work(AuthorityStoreError::Cancelled))?;
1309 let value = self
1310 .operation(authority_id, operation_id)
1311 .await
1312 .map_err(OperationFailure::before_work)?;
1313 let records_read = u64::from(value.is_some());
1314 authority_success(
1315 value,
1316 WorkCounters {
1317 authority_records_read: records_read,
1318 backend_read_operations: 1,
1319 ..WorkCounters::default()
1320 },
1321 budget,
1322 )
1323 }
1324}
1325
1326#[derive(Clone)]
1331pub struct ProviderObjectStore<P> {
1332 provider: Arc<P>,
1333 bucket: wire::BucketRef,
1334}
1335
1336fn provider_put_request(bucket: &wire::BucketRef, write: &ObjectWrite) -> PutRequest {
1337 PutRequest {
1338 bucket: bucket.clone(),
1339 object_key: object_key(write.object_id),
1340 body: write.bytes.clone(),
1341 metadata: wire::ObjectMetadata {
1342 content_type: "application/vnd.acyclic.fs-object-v1".to_owned(),
1343 ..wire::ObjectMetadata::default()
1344 },
1345 condition: Some(Condition::IfAbsent),
1346 idempotency_key: Some(format!(
1347 "fs-object-{}",
1348 hex::encode(write.object_id.digest.as_bytes())
1349 )),
1350 }
1351}
1352
1353impl<P> ProviderObjectStore<P> {
1354 #[must_use]
1356 pub fn new(provider: Arc<P>, bucket: wire::BucketRef) -> Self {
1357 Self { provider, bucket }
1358 }
1359
1360 #[must_use]
1362 pub fn bucket(&self) -> &wire::BucketRef {
1363 &self.bucket
1364 }
1365
1366 #[must_use]
1368 pub fn provider(&self) -> &Arc<P> {
1369 &self.provider
1370 }
1371}
1372
1373impl<P: ObjectsProvider> AsyncObjectStore for ProviderObjectStore<P> {
1374 async fn put(
1375 &self,
1376 object_id: ObjectId,
1377 bytes: Bytes,
1378 budget: WorkBudget,
1379 cancellation: &CancellationToken,
1380 ) -> ObjectResult<()> {
1381 cancellation
1382 .check()
1383 .map_err(|_| OperationFailure::before_work(ObjectStoreError::Cancelled))?;
1384 if object_digest(object_id.kind, &bytes) != object_id.digest {
1385 return Err(OperationFailure::before_work(
1386 ObjectStoreError::DigestMismatch,
1387 ));
1388 }
1389 let byte_count = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
1390 let mut work = WorkCounters {
1391 backend_write_operations: 1,
1392 object_bytes_written: byte_count,
1393 bytes_hashed: byte_count,
1394 ..WorkCounters::default()
1395 };
1396 admit(work, budget)?;
1397 let request = PutRequest {
1398 bucket: self.bucket.clone(),
1399 object_key: object_key(object_id),
1400 body: bytes,
1401 metadata: wire::ObjectMetadata {
1402 content_type: "application/vnd.acyclic.fs-object-v1".to_owned(),
1403 ..wire::ObjectMetadata::default()
1404 },
1405 condition: Some(Condition::IfAbsent),
1406 idempotency_key: Some(format!(
1407 "fs-object-{}",
1408 hex::encode(object_id.digest.as_bytes())
1409 )),
1410 };
1411 match self.provider.put(request).await {
1412 Ok(version) if version.size == byte_count => success((), work, budget),
1413 Ok(_) => Err(OperationFailure::new(ObjectStoreError::Corrupt, work)),
1414 Err(ObjectsError::PreconditionFailed) => {
1415 work.backend_read_operations = work.backend_read_operations.saturating_add(1);
1416 let existing = self
1417 .provider
1418 .get(read_request(&self.bucket, object_id, byte_count))
1419 .await
1420 .map_err(|error| OperationFailure::new(map_objects_error(error), work))?;
1421 let existing_bytes = u64::try_from(existing.body.len()).unwrap_or(u64::MAX);
1422 work.object_bytes_read = work.object_bytes_read.saturating_add(existing_bytes);
1423 work.bytes_hashed = work.bytes_hashed.saturating_add(existing_bytes);
1424 if existing_bytes != byte_count
1425 || object_digest(object_id.kind, &existing.body) != object_id.digest
1426 {
1427 return Err(OperationFailure::new(ObjectStoreError::Corrupt, work));
1428 }
1429 success((), work, budget)
1430 }
1431 Err(error) => Err(OperationFailure::new(map_objects_error(error), work)),
1432 }
1433 }
1434
1435 async fn put_many(
1436 &self,
1437 writes: &[ObjectWrite],
1438 budget: WorkBudget,
1439 cancellation: &CancellationToken,
1440 ) -> ObjectResult<()> {
1441 cancellation
1442 .check()
1443 .map_err(|_| OperationFailure::before_work(ObjectStoreError::Cancelled))?;
1444 if writes.is_empty() {
1445 return Err(OperationFailure::before_work(ObjectStoreError::Rejected(
1446 "object write batch is empty".to_owned(),
1447 )));
1448 }
1449 let (requests, unique_writes, mut work) =
1450 prepare_provider_put_batch(&self.bucket, writes, budget, cancellation)?;
1451 work.backend_write_operations = 1;
1452 admit(work, budget)?;
1453 let results = self.provider.put_batch(requests).await;
1454 if results.len() != unique_writes.len() {
1455 return Err(OperationFailure::new(ObjectStoreError::Corrupt, work));
1456 }
1457 for (write_index, result) in unique_writes.into_iter().zip(results) {
1458 let write = writes
1459 .get(write_index)
1460 .ok_or_else(|| OperationFailure::new(ObjectStoreError::Corrupt, work))?;
1461 let byte_count = u64::try_from(write.bytes.len()).unwrap_or(u64::MAX);
1462 match result {
1463 Ok(version) if version.size == byte_count => {}
1464 Ok(_) => return Err(OperationFailure::new(ObjectStoreError::Corrupt, work)),
1465 Err(ObjectsError::PreconditionFailed) => {
1466 cancellation
1467 .check()
1468 .map_err(|_| OperationFailure::new(ObjectStoreError::Cancelled, work))?;
1469 work.backend_read_operations = work.backend_read_operations.saturating_add(1);
1470 let existing = self
1471 .provider
1472 .get(read_request(&self.bucket, write.object_id, byte_count))
1473 .await
1474 .map_err(|error| OperationFailure::new(map_objects_error(error), work))?;
1475 let existing_bytes = u64::try_from(existing.body.len()).unwrap_or(u64::MAX);
1476 work.object_bytes_read = work.object_bytes_read.saturating_add(existing_bytes);
1477 work.bytes_hashed = work.bytes_hashed.saturating_add(existing_bytes);
1478 if existing_bytes != byte_count
1479 || object_digest(write.object_id.kind, &existing.body)
1480 != write.object_id.digest
1481 {
1482 return Err(OperationFailure::new(ObjectStoreError::Corrupt, work));
1483 }
1484 }
1485 Err(error) => {
1486 return Err(OperationFailure::new(map_objects_error(error), work));
1487 }
1488 }
1489 }
1490 success((), work, budget)
1491 }
1492
1493 async fn read(
1494 &self,
1495 object_id: ObjectId,
1496 maximum_bytes: u64,
1497 budget: WorkBudget,
1498 cancellation: &CancellationToken,
1499 ) -> ObjectResult<ObjectRead> {
1500 cancellation
1501 .check()
1502 .map_err(|_| OperationFailure::before_work(ObjectStoreError::Cancelled))?;
1503 let value = self
1504 .provider
1505 .get(read_request(&self.bucket, object_id, maximum_bytes))
1506 .await
1507 .map_err(|error| OperationFailure::before_work(map_objects_error(error)))?;
1508 let observed = u64::try_from(value.body.len()).unwrap_or(u64::MAX);
1509 let work = WorkCounters {
1510 object_probes: 1,
1511 backend_read_operations: 1,
1512 object_bytes_read: observed,
1513 bytes_hashed: observed,
1514 bytes_copied: observed,
1515 allocation_operations: u64::from(!value.body.is_empty()),
1516 peak_allocation_bytes: observed,
1517 ..WorkCounters::default()
1518 };
1519 if observed > maximum_bytes {
1520 return Err(OperationFailure::new(
1521 ObjectStoreError::TooLarge {
1522 observed,
1523 maximum: maximum_bytes,
1524 },
1525 work,
1526 ));
1527 }
1528 if object_digest(object_id.kind, &value.body) != object_id.digest {
1529 return Err(OperationFailure::new(ObjectStoreError::Corrupt, work));
1530 }
1531 success(
1532 ObjectRead {
1533 bytes: value.body,
1534 retention: ObjectReadRetention::Owned {
1535 logical_bytes: observed,
1536 },
1537 },
1538 work,
1539 budget,
1540 )
1541 }
1542
1543 async fn read_many(
1544 &self,
1545 requests: &[ObjectReadRequest],
1546 budget: WorkBudget,
1547 cancellation: &CancellationToken,
1548 ) -> ObjectResult<Vec<ObjectRead>> {
1549 if requests.is_empty() {
1550 return Err(OperationFailure::before_work(ObjectStoreError::Rejected(
1551 "object read batch is empty".to_owned(),
1552 )));
1553 }
1554 cancellation
1555 .check()
1556 .map_err(|_| OperationFailure::before_work(ObjectStoreError::Cancelled))?;
1557 let mut values = Vec::new();
1558 values.try_reserve_exact(requests.len()).map_err(|_| {
1559 OperationFailure::before_work(ObjectStoreError::Rejected(
1560 "object batch allocation failed".to_owned(),
1561 ))
1562 })?;
1563 let provider_requests = requests
1564 .iter()
1565 .map(|request| read_request(&self.bucket, request.object_id, request.maximum_bytes))
1566 .collect();
1567 let results = self.provider.get_batch(provider_requests).await;
1568 if results.len() != requests.len() {
1569 return Err(OperationFailure::before_work(ObjectStoreError::Corrupt));
1570 }
1571 let vector_bytes = u64::try_from(
1572 requests
1573 .len()
1574 .saturating_mul(size_of::<ObjectRead>() + size_of::<GetRequest>()),
1575 )
1576 .unwrap_or(u64::MAX);
1577 let retained_bytes = results.iter().try_fold(0_u64, |total, result| {
1578 total.checked_add(result.as_ref().map_or(0, |value| {
1579 u64::try_from(value.body.len()).unwrap_or(u64::MAX)
1580 }))
1581 });
1582 let retained_bytes = retained_bytes.ok_or_else(|| {
1583 OperationFailure::before_work(ObjectStoreError::Work(WorkError::Overflow))
1584 })?;
1585 let mut work = WorkCounters {
1586 backend_read_operations: 1,
1587 allocation_operations: 2,
1588 peak_allocation_bytes: vector_bytes.checked_add(retained_bytes).ok_or_else(|| {
1589 OperationFailure::before_work(ObjectStoreError::Work(WorkError::Overflow))
1590 })?,
1591 ..WorkCounters::default()
1592 };
1593 work.verify(budget)
1594 .map_err(|error| OperationFailure::before_work(error.into()))?;
1595 for (request, result) in requests.iter().zip(results) {
1596 cancellation
1597 .check()
1598 .map_err(|_| OperationFailure::new(ObjectStoreError::Cancelled, work))?;
1599 let value =
1600 result.map_err(|error| OperationFailure::new(map_objects_error(error), work))?;
1601 let observed = u64::try_from(value.body.len()).unwrap_or(u64::MAX);
1602 work = work
1603 .checked_add(WorkCounters {
1604 object_probes: 1,
1605 object_bytes_read: observed,
1606 bytes_hashed: observed,
1607 bytes_copied: observed,
1608 ..WorkCounters::default()
1609 })
1610 .map_err(|error| OperationFailure::new(error.into(), work))?;
1611 work.verify(budget)
1612 .map_err(|error| OperationFailure::new(error.into(), work))?;
1613 if observed > request.maximum_bytes {
1614 return Err(OperationFailure::new(
1615 ObjectStoreError::TooLarge {
1616 observed,
1617 maximum: request.maximum_bytes,
1618 },
1619 work,
1620 ));
1621 }
1622 if object_digest(request.object_id.kind, &value.body) != request.object_id.digest {
1623 return Err(OperationFailure::new(ObjectStoreError::Corrupt, work));
1624 }
1625 values.push(ObjectRead {
1626 bytes: value.body,
1627 retention: ObjectReadRetention::Owned {
1628 logical_bytes: observed,
1629 },
1630 });
1631 }
1632 success(values, work, budget)
1633 }
1634
1635 async fn contains(
1636 &self,
1637 object_id: ObjectId,
1638 budget: WorkBudget,
1639 cancellation: &CancellationToken,
1640 ) -> ObjectResult<bool> {
1641 cancellation
1642 .check()
1643 .map_err(|_| OperationFailure::before_work(ObjectStoreError::Cancelled))?;
1644 let work = WorkCounters {
1645 object_probes: 1,
1646 backend_read_operations: 1,
1647 ..WorkCounters::default()
1648 };
1649 admit(work, budget)?;
1650 match self
1651 .provider
1652 .get(read_request(
1653 &self.bucket,
1654 object_id,
1655 budget.object_bytes_read,
1656 ))
1657 .await
1658 {
1659 Ok(value) => {
1660 let valid = object_digest(object_id.kind, &value.body) == object_id.digest;
1661 if valid {
1662 success(true, work, budget)
1663 } else {
1664 Err(OperationFailure::new(ObjectStoreError::Corrupt, work))
1665 }
1666 }
1667 Err(ObjectsError::NotFound) => success(false, work, budget),
1668 Err(error) => Err(OperationFailure::new(map_objects_error(error), work)),
1669 }
1670 }
1671}
1672
1673pub(crate) fn object_key(object_id: ObjectId) -> String {
1674 format!(
1675 "fs/v1/{}/{}",
1676 object_id.kind.canonical_tag(),
1677 hex::encode(object_id.digest.as_bytes())
1678 )
1679}
1680
1681fn authority_prefix(authority_id: AuthorityId) -> String {
1682 format!("fs/authorities/{}", hex::encode(authority_id.into_bytes()))
1683}
1684
1685fn authority_path(
1686 authority_id: AuthorityId,
1687) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1688 acyclic_stream::StreamPath::new(authority_prefix(authority_id)).map_err(map_stream_error)
1689}
1690
1691fn records_path(
1692 authority_id: AuthorityId,
1693) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1694 acyclic_stream::StreamPath::new(format!("{}/records", authority_prefix(authority_id)))
1695 .map_err(map_stream_error)
1696}
1697
1698fn epochs_path(
1699 authority_id: AuthorityId,
1700) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1701 acyclic_stream::StreamPath::new(format!("{}/epochs", authority_prefix(authority_id)))
1702 .map_err(map_stream_error)
1703}
1704
1705fn publication_gate_path(
1706 authority_id: AuthorityId,
1707) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1708 acyclic_stream::StreamPath::new(format!(
1709 "{}/publication-gate",
1710 authority_prefix(authority_id)
1711 ))
1712 .map_err(map_stream_error)
1713}
1714
1715fn encode_active_reservation(operation_id: OperationId) -> Bytes {
1716 let mut value = Vec::with_capacity(PUBLICATION_GATE_ACTIVE.len() + 16);
1717 value.extend_from_slice(PUBLICATION_GATE_ACTIVE);
1718 value.extend_from_slice(&operation_id.into_bytes());
1719 Bytes::from(value)
1720}
1721
1722fn decode_publication_gate(value: &[u8]) -> Result<Option<OperationId>, AuthorityStoreError> {
1723 if value == PUBLICATION_GATE_FREE {
1724 return Ok(None);
1725 }
1726 let operation = value
1727 .strip_prefix(PUBLICATION_GATE_ACTIVE)
1728 .and_then(|bytes| <[u8; 16]>::try_from(bytes).ok())
1729 .ok_or_else(|| {
1730 AuthorityStoreError::Corrupt("invalid publication gate record".to_owned())
1731 })?;
1732 Ok(Some(OperationId::from_bytes(operation)))
1733}
1734
1735fn durable_from_operation_envelope(
1736 authority_id: AuthorityId,
1737 operation_id: OperationId,
1738 envelope: acyclic_stream::CommittedEnvelope,
1739) -> Result<DurableCommit, AuthorityStoreError> {
1740 let records = records_path(authority_id)?;
1741 let mut retained = None;
1742 for mutation in envelope.mutations {
1743 let acyclic_stream::CommittedMutation::Append(append) = mutation else {
1744 continue;
1745 };
1746 if append.path != records {
1747 continue;
1748 }
1749 let [record] = append.records.as_slice() else {
1750 return Err(AuthorityStoreError::Corrupt(
1751 "operation appended an invalid authority record count".to_owned(),
1752 ));
1753 };
1754 let durable = decode_durable(authority_id, &record.value)?;
1755 if durable.operation_id != operation_id || retained.replace(durable).is_some() {
1756 return Err(AuthorityStoreError::Corrupt(
1757 "operation envelope does not bind its authority operation".to_owned(),
1758 ));
1759 }
1760 }
1761 retained.ok_or_else(|| {
1762 AuthorityStoreError::Corrupt("operation envelope omits its authority record".to_owned())
1763 })
1764}
1765
1766fn lineage_path(
1767 authority_id: AuthorityId,
1768) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1769 acyclic_stream::StreamPath::new(format!("{}/lineage", authority_prefix(authority_id)))
1770 .map_err(map_stream_error)
1771}
1772
1773fn generation_path(
1774 authority_id: AuthorityId,
1775 generation: GenerationId,
1776) -> Result<acyclic_stream::StreamPath, AuthorityStoreError> {
1777 acyclic_stream::StreamPath::new(format!(
1778 "{}/generations/{}",
1779 authority_prefix(authority_id),
1780 hex::encode(generation.digest().as_bytes())
1781 ))
1782 .map_err(map_stream_error)
1783}
1784
1785fn stream_key(
1786 domain: &[u8],
1787 identity: &[u8],
1788) -> Result<acyclic_stream::IdempotencyKey, AuthorityStoreError> {
1789 let mut value = Vec::with_capacity(
1790 domain
1791 .len()
1792 .saturating_add(identity.len())
1793 .saturating_add(1),
1794 );
1795 value.extend_from_slice(domain);
1796 value.push(0);
1797 value.extend_from_slice(identity);
1798 acyclic_stream::IdempotencyKey::new(Bytes::from(value)).map_err(map_stream_error)
1799}
1800
1801struct ForkDestinationPaths {
1803 root: acyclic_stream::StreamPath,
1804 records: acyclic_stream::StreamPath,
1805 epochs: acyclic_stream::StreamPath,
1806 locator: acyclic_stream::StreamPath,
1807 lineage: acyclic_stream::StreamPath,
1808 gate: acyclic_stream::StreamPath,
1809}
1810
1811fn fork_generation_commit_request(
1814 destination: ForkDestinationPaths,
1815 source_lineage: acyclic_stream::StreamPath,
1816 forked_at: u64,
1817 operation_id: OperationId,
1818) -> Result<acyclic_stream::CommitRequest, OperationFailure<AuthorityStoreError>> {
1819 Ok(acyclic_stream::CommitRequest {
1820 conditions: vec![
1821 acyclic_stream::CommitCondition::Absent {
1822 path: destination.root.clone(),
1823 },
1824 acyclic_stream::CommitCondition::Absent {
1825 path: destination.records.clone(),
1826 },
1827 acyclic_stream::CommitCondition::Absent {
1828 path: destination.epochs.clone(),
1829 },
1830 acyclic_stream::CommitCondition::Absent {
1831 path: destination.locator.clone(),
1832 },
1833 acyclic_stream::CommitCondition::Absent {
1834 path: destination.lineage.clone(),
1835 },
1836 acyclic_stream::CommitCondition::Absent {
1837 path: destination.gate.clone(),
1838 },
1839 ],
1840 mutations: vec![
1841 acyclic_stream::CommitMutation::Append {
1842 path: destination.root,
1843 records: vec![Bytes::from_static(AUTHORITY_MARKER)],
1844 },
1845 acyclic_stream::CommitMutation::Fork {
1846 source: source_lineage,
1847 destination: destination.lineage,
1848 at_tail: forked_at,
1849 },
1850 acyclic_stream::CommitMutation::Append {
1851 path: destination.records,
1852 records: vec![encode_epoch(GENESIS_DOMAIN, Epoch::GENESIS)],
1853 },
1854 acyclic_stream::CommitMutation::Append {
1855 path: destination.epochs,
1856 records: vec![encode_epoch(EPOCH_DOMAIN, Epoch::GENESIS)],
1857 },
1858 acyclic_stream::CommitMutation::Append {
1859 path: destination.locator,
1860 records: vec![encode_lineage_tail(forked_at)],
1861 },
1862 acyclic_stream::CommitMutation::Append {
1863 path: destination.gate,
1864 records: vec![Bytes::from_static(PUBLICATION_GATE_FREE)],
1865 },
1866 ],
1867 idempotency_key: stream_key(b"fork-authority", &operation_id.into_bytes())
1868 .map_err(OperationFailure::before_work)?,
1869 })
1870}
1871
1872fn operation_key(
1873 authority_id: AuthorityId,
1874 operation_id: OperationId,
1875) -> Result<acyclic_stream::IdempotencyKey, AuthorityStoreError> {
1876 let mut identity = [0; 32];
1877 identity[..16].copy_from_slice(&authority_id.into_bytes());
1878 identity[16..].copy_from_slice(&operation_id.into_bytes());
1879 stream_key(b"operation", &identity)
1880}
1881
1882fn publication_reservation_key(
1883 domain: &[u8],
1884 authority_id: AuthorityId,
1885 operation_id: OperationId,
1886 gate_tail: u64,
1887) -> Result<acyclic_stream::IdempotencyKey, AuthorityStoreError> {
1888 let mut identity = Vec::with_capacity(40);
1889 identity.extend_from_slice(&authority_id.into_bytes());
1890 identity.extend_from_slice(&operation_id.into_bytes());
1891 identity.extend_from_slice(&gate_tail.to_le_bytes());
1892 stream_key(domain, &identity)
1893}
1894
1895fn encode_epoch(domain: &[u8], epoch: Epoch) -> Bytes {
1896 let mut value = Vec::with_capacity(domain.len().saturating_add(8));
1897 value.extend_from_slice(domain);
1898 value.extend_from_slice(&epoch.get().to_le_bytes());
1899 Bytes::from(value)
1900}
1901
1902fn generation_from_payload(
1903 authority_id: AuthorityId,
1904 sequence: Sequence,
1905 payload: &[u8],
1906) -> Result<Option<GenerationId>, AuthorityStoreError> {
1907 if let Ok(created) = decode_volume_created(payload, STREAM_RECORD_LIMIT) {
1908 if sequence != Sequence::new(1) || volume_authority_id(created.volume_id) != authority_id {
1909 return Err(AuthorityStoreError::Corrupt(
1910 "volume creation identity or sequence does not match its authority".to_owned(),
1911 ));
1912 }
1913 return Ok(Some(GenerationId::new(
1914 created.initial_generation_root.digest,
1915 )));
1916 }
1917 if let Ok(published) = decode_published_generation(payload, STREAM_RECORD_LIMIT) {
1918 if sequence == Sequence::new(1) || volume_authority_id(published.volume_id) != authority_id
1919 {
1920 return Err(AuthorityStoreError::Corrupt(
1921 "generation publication identity or sequence does not match its authority"
1922 .to_owned(),
1923 ));
1924 }
1925 return Ok(Some(GenerationId::new(published.generation_root.digest)));
1926 }
1927 Ok(None)
1928}
1929
1930fn encode_lineage_generation(generation: GenerationId) -> Bytes {
1931 let mut value = Vec::with_capacity(LINEAGE_DOMAIN.len().saturating_add(32));
1932 value.extend_from_slice(LINEAGE_DOMAIN);
1933 value.extend_from_slice(generation.digest().as_bytes());
1934 Bytes::from(value)
1935}
1936
1937fn decode_lineage_generation(encoded: &[u8]) -> Result<GenerationId, AuthorityStoreError> {
1938 if encoded.len() != LINEAGE_DOMAIN.len().saturating_add(32)
1939 || !encoded.starts_with(LINEAGE_DOMAIN)
1940 {
1941 return Err(AuthorityStoreError::Corrupt(
1942 "invalid Stream generation-lineage record".to_owned(),
1943 ));
1944 }
1945 let digest = encoded
1946 .get(LINEAGE_DOMAIN.len()..)
1947 .ok_or_else(|| {
1948 AuthorityStoreError::Corrupt("truncated Stream generation-lineage record".to_owned())
1949 })?
1950 .try_into()
1951 .map(Digest::from_bytes)
1952 .map_err(|_| {
1953 AuthorityStoreError::Corrupt("truncated Stream generation-lineage record".to_owned())
1954 })?;
1955 Ok(GenerationId::new(digest))
1956}
1957
1958fn encode_lineage_tail(tail: u64) -> Bytes {
1959 let mut value = Vec::with_capacity(LINEAGE_TAIL_DOMAIN.len().saturating_add(8));
1960 value.extend_from_slice(LINEAGE_TAIL_DOMAIN);
1961 value.extend_from_slice(&tail.to_le_bytes());
1962 Bytes::from(value)
1963}
1964
1965fn decode_lineage_tail(encoded: &[u8]) -> Result<u64, AuthorityStoreError> {
1966 if encoded.len() != LINEAGE_TAIL_DOMAIN.len().saturating_add(8)
1967 || !encoded.starts_with(LINEAGE_TAIL_DOMAIN)
1968 {
1969 return Err(AuthorityStoreError::Corrupt(
1970 "invalid Stream generation locator".to_owned(),
1971 ));
1972 }
1973 let bytes: [u8; 8] = encoded
1974 .get(LINEAGE_TAIL_DOMAIN.len()..)
1975 .ok_or_else(|| AuthorityStoreError::Corrupt("truncated generation locator".to_owned()))?
1976 .try_into()
1977 .map_err(|_| AuthorityStoreError::Corrupt("truncated generation locator".to_owned()))?;
1978 Ok(u64::from_le_bytes(bytes))
1979}
1980
1981fn decode_epoch(encoded: &[u8], domain: &[u8]) -> Result<Epoch, AuthorityStoreError> {
1982 if encoded.len() != domain.len().saturating_add(8) || !encoded.starts_with(domain) {
1983 return Err(AuthorityStoreError::Corrupt(
1984 "invalid Stream epoch record".to_owned(),
1985 ));
1986 }
1987 let bytes: [u8; 8] = encoded
1988 .get(domain.len()..)
1989 .ok_or_else(|| AuthorityStoreError::Corrupt("truncated Stream epoch record".to_owned()))?
1990 .try_into()
1991 .map_err(|_| AuthorityStoreError::Corrupt("truncated Stream epoch record".to_owned()))?;
1992 Epoch::new(u64::from_le_bytes(bytes))
1993 .map_err(|error| AuthorityStoreError::Corrupt(error.to_string()))
1994}
1995
1996async fn read_one<P: acyclic_stream::StreamProvider>(
1997 provider: &P,
1998 path: acyclic_stream::StreamPath,
1999 from: u64,
2000) -> Result<acyclic_stream::Record, AuthorityStoreError> {
2001 let mut records = provider
2002 .read(acyclic_stream::ReadRequest {
2003 path,
2004 from,
2005 limit: 1,
2006 })
2007 .await
2008 .map_err(map_stream_error)?;
2009 let record = records
2010 .next()
2011 .await
2012 .ok_or_else(|| {
2013 AuthorityStoreError::Corrupt("Stream returned an empty exact read".to_owned())
2014 })?
2015 .map_err(map_stream_error)?;
2016 if record.sequence != from {
2017 return Err(AuthorityStoreError::Corrupt(
2018 "Stream returned a non-contiguous record".to_owned(),
2019 ));
2020 }
2021 Ok(record)
2022}
2023
2024fn decode_durable(
2025 authority_id: AuthorityId,
2026 encoded: &[u8],
2027) -> Result<DurableCommit, AuthorityStoreError> {
2028 let durable = StreamsDurableRecord::decode(encoded, STREAM_RECORD_LIMIT)
2029 .map_err(|error| AuthorityStoreError::Corrupt(error.to_string()))?
2030 .0;
2031 let expected = authority_commit_digest(
2032 authority_id,
2033 durable.epoch,
2034 durable.sequence,
2035 durable.operation_id,
2036 durable.fingerprint,
2037 durable.previous_digest,
2038 &durable.payload,
2039 );
2040 if expected != durable.digest {
2041 return Err(AuthorityStoreError::Corrupt(
2042 "Stream authority record digest mismatch".to_owned(),
2043 ));
2044 }
2045 Ok(durable)
2046}
2047
2048fn map_stream_error(error: acyclic_stream::StreamError) -> AuthorityStoreError {
2049 match error {
2050 acyclic_stream::StreamError::NotFound => AuthorityStoreError::Missing,
2051 acyclic_stream::StreamError::Capacity => {
2052 AuthorityStoreError::Rejected("Stream capacity exhausted".to_owned())
2053 }
2054 other => AuthorityStoreError::Rejected(other.to_string()),
2055 }
2056}
2057
2058fn authority_read_work(records: u64) -> WorkCounters {
2059 WorkCounters {
2060 authority_records_read: records,
2061 backend_read_operations: records,
2062 ..WorkCounters::default()
2063 }
2064}
2065
2066fn authority_write_work(records: u64, payload_bytes: u64) -> WorkCounters {
2067 WorkCounters {
2068 authority_records_appended: records,
2069 authority_bytes_written: payload_bytes,
2070 backend_write_operations: 1,
2071 durability_operations: 1,
2072 ..WorkCounters::default()
2073 }
2074}
2075
2076fn admit_authority(
2077 work: WorkCounters,
2078 budget: WorkBudget,
2079) -> Result<(), OperationFailure<AuthorityStoreError>> {
2080 work.verify(budget)
2081 .map_err(|error| OperationFailure::new(error.into(), work))
2082}
2083
2084fn authority_success<T>(value: T, work: WorkCounters, budget: WorkBudget) -> AuthorityResult<T> {
2085 admit_authority(work, budget)?;
2086 Ok(AuthorityReceipt { value, work })
2087}
2088
2089fn read_request(bucket: &wire::BucketRef, object_id: ObjectId, maximum_bytes: u64) -> GetRequest {
2090 GetRequest {
2091 target: ReadTarget::Bucket(bucket.clone()),
2092 object_key: object_key(object_id),
2093 version_id: None,
2094 range: None,
2095 if_match: None,
2096 if_none_match: None,
2097 maximum_bytes,
2098 }
2099}
2100
2101fn prepare_provider_put_batch(
2102 bucket: &wire::BucketRef,
2103 writes: &[ObjectWrite],
2104 budget: WorkBudget,
2105 cancellation: &CancellationToken,
2106) -> Result<(Vec<PutRequest>, Vec<usize>, WorkCounters), OperationFailure<ObjectStoreError>> {
2107 let minimum_bytes =
2108 u64::try_from(writes.len().saturating_mul(size_of::<PutRequest>())).unwrap_or(u64::MAX);
2109 let prospective = WorkCounters {
2110 allocation_operations: 1,
2111 peak_allocation_bytes: minimum_bytes,
2112 ..WorkCounters::default()
2113 };
2114 prospective
2115 .verify(budget)
2116 .map_err(|error| OperationFailure::before_work(error.into()))?;
2117 let mut requests = Vec::new();
2118 requests.try_reserve_exact(writes.len()).map_err(|_| {
2119 OperationFailure::before_work(ObjectStoreError::Rejected(
2120 "object batch allocation failed".to_owned(),
2121 ))
2122 })?;
2123 let mut work = WorkCounters {
2124 allocation_operations: 1,
2125 peak_allocation_bytes: u64::try_from(
2126 requests.capacity().saturating_mul(size_of::<PutRequest>()),
2127 )
2128 .unwrap_or(u64::MAX),
2129 ..WorkCounters::default()
2130 };
2131 admit(work, budget)?;
2132 let mut unique: BTreeMap<ObjectId, usize> = BTreeMap::new();
2133 let mut unique_writes = Vec::new();
2134 for (write_index, write) in writes.iter().enumerate() {
2135 cancellation
2136 .check()
2137 .map_err(|_| OperationFailure::new(ObjectStoreError::Cancelled, work))?;
2138 if object_digest(write.object_id.kind, &write.bytes) != write.object_id.digest {
2139 return Err(OperationFailure::new(
2140 ObjectStoreError::DigestMismatch,
2141 work,
2142 ));
2143 }
2144 let byte_count = u64::try_from(write.bytes.len()).unwrap_or(u64::MAX);
2145 work.object_bytes_written = work
2146 .object_bytes_written
2147 .checked_add(byte_count)
2148 .ok_or_else(|| {
2149 OperationFailure::new(
2150 ObjectStoreError::Rejected("object batch byte count overflowed".to_owned()),
2151 work,
2152 )
2153 })?;
2154 work.bytes_hashed = work.bytes_hashed.checked_add(byte_count).ok_or_else(|| {
2155 OperationFailure::new(
2156 ObjectStoreError::Rejected("object batch byte count overflowed".to_owned()),
2157 work,
2158 )
2159 })?;
2160 admit(work, budget)?;
2161 if let Some(existing_index) = unique.get(&write.object_id) {
2162 if writes
2163 .get(*existing_index)
2164 .is_none_or(|existing| existing.bytes != write.bytes)
2165 {
2166 return Err(OperationFailure::new(
2167 ObjectStoreError::DigestMismatch,
2168 work,
2169 ));
2170 }
2171 } else {
2172 unique.insert(write.object_id, write_index);
2173 unique_writes.push(write_index);
2174 requests.push(provider_put_request(bucket, write));
2175 }
2176 }
2177 Ok((requests, unique_writes, work))
2178}
2179
2180fn map_objects_error(error: ObjectsError) -> ObjectStoreError {
2181 match error {
2182 ObjectsError::NotFound => ObjectStoreError::Missing,
2183 ObjectsError::Capacity => {
2184 ObjectStoreError::Rejected("Objects capacity exhausted".to_owned())
2185 }
2186 other => ObjectStoreError::Rejected(other.to_string()),
2187 }
2188}
2189
2190fn admit(work: WorkCounters, budget: WorkBudget) -> Result<(), OperationFailure<ObjectStoreError>> {
2191 work.verify(budget)
2192 .map_err(|error| OperationFailure::new(error.into(), work))
2193}
2194
2195fn success<T>(value: T, work: WorkCounters, budget: WorkBudget) -> ObjectResult<T> {
2196 admit(work, budget)?;
2197 Ok(ObjectReceipt { value, work })
2198}
2199
2200#[cfg(test)]
2201#[allow(clippy::expect_used, clippy::panic)]
2202mod tests {
2203 use super::*;
2204 use crate::kernel::{VolumeCreated, encode_publication_payload, encode_volume_created};
2205 use crate::model::{Lifecycle, VolumeConfig};
2206 use crate::storage::ObjectKind;
2207
2208 #[tokio::test]
2209 async fn existing_authority_without_publication_gate_is_upgraded() {
2210 let stream = Arc::new(acyclic_stream::MemoryStream::default());
2211 let authority_id = AuthorityId::from_bytes([0x31; 16]);
2212 let root = authority_path(authority_id).expect("root path");
2213 let records = records_path(authority_id).expect("records path");
2214 let epochs = epochs_path(authority_id).expect("epochs path");
2215 acyclic_stream::StreamProvider::commit(
2216 stream.as_ref(),
2217 acyclic_stream::CommitRequest {
2218 conditions: vec![
2219 acyclic_stream::CommitCondition::Absent { path: root.clone() },
2220 acyclic_stream::CommitCondition::Absent {
2221 path: records.clone(),
2222 },
2223 acyclic_stream::CommitCondition::Absent {
2224 path: epochs.clone(),
2225 },
2226 ],
2227 mutations: vec![
2228 acyclic_stream::CommitMutation::Append {
2229 path: root,
2230 records: vec![Bytes::from_static(AUTHORITY_MARKER)],
2231 },
2232 acyclic_stream::CommitMutation::Append {
2233 path: records,
2234 records: vec![encode_epoch(GENESIS_DOMAIN, Epoch::GENESIS)],
2235 },
2236 acyclic_stream::CommitMutation::Append {
2237 path: epochs,
2238 records: vec![encode_epoch(EPOCH_DOMAIN, Epoch::GENESIS)],
2239 },
2240 ],
2241 idempotency_key: stream_key(b"legacy-authority", &authority_id.into_bytes())
2242 .expect("legacy idempotency key"),
2243 },
2244 )
2245 .await
2246 .expect("seed legacy authority");
2247
2248 let authority = StreamAuthorityStore::new(Arc::clone(&stream));
2249 let cancellation = CancellationToken::new();
2250 let denied = authority
2251 .create_authority(
2252 authority_id,
2253 Epoch::GENESIS,
2254 WorkBudget {
2255 authority_records_read: 3,
2256 backend_read_operations: 3,
2257 ..WorkBudget::UNBOUNDED
2258 },
2259 &cancellation,
2260 )
2261 .await
2262 .expect_err("write budget must reject migration before mutation");
2263 assert!(
2264 matches!(denied.error, AuthorityStoreError::Work(_)),
2265 "unexpected migration denial: {:?}",
2266 denied.error
2267 );
2268 assert!(matches!(
2269 acyclic_stream::StreamProvider::tail(
2270 stream.as_ref(),
2271 publication_gate_path(authority_id).expect("gate path"),
2272 )
2273 .await,
2274 Err(acyclic_stream::StreamError::NotFound)
2275 ));
2276
2277 let opened = authority
2278 .create_authority(
2279 authority_id,
2280 Epoch::GENESIS,
2281 WorkBudget::UNBOUNDED,
2282 &cancellation,
2283 )
2284 .await
2285 .expect("upgrade legacy authority");
2286 let CreateAuthorityOutcome::Existing(head) = opened.value else {
2287 panic!("legacy authority must reopen as existing");
2288 };
2289 assert_eq!(opened.work.authority_records_appended, 1);
2290
2291 let committed = authority
2292 .compare_and_append_guarded(
2293 crate::GuardedAppend {
2294 authority_id,
2295 epoch: head.epoch,
2296 expected: head,
2297 commit: ProposedCommit {
2298 operation_id: OperationId::from_bytes([0x32; 16]),
2299 fingerprint: Digest::from_bytes([0x33; 32]),
2300 payload: Bytes::from_static(b"legacy authority write"),
2301 },
2302 permit: PublicationPermit::Unrestricted,
2303 },
2304 WorkBudget::UNBOUNDED,
2305 &cancellation,
2306 )
2307 .await
2308 .expect("append after upgrade")
2309 .value;
2310 let FsAppendOutcome::Committed(committed) = committed else {
2311 panic!("unexpected append outcome: {committed:?}");
2312 };
2313 let committed_head = Head {
2314 epoch: committed.epoch,
2315 sequence: committed.sequence,
2316 digest: committed.digest,
2317 };
2318 let reservation = authority
2319 .reserve_publication(
2320 authority_id,
2321 committed_head,
2322 OperationId::from_bytes([0x34; 16]),
2323 WorkBudget::UNBOUNDED,
2324 &cancellation,
2325 )
2326 .await
2327 .expect("reserve upgraded authority")
2328 .value;
2329 let ReservationOutcome::Reserved(reservation) = reservation else {
2330 panic!("unexpected reservation outcome: {reservation:?}");
2331 };
2332 authority
2333 .release_publication(reservation, WorkBudget::UNBOUNDED, &cancellation)
2334 .await
2335 .expect("release upgraded authority");
2336 }
2337
2338 #[tokio::test]
2339 async fn existing_authority_rejects_a_corrupt_publication_gate() {
2340 let stream = Arc::new(acyclic_stream::MemoryStream::default());
2341 let authority = StreamAuthorityStore::new(Arc::clone(&stream));
2342 let authority_id = AuthorityId::from_bytes([0x35; 16]);
2343 let cancellation = CancellationToken::new();
2344 authority
2345 .create_authority(
2346 authority_id,
2347 Epoch::GENESIS,
2348 WorkBudget::UNBOUNDED,
2349 &cancellation,
2350 )
2351 .await
2352 .expect("create authority");
2353 let gate = publication_gate_path(authority_id).expect("gate path");
2354 acyclic_stream::StreamProvider::commit(
2355 stream.as_ref(),
2356 acyclic_stream::CommitRequest {
2357 conditions: vec![acyclic_stream::CommitCondition::Tail {
2358 path: gate.clone(),
2359 expected: 1,
2360 }],
2361 mutations: vec![acyclic_stream::CommitMutation::Append {
2362 path: gate,
2363 records: vec![Bytes::from_static(b"invalid-publication-gate")],
2364 }],
2365 idempotency_key: stream_key(
2366 b"corrupt-publication-gate",
2367 &authority_id.into_bytes(),
2368 )
2369 .expect("corruption key"),
2370 },
2371 )
2372 .await
2373 .expect("append invalid gate record");
2374
2375 let error = authority
2376 .create_authority(
2377 authority_id,
2378 Epoch::GENESIS,
2379 WorkBudget::UNBOUNDED,
2380 &cancellation,
2381 )
2382 .await
2383 .expect_err("corrupt gate must fail authority reopen");
2384 assert!(matches!(error.error, AuthorityStoreError::Corrupt(_)));
2385 }
2386
2387 #[tokio::test]
2388 async fn publication_reservation_excludes_unreserved_writers() {
2389 let stream = Arc::new(acyclic_stream::MemoryStream::default());
2390 let authority = StreamAuthorityStore::new(Arc::clone(&stream));
2391 let authority_id = AuthorityId::from_bytes([0x41; 16]);
2392 let cancellation = CancellationToken::new();
2393 authority
2394 .create_authority(
2395 authority_id,
2396 Epoch::GENESIS,
2397 WorkBudget::UNBOUNDED,
2398 &cancellation,
2399 )
2400 .await
2401 .expect("create authority");
2402 let head = authority
2403 .head(authority_id, WorkBudget::UNBOUNDED, &cancellation)
2404 .await
2405 .expect("head")
2406 .value;
2407 let operation_id = OperationId::from_bytes([0x42; 16]);
2408 let reservation = match authority
2409 .reserve_publication(
2410 authority_id,
2411 head,
2412 operation_id,
2413 WorkBudget::UNBOUNDED,
2414 &cancellation,
2415 )
2416 .await
2417 .expect("reserve")
2418 .value
2419 {
2420 ReservationOutcome::Reserved(reservation) => reservation,
2421 outcome => panic!("unexpected reservation outcome: {outcome:?}"),
2422 };
2423 let proposed = ProposedCommit {
2424 operation_id: OperationId::from_bytes([0x43; 16]),
2425 fingerprint: Digest::from_bytes([0x44; 32]),
2426 payload: Bytes::from_static(b"reserved write"),
2427 };
2428 let ordinary = authority
2429 .compare_and_append_guarded(
2430 crate::GuardedAppend {
2431 authority_id,
2432 epoch: head.epoch,
2433 expected: head,
2434 commit: proposed.clone(),
2435 permit: PublicationPermit::Unrestricted,
2436 },
2437 WorkBudget::UNBOUNDED,
2438 &cancellation,
2439 )
2440 .await
2441 .expect("ordinary writer is rejected")
2442 .value;
2443 assert!(matches!(ordinary, FsAppendOutcome::Fenced { .. }));
2444 let reserved = authority
2445 .compare_and_append_guarded(
2446 crate::GuardedAppend {
2447 authority_id,
2448 epoch: head.epoch,
2449 expected: head,
2450 commit: proposed,
2451 permit: PublicationPermit::Reservation {
2452 operation_id: operation_id.into_bytes(),
2453 gate_tail: reservation.gate_tail,
2454 expected: reservation.expected,
2455 },
2456 },
2457 WorkBudget::UNBOUNDED,
2458 &cancellation,
2459 )
2460 .await
2461 .expect("reserved writer")
2462 .value;
2463 assert!(matches!(reserved, FsAppendOutcome::Committed(_)));
2464 let denied_release = authority
2465 .release_publication(reservation, WorkBudget::default(), &cancellation)
2466 .await
2467 .expect_err("release must charge its idempotency read before doing work");
2468 assert!(matches!(denied_release.error, AuthorityStoreError::Work(_)));
2469 authority
2470 .release_publication(reservation, WorkBudget::UNBOUNDED, &cancellation)
2471 .await
2472 .expect("release");
2473 authority
2474 .release_publication(reservation, WorkBudget::UNBOUNDED, &cancellation)
2475 .await
2476 .expect("exact release retry is idempotent");
2477 let current = authority
2478 .head(authority_id, WorkBudget::UNBOUNDED, &cancellation)
2479 .await
2480 .expect("head after publication")
2481 .value;
2482 let reacquired = match authority
2483 .reserve_publication(
2484 authority_id,
2485 current,
2486 operation_id,
2487 WorkBudget::UNBOUNDED,
2488 &cancellation,
2489 )
2490 .await
2491 .expect("reacquire")
2492 .value
2493 {
2494 ReservationOutcome::Reserved(reservation) => reservation,
2495 outcome => panic!("unexpected reacquisition outcome: {outcome:?}"),
2496 };
2497 let forged = PublicationReservation {
2498 operation_id: OperationId::from_bytes([0x99; 16]),
2499 ..reacquired
2500 };
2501 assert!(
2502 authority
2503 .release_publication(forged, WorkBudget::UNBOUNDED, &cancellation)
2504 .await
2505 .is_err()
2506 );
2507 let gate = publication_gate_path(authority_id).expect("gate path");
2508 let advanced = acyclic_stream::StreamProvider::commit(
2509 stream.as_ref(),
2510 acyclic_stream::CommitRequest {
2511 conditions: vec![acyclic_stream::CommitCondition::Tail {
2512 path: gate.clone(),
2513 expected: reacquired.gate_tail,
2514 }],
2515 mutations: vec![acyclic_stream::CommitMutation::Append {
2516 path: gate.clone(),
2517 records: vec![encode_active_reservation(reacquired.operation_id)],
2518 }],
2519 idempotency_key: stream_key(b"advance-active-gate", &authority_id.into_bytes())
2520 .expect("advance key"),
2521 },
2522 )
2523 .await
2524 .expect("advance active gate");
2525 assert!(matches!(
2526 advanced,
2527 acyclic_stream::CommitOutcome::Committed(_)
2528 ));
2529
2530 let release_key = publication_reservation_key(
2531 b"release-publication",
2532 reacquired.authority_id,
2533 reacquired.operation_id,
2534 reacquired.gate_tail,
2535 )
2536 .expect("release key");
2537 let retained_conflict = acyclic_stream::StreamProvider::commit(
2538 stream.as_ref(),
2539 acyclic_stream::CommitRequest {
2540 conditions: vec![acyclic_stream::CommitCondition::Tail {
2541 path: gate.clone(),
2542 expected: reacquired.gate_tail,
2543 }],
2544 mutations: vec![acyclic_stream::CommitMutation::Append {
2545 path: gate,
2546 records: vec![Bytes::from_static(PUBLICATION_GATE_FREE)],
2547 }],
2548 idempotency_key: release_key,
2549 },
2550 )
2551 .await
2552 .expect("retain release conflict");
2553 assert!(matches!(
2554 retained_conflict,
2555 acyclic_stream::CommitOutcome::Conflict(_)
2556 ));
2557 authority
2558 .release_publication(reacquired, WorkBudget::UNBOUNDED, &cancellation)
2559 .await
2560 .expect_err("retained release conflict must not become success");
2561 let (_, active) = authority
2562 .publication_gate(authority_id)
2563 .await
2564 .expect("read active gate");
2565 assert_eq!(active, Some(reacquired.operation_id));
2566 }
2567
2568 #[tokio::test]
2569 async fn provider_object_batch_reads_once_and_preserves_order()
2570 -> Result<(), Box<dyn std::error::Error>> {
2571 let (provider, bucket) = acyclic_objects::MemoryObjects::with_default_bucket();
2572 let store = ProviderObjectStore::new(Arc::new(provider), bucket);
2573 let first_bytes = Bytes::from_static(b"first");
2574 let second_bytes = Bytes::from_static(b"second");
2575 let first = ObjectId {
2576 kind: ObjectKind::BlobChunk,
2577 digest: object_digest(ObjectKind::BlobChunk, &first_bytes),
2578 };
2579 let second = ObjectId {
2580 kind: ObjectKind::BlobChunk,
2581 digest: object_digest(ObjectKind::BlobChunk, &second_bytes),
2582 };
2583 let cancellation = CancellationToken::new();
2584 store
2585 .put(
2586 first,
2587 first_bytes.clone(),
2588 WorkBudget::UNBOUNDED,
2589 &cancellation,
2590 )
2591 .await?;
2592 store
2593 .put(
2594 second,
2595 second_bytes.clone(),
2596 WorkBudget::UNBOUNDED,
2597 &cancellation,
2598 )
2599 .await?;
2600 let receipt = store
2601 .read_many(
2602 &[
2603 ObjectReadRequest {
2604 object_id: second,
2605 maximum_bytes: 6,
2606 },
2607 ObjectReadRequest {
2608 object_id: first,
2609 maximum_bytes: 5,
2610 },
2611 ],
2612 WorkBudget::UNBOUNDED,
2613 &cancellation,
2614 )
2615 .await?;
2616 assert_eq!(
2617 receipt.value.first().map(|value| &value.bytes),
2618 Some(&second_bytes)
2619 );
2620 assert_eq!(
2621 receipt.value.get(1).map(|value| &value.bytes),
2622 Some(&first_bytes)
2623 );
2624 assert_eq!(receipt.work.backend_read_operations, 1);
2625 assert_eq!(receipt.work.object_probes, 2);
2626 Ok(())
2627 }
2628
2629 #[tokio::test]
2630 async fn provider_object_batch_counts_one_backend_write()
2631 -> Result<(), Box<dyn std::error::Error>> {
2632 let (provider, bucket) = acyclic_objects::MemoryObjects::with_default_bucket();
2633 let store = ProviderObjectStore::new(Arc::new(provider), bucket);
2634 let first_bytes = Bytes::from_static(b"first");
2635 let second_bytes = Bytes::from_static(b"second");
2636 let writes = [
2637 ObjectWrite {
2638 object_id: ObjectId {
2639 kind: ObjectKind::BlobChunk,
2640 digest: object_digest(ObjectKind::BlobChunk, &first_bytes),
2641 },
2642 bytes: first_bytes,
2643 },
2644 ObjectWrite {
2645 object_id: ObjectId {
2646 kind: ObjectKind::BlobChunk,
2647 digest: object_digest(ObjectKind::BlobChunk, &second_bytes),
2648 },
2649 bytes: second_bytes,
2650 },
2651 ];
2652 let receipt = store
2653 .put_many(&writes, WorkBudget::UNBOUNDED, &CancellationToken::new())
2654 .await?;
2655 assert_eq!(receipt.work.backend_write_operations, 1);
2656 assert_eq!(receipt.work.object_bytes_written, 11);
2657 Ok(())
2658 }
2659
2660 #[test]
2661 fn provider_put_batch_interns_identical_object_requests()
2662 -> Result<(), Box<dyn std::error::Error>> {
2663 let bucket = wire::BucketRef {
2664 bucket_id: "bucket".to_owned(),
2665 name: "bucket".to_owned(),
2666 };
2667 let bytes = Bytes::from_static(b"shared");
2668 let object_id = ObjectId {
2669 kind: ObjectKind::BlobChunk,
2670 digest: object_digest(ObjectKind::BlobChunk, &bytes),
2671 };
2672 let writes = [
2673 ObjectWrite {
2674 object_id,
2675 bytes: bytes.clone(),
2676 },
2677 ObjectWrite { object_id, bytes },
2678 ];
2679 let (requests, unique_writes, work) = prepare_provider_put_batch(
2680 &bucket,
2681 &writes,
2682 WorkBudget::UNBOUNDED,
2683 &CancellationToken::new(),
2684 )?;
2685 assert_eq!(requests.len(), 1);
2686 assert_eq!(unique_writes, [0]);
2687 assert_eq!(work.object_bytes_written, 12);
2688 assert_eq!(work.bytes_hashed, 12);
2689 Ok(())
2690 }
2691
2692 #[test]
2693 fn provider_put_batch_rejects_unadmitted_request_allocation() {
2694 let bucket = wire::BucketRef {
2695 bucket_id: "bucket".to_owned(),
2696 name: "bucket".to_owned(),
2697 };
2698 let bytes = Bytes::from_static(b"body");
2699 let writes = [ObjectWrite {
2700 object_id: ObjectId {
2701 kind: ObjectKind::BlobChunk,
2702 digest: object_digest(ObjectKind::BlobChunk, &bytes),
2703 },
2704 bytes,
2705 }];
2706 let mut budget = WorkBudget::UNBOUNDED;
2707 budget.allocation_operations = 0;
2708 let failure =
2709 prepare_provider_put_batch(&bucket, &writes, budget, &CancellationToken::new())
2710 .err()
2711 .unwrap_or_else(|| {
2712 OperationFailure::before_work(ObjectStoreError::Rejected(
2713 "unadmitted allocation unexpectedly succeeded".to_owned(),
2714 ))
2715 });
2716 assert!(matches!(failure.error, ObjectStoreError::Work(_)));
2717 assert_eq!(*failure.work, WorkCounters::default());
2718 }
2719
2720 #[test]
2721 fn generation_records_are_bound_to_their_authority_and_sequence()
2722 -> Result<(), Box<dyn std::error::Error>> {
2723 let volume_id = crate::foundation::VolumeId::from_bytes([0x71; 16]);
2724 let authority_id = volume_authority_id(volume_id);
2725 let root = ObjectId {
2726 kind: ObjectKind::GenerationRoot,
2727 digest: Digest::from_bytes([0x72; 32]),
2728 };
2729 let creation = encode_volume_created(VolumeCreated {
2730 volume_id,
2731 config: VolumeConfig::portable(Lifecycle::Ephemeral),
2732 initial_generation_root: root,
2733 })?;
2734 let publication = encode_publication_payload(volume_id, root);
2735 let generation = GenerationId::new(root.digest);
2736
2737 assert_eq!(
2738 generation_from_payload(authority_id, Sequence::new(1), &creation)?,
2739 Some(generation)
2740 );
2741 assert_eq!(
2742 generation_from_payload(authority_id, Sequence::new(2), &publication)?,
2743 Some(generation)
2744 );
2745 assert!(generation_from_payload(authority_id, Sequence::new(2), &creation).is_err());
2746 assert!(generation_from_payload(authority_id, Sequence::new(1), &publication).is_err());
2747 assert!(
2748 generation_from_payload(
2749 AuthorityId::from_bytes([0x73; 16]),
2750 Sequence::new(1),
2751 &creation,
2752 )
2753 .is_err()
2754 );
2755 assert!(
2756 generation_from_payload(
2757 AuthorityId::from_bytes([0x73; 16]),
2758 Sequence::new(2),
2759 &publication,
2760 )
2761 .is_err()
2762 );
2763 Ok(())
2764 }
2765}