1use std::{
4 collections::{BTreeMap, BTreeSet},
5 sync::Arc,
6};
7
8use async_trait::async_trait;
9use bytes::Bytes;
10use futures::{StreamExt as _, stream};
11use sha2::{Digest, Sha256};
12use tokio::sync::{RwLock, watch};
13
14use crate::{
15 AppendOutcome, AppendReceipt, AppendRequest, Child, ChildStream, ChildrenRequest,
16 CommitCondition, CommitConflict, CommitId, CommitMutation, CommitOutcome, CommitRequest,
17 CommittedAppend, CommittedDelete, CommittedEnvelope, CommittedFork, CommittedMutation,
18 CommittedTrim, DeleteReceipt, ForkReceipt, ForkRequest, IdempotencyKey, IdempotencyObservation,
19 IdempotencyOutcome, MAX_COMMAND_BYTES, MAX_ITEMS, MAX_RECORD_BYTES, ReadRequest, Record,
20 RecordStream, StreamError, StreamPath, StreamProvider, SystemUnixMillisClock, TrimReceipt,
21 UnixMillisClock,
22};
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct MemoryLimits {
27 pub paths: usize,
29 pub path_bytes: usize,
31 pub records: usize,
33 pub payload_bytes: usize,
35 pub commits: usize,
37 pub idempotency_results: usize,
39}
40
41impl Default for MemoryLimits {
42 fn default() -> Self {
43 Self {
44 paths: 100_000,
45 path_bytes: 64 * 1024 * 1024,
46 records: 1_000_000,
47 payload_bytes: 256 * 1024 * 1024,
48 commits: 1_000_000,
49 idempotency_results: 1_000_000,
50 }
51 }
52}
53
54#[derive(Clone)]
57pub struct MemoryStream {
58 state: Arc<RwLock<State>>,
59 limits: MemoryLimits,
60 clock: Arc<dyn UnixMillisClock>,
61}
62
63impl Default for MemoryStream {
64 fn default() -> Self {
65 Self::new(MemoryLimits::default())
66 }
67}
68
69impl MemoryStream {
70 #[must_use]
72 pub fn new(limits: MemoryLimits) -> Self {
73 Self::new_with_clock(limits, Arc::new(SystemUnixMillisClock))
74 }
75
76 #[must_use]
78 pub fn new_with_clock(limits: MemoryLimits, clock: Arc<dyn UnixMillisClock>) -> Self {
79 Self {
80 state: Arc::new(RwLock::new(State::default())),
81 limits,
82 clock,
83 }
84 }
85
86 async fn commit_inner(
87 &self,
88 mut request: CommitRequest,
89 deadline_unix_millis: Option<u64>,
90 ) -> Result<CommitOutcome, StreamError> {
91 normalize_commit(&mut request)?;
92 validate_commit_shape(&request)?;
93 let digest = commit_digest(&request);
94 let mut state = self.state.write().await;
95 if let Some(result) = replay_commit(&state, &request.idempotency_key, digest)? {
96 return Ok(result);
97 }
98 if deadline_unix_millis.is_some_and(|deadline| self.clock.now_unix_millis() >= deadline) {
99 return Err(StreamError::DeadlineElapsed);
100 }
101 admit_replay(&state, Some(&request.idempotency_key), self.limits)?;
102 let conflicts = commit_conflicts(&state, &request.conditions);
103 if !conflicts.is_empty() {
104 let result = CommitOutcome::Conflict(conflicts);
105 retain_replay(
106 &mut state,
107 Some(request.idempotency_key),
108 digest,
109 IdempotencyOutcome::Commit(result.clone()),
110 );
111 return Ok(result);
112 }
113 validate_commit_authority(&state, &request)?;
114 reserve_coordinated(&state, &request, self.limits)?;
115 let commit_id = next_commit_id(&mut state, digest)?;
116 let before = request
117 .mutations
118 .iter()
119 .filter_map(|mutation| match mutation {
120 CommitMutation::Fork { source, .. } => state
121 .paths
122 .get(source)
123 .map(|stream| (source.clone(), (stream.history.clone(), stream.tail))),
124 CommitMutation::Append { .. }
125 | CommitMutation::Trim { .. }
126 | CommitMutation::Delete { .. } => None,
127 })
128 .collect::<BTreeMap<_, _>>();
129 let mutations = apply_coordinated(&mut state, request.mutations, &before, commit_id)?;
130 let envelope = CommittedEnvelope {
131 commit_id,
132 mutations,
133 };
134 state.commits.insert(commit_id, envelope.clone());
135 let result = CommitOutcome::Committed(envelope);
136 retain_replay(
137 &mut state,
138 Some(request.idempotency_key),
139 digest,
140 IdempotencyOutcome::Commit(result.clone()),
141 );
142 Ok(result)
143 }
144}
145
146#[derive(Default)]
147struct State {
148 paths: BTreeMap<StreamPath, PathState>,
149 retired: BTreeSet<StreamPath>,
150 commits: BTreeMap<CommitId, CommittedEnvelope>,
151 replays: BTreeMap<Bytes, Replay>,
152 path_bytes: usize,
153 record_count: usize,
154 payload_bytes: usize,
155 decision: u64,
156}
157
158struct PathState {
159 history: Option<Arc<History>>,
160 tail: u64,
161 trim_point: u64,
162 changed: watch::Sender<u64>,
163}
164
165enum History {
166 Batch {
167 parent: Option<Arc<Self>>,
168 records: Arc<[Record]>,
169 },
170 Prefix {
171 source: Option<Arc<Self>>,
172 tail: u64,
173 },
174}
175
176#[derive(Clone)]
177struct Replay {
178 digest: [u8; 32],
179 result: IdempotencyOutcome,
180}
181
182#[async_trait]
183impl StreamProvider for MemoryStream {
184 async fn inspect_idempotency(
185 &self,
186 idempotency_key: IdempotencyKey,
187 ) -> Result<Option<IdempotencyObservation>, StreamError> {
188 let state = self.state.read().await;
189 Ok(state
190 .replays
191 .get(idempotency_key.as_bytes())
192 .map(|replay| IdempotencyObservation {
193 idempotency_key,
194 request_digest: replay.digest,
195 outcome: replay.result.clone(),
196 }))
197 }
198
199 async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
200 let state = self.state.read().await;
201 reject_retired(&state, &path)?;
202 state
203 .paths
204 .get(&path)
205 .map(|stream| stream.tail)
206 .ok_or(StreamError::NotFound)
207 }
208
209 async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
210 validate_records(&request.records)?;
211 validate_append_size(&request)?;
212 let digest = append_digest(&request);
213 let mut state = self.state.write().await;
214 if let Some(result) = replay_append(&state, request.idempotency_key.as_ref(), digest)? {
215 return Ok(result);
216 }
217 admit_replay(&state, request.idempotency_key.as_ref(), self.limits)?;
218 reject_retired(&state, &request.path)?;
219 let actual_tail = state.paths.get(&request.path).map_or(0, |path| path.tail);
220 if request
221 .if_tail
222 .is_some_and(|expected| expected != actual_tail)
223 {
224 let result = AppendOutcome::TailConflict { actual_tail };
225 retain_replay(
226 &mut state,
227 request.idempotency_key,
228 digest,
229 IdempotencyOutcome::Append(result.clone()),
230 );
231 return Ok(result);
232 }
233 reserve_append(&state, &request.path, &request.records, self.limits)?;
234 let commit_id = next_commit_id(&mut state, digest)?;
235 let records = build_records(actual_tail, request.records, commit_id)?;
236 ensure_path(&mut state, &request.path);
237 let resulting_tail = {
238 let stream = state
239 .paths
240 .get_mut(&request.path)
241 .ok_or(StreamError::Unavailable)?;
242 stream.history = Some(Arc::new(History::Batch {
243 parent: stream.history.clone(),
244 records: Arc::from(records.clone()),
245 }));
246 stream.tail = records
247 .last()
248 .map_or(actual_tail, |record| record.sequence + 1);
249 stream.changed.send_replace(stream.tail);
250 stream.tail
251 };
252 state.record_count += records.len();
253 state.payload_bytes += records
254 .iter()
255 .map(|record| record.value.len())
256 .sum::<usize>();
257 let receipt = AppendReceipt {
258 start: actual_tail,
259 end: resulting_tail,
260 tail: resulting_tail,
261 commit_id,
262 };
263 let envelope = CommittedEnvelope {
264 commit_id,
265 mutations: vec![CommittedMutation::Append(CommittedAppend {
266 path: request.path,
267 start: receipt.start,
268 end: receipt.end,
269 tail: receipt.tail,
270 records,
271 })],
272 };
273 state.commits.insert(commit_id, envelope);
274 let result = AppendOutcome::Committed(receipt);
275 retain_replay(
276 &mut state,
277 request.idempotency_key,
278 digest,
279 IdempotencyOutcome::Append(result.clone()),
280 );
281 Ok(result)
282 }
283
284 async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
285 if request.source == request.destination {
286 return Err(StreamError::InvalidArgument);
287 }
288 validate_fork_size(&request)?;
289 let digest = fork_digest(&request);
290 let mut state = self.state.write().await;
291 if let Some(result) = replay_fork(&state, request.idempotency_key.as_ref(), digest)? {
292 return Ok(result);
293 }
294 admit_replay(&state, request.idempotency_key.as_ref(), self.limits)?;
295 reject_retired(&state, &request.destination)?;
296 if state.paths.contains_key(&request.destination) {
297 return Err(StreamError::AlreadyExists);
298 }
299 let source = state
300 .paths
301 .get(&request.source)
302 .ok_or(StreamError::NotFound)?;
303 let forked_at = request.at_tail.unwrap_or(source.tail);
304 if forked_at > source.tail {
305 return Err(StreamError::PrefixNotRetained);
306 }
307 let source_history = source.history.clone();
308 reserve_paths(&state, &request.destination, self.limits)?;
309 reserve_commit(&state, self.limits)?;
310 let commit_id = next_commit_id(&mut state, digest)?;
311 ensure_path(&mut state, &request.destination);
312 let destination = state
313 .paths
314 .get_mut(&request.destination)
315 .ok_or(StreamError::Unavailable)?;
316 destination.history = Some(Arc::new(History::Prefix {
317 source: source_history,
318 tail: forked_at,
319 }));
320 destination.tail = forked_at;
321 destination.changed.send_replace(forked_at);
322 let receipt = ForkReceipt {
323 source: request.source,
324 destination: request.destination,
325 forked_at,
326 tail: forked_at,
327 commit_id,
328 };
329 state.commits.insert(
330 commit_id,
331 CommittedEnvelope {
332 commit_id,
333 mutations: vec![CommittedMutation::Fork(CommittedFork {
334 source: receipt.source.clone(),
335 destination: receipt.destination.clone(),
336 forked_at,
337 tail: forked_at,
338 })],
339 },
340 );
341 retain_replay(
342 &mut state,
343 request.idempotency_key,
344 digest,
345 IdempotencyOutcome::Fork(receipt.clone()),
346 );
347 Ok(receipt)
348 }
349
350 async fn trim(
351 &self,
352 path: StreamPath,
353 before: u64,
354 idempotency_key: IdempotencyKey,
355 ) -> Result<TrimReceipt, StreamError> {
356 let digest = trim_digest(&path, before);
357 let mut state = self.state.write().await;
358 if let Some(result) = replay_trim(&state, &idempotency_key, digest)? {
359 return Ok(result);
360 }
361 admit_replay(&state, Some(&idempotency_key), self.limits)?;
362 reserve_commit(&state, self.limits)?;
363 let current = state.paths.get(&path).ok_or(StreamError::NotFound)?;
364 if before > current.tail {
365 return Err(StreamError::OutOfRange);
366 }
367 let trim_point = current.trim_point.max(before);
368 let commit_id = next_commit_id(&mut state, digest)?;
369 let stream = state.paths.get_mut(&path).ok_or(StreamError::Unavailable)?;
370 stream.trim_point = trim_point;
371 stream.changed.send_replace(stream.tail);
372 let receipt = TrimReceipt {
373 path: path.clone(),
374 trim_point,
375 commit_id,
376 };
377 state.commits.insert(
378 commit_id,
379 CommittedEnvelope {
380 commit_id,
381 mutations: vec![CommittedMutation::Trim(CommittedTrim { path, trim_point })],
382 },
383 );
384 retain_replay(
385 &mut state,
386 Some(idempotency_key),
387 digest,
388 IdempotencyOutcome::Trim(receipt.clone()),
389 );
390 Ok(receipt)
391 }
392
393 async fn delete(
394 &self,
395 path: StreamPath,
396 idempotency_key: IdempotencyKey,
397 ) -> Result<DeleteReceipt, StreamError> {
398 let digest = delete_digest(&path);
399 let mut state = self.state.write().await;
400 if let Some(result) = replay_delete(&state, &idempotency_key, digest)? {
401 return Ok(result);
402 }
403 admit_replay(&state, Some(&idempotency_key), self.limits)?;
404 reserve_commit(&state, self.limits)?;
405 if !state.paths.contains_key(&path) {
406 return Err(StreamError::NotFound);
407 }
408 if state
409 .paths
410 .keys()
411 .any(|candidate| is_descendant(&path, candidate))
412 {
413 return Err(StreamError::InvalidArgument);
414 }
415 let commit_id = next_commit_id(&mut state, digest)?;
416 state.paths.remove(&path);
417 state.retired.insert(path.clone());
418 let receipt = DeleteReceipt {
419 path: path.clone(),
420 commit_id,
421 };
422 state.commits.insert(
423 commit_id,
424 CommittedEnvelope {
425 commit_id,
426 mutations: vec![CommittedMutation::Delete(CommittedDelete { path })],
427 },
428 );
429 retain_replay(
430 &mut state,
431 Some(idempotency_key),
432 digest,
433 IdempotencyOutcome::Delete(receipt.clone()),
434 );
435 Ok(receipt)
436 }
437
438 async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
439 validate_limit(request.limit)?;
440 let state = self.state.read().await;
441 reject_retired(&state, &request.path)?;
442 let path = state
443 .paths
444 .get(&request.path)
445 .ok_or(StreamError::NotFound)?;
446 if request.from > path.tail {
447 return Err(StreamError::OutOfRange);
448 }
449 if request.from < path.trim_point {
450 return Err(StreamError::OutOfRange);
451 }
452 let records = read_history(
453 path.history.as_ref(),
454 path.tail,
455 request.from,
456 usize::try_from(request.limit).map_err(|_| StreamError::LimitExceeded)?,
457 );
458 Ok(stream::iter(records.into_iter().map(Ok)).boxed())
459 }
460
461 async fn follow(&self, path: StreamPath, from: u64) -> Result<RecordStream, StreamError> {
462 let (receiver, cursor) = {
463 let state = self.state.read().await;
464 reject_retired(&state, &path)?;
465 let stream = state.paths.get(&path).ok_or(StreamError::NotFound)?;
466 if from > stream.tail {
467 return Err(StreamError::OutOfRange);
468 }
469 if from < stream.trim_point {
470 return Err(StreamError::OutOfRange);
471 }
472 (
473 stream.changed.subscribe(),
474 HistoryCursor::new(stream.history.as_ref(), stream.tail, from),
475 )
476 };
477 let state = Arc::clone(&self.state);
478 Ok(stream::unfold(
479 (state, path, receiver, cursor),
480 |(state, path, mut receiver, mut cursor)| async move {
481 loop {
482 let record = {
483 let guard = state.read().await;
484 if reject_retired(&guard, &path).is_err() {
485 Err(StreamError::Retired)
486 } else if let Some(current) = guard.paths.get(&path) {
487 if cursor.next < current.trim_point {
488 Err(StreamError::OutOfRange)
489 } else {
490 cursor.sync(current.history.as_ref(), current.tail);
491 Ok(cursor.next_record())
492 }
493 } else {
494 Err(StreamError::NotFound)
495 }
496 };
497 let Some(record) = (match record {
498 Ok(record) => record,
499 Err(error) => {
500 return Some((Err(error), (state, path, receiver, cursor)));
501 }
502 }) else {
503 if receiver.changed().await.is_err() {
504 return Some((
505 Err(StreamError::Unavailable),
506 (state, path, receiver, cursor),
507 ));
508 }
509 continue;
510 };
511 return Some((Ok(record), (state, path, receiver, cursor)));
512 }
513 },
514 )
515 .boxed())
516 }
517
518 async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
519 validate_limit(request.limit)?;
520 let state = self.state.read().await;
521 let limit = usize::try_from(request.limit).map_err(|_| StreamError::LimitExceeded)?;
522 let children = state
523 .paths
524 .keys()
525 .filter(|path| {
526 request.parent.as_ref().map_or_else(
527 || !path.as_str().contains('/'),
528 |parent| is_direct_child(parent, path),
529 )
530 })
531 .take(limit)
532 .cloned()
533 .map(|path| Child { path })
534 .collect::<Vec<_>>();
535 Ok(stream::iter(children.into_iter().map(Ok)).boxed())
536 }
537
538 async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
539 self.commit_inner(request, None).await
540 }
541
542 async fn commit_before(
543 &self,
544 request: CommitRequest,
545 deadline_unix_millis: u64,
546 ) -> Result<CommitOutcome, StreamError> {
547 self.commit_inner(request, Some(deadline_unix_millis)).await
548 }
549
550 async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
551 self.state
552 .read()
553 .await
554 .commits
555 .get(&commit_id)
556 .cloned()
557 .ok_or(StreamError::NotFound)
558 }
559}
560
561fn validate_limit(limit: u32) -> Result<(), StreamError> {
562 if limit == 0 || usize::try_from(limit).map_or(true, |limit| limit > MAX_ITEMS) {
563 Err(StreamError::LimitExceeded)
564 } else {
565 Ok(())
566 }
567}
568
569fn validate_records(records: &[Bytes]) -> Result<(), StreamError> {
570 if records.is_empty() || records.len() > MAX_ITEMS {
571 return Err(StreamError::LimitExceeded);
572 }
573 let mut total = 0_usize;
574 for record in records {
575 if record.len() > MAX_RECORD_BYTES {
576 return Err(StreamError::LimitExceeded);
577 }
578 total = total
579 .checked_add(record.len())
580 .and_then(|value| value.checked_add(8))
581 .ok_or(StreamError::LimitExceeded)?;
582 }
583 if total > MAX_COMMAND_BYTES {
584 return Err(StreamError::LimitExceeded);
585 }
586 Ok(())
587}
588
589fn add_size(total: &mut usize, amount: usize) -> Result<(), StreamError> {
590 *total = total
591 .checked_add(amount)
592 .ok_or(StreamError::LimitExceeded)?;
593 if *total > MAX_COMMAND_BYTES {
594 return Err(StreamError::LimitExceeded);
595 }
596 Ok(())
597}
598
599fn add_path_size(total: &mut usize, path: &StreamPath) -> Result<(), StreamError> {
600 add_size(total, 8)?;
601 add_size(total, path.as_str().len())
602}
603
604fn add_records_size(total: &mut usize, records: &[Bytes]) -> Result<(), StreamError> {
605 add_size(total, 8)?;
606 for record in records {
607 add_size(total, 8)?;
608 add_size(total, record.len())?;
609 }
610 Ok(())
611}
612
613fn validate_append_size(request: &AppendRequest) -> Result<(), StreamError> {
614 let mut total = 1 + 8 + 1;
615 add_path_size(&mut total, &request.path)?;
616 add_records_size(&mut total, &request.records)?;
617 if let Some(key) = &request.idempotency_key {
618 add_size(&mut total, 8 + key.as_bytes().len())?;
619 }
620 Ok(())
621}
622
623fn validate_fork_size(request: &ForkRequest) -> Result<(), StreamError> {
624 let mut total = 1 + 8 + 1;
625 add_path_size(&mut total, &request.source)?;
626 add_path_size(&mut total, &request.destination)?;
627 if let Some(key) = &request.idempotency_key {
628 add_size(&mut total, 8 + key.as_bytes().len())?;
629 }
630 Ok(())
631}
632
633fn reject_retired(state: &State, path: &StreamPath) -> Result<(), StreamError> {
634 let mut current = Some(path.clone());
635 while let Some(path) = current {
636 if state.retired.contains(&path) {
637 return Err(StreamError::Retired);
638 }
639 current = path.parent();
640 }
641 Ok(())
642}
643
644fn missing_paths(state: &State, path: &StreamPath) -> usize {
645 let mut count = 0;
646 let mut current = Some(path.clone());
647 while let Some(path) = current {
648 count += usize::from(!state.paths.contains_key(&path));
649 current = path.parent();
650 }
651 count
652}
653
654fn reserve_paths(
655 state: &State,
656 path: &StreamPath,
657 limits: MemoryLimits,
658) -> Result<(), StreamError> {
659 let missing_path_bytes = missing_path_bytes(state, path);
660 if state.paths.len().saturating_add(missing_paths(state, path)) > limits.paths
661 || state.path_bytes.saturating_add(missing_path_bytes) > limits.path_bytes
662 {
663 Err(StreamError::Capacity)
664 } else {
665 Ok(())
666 }
667}
668
669fn missing_path_bytes(state: &State, path: &StreamPath) -> usize {
670 let mut bytes = 0_usize;
671 let mut current = Some(path.clone());
672 while let Some(path) = current {
673 if !state.paths.contains_key(&path) {
674 bytes = bytes.saturating_add(path.as_str().len());
675 }
676 current = path.parent();
677 }
678 bytes
679}
680
681fn reserve_commit(state: &State, limits: MemoryLimits) -> Result<(), StreamError> {
682 if state.commits.len() >= limits.commits {
683 Err(StreamError::Capacity)
684 } else {
685 Ok(())
686 }
687}
688
689fn reserve_append(
690 state: &State,
691 path: &StreamPath,
692 records: &[Bytes],
693 limits: MemoryLimits,
694) -> Result<(), StreamError> {
695 reserve_paths(state, path, limits)?;
696 reserve_commit(state, limits)?;
697 let bytes = records.iter().map(Bytes::len).sum::<usize>();
698 if state.record_count.saturating_add(records.len()) > limits.records
699 || state.payload_bytes.saturating_add(bytes) > limits.payload_bytes
700 {
701 return Err(StreamError::Capacity);
702 }
703 Ok(())
704}
705
706fn ensure_path(state: &mut State, path: &StreamPath) {
707 let mut missing = Vec::new();
708 let mut current = Some(path.clone());
709 while let Some(path) = current {
710 if state.paths.contains_key(&path) {
711 break;
712 }
713 current = path.parent();
714 missing.push(path);
715 }
716 for path in missing.into_iter().rev() {
717 let (changed, _) = watch::channel(0);
718 state.path_bytes = state.path_bytes.saturating_add(path.as_str().len());
719 state.paths.insert(
720 path,
721 PathState {
722 history: None,
723 tail: 0,
724 trim_point: 0,
725 changed,
726 },
727 );
728 }
729}
730
731fn build_records(
732 start: u64,
733 bodies: Vec<Bytes>,
734 commit_id: CommitId,
735) -> Result<Vec<Record>, StreamError> {
736 bodies
737 .into_iter()
738 .enumerate()
739 .map(|(index, value)| {
740 let sequence = start
741 .checked_add(u64::try_from(index).map_err(|_| StreamError::LimitExceeded)?)
742 .ok_or(StreamError::LimitExceeded)?;
743 Ok(Record {
744 sequence,
745 value,
746 commit_id,
747 })
748 })
749 .collect()
750}
751
752struct HistoryWindow {
753 records: Arc<[Record]>,
754 start: usize,
755 end: usize,
756}
757
758struct HistoryCursor {
765 root: Option<Arc<History>>,
766 windows: Vec<HistoryWindow>,
767 window: usize,
768 record: usize,
769 next: u64,
770}
771
772impl HistoryCursor {
773 fn new(history: Option<&Arc<History>>, tail: u64, next: u64) -> Self {
774 let mut cursor = Self {
775 root: None,
776 windows: Vec::new(),
777 window: 0,
778 record: 0,
779 next,
780 };
781 cursor.rebuild(history, tail);
782 cursor
783 }
784
785 fn rebuild(&mut self, root: Option<&Arc<History>>, tail: u64) {
786 self.root = root.cloned();
787 self.windows = collect_history_windows(root, tail);
788 self.window = 0;
789 self.record = 0;
790 self.seek_next();
791 }
792
793 fn sync(&mut self, history: Option<&Arc<History>>, tail: u64) {
794 let Some(root) = history else {
795 self.rebuild(None, tail);
796 return;
797 };
798 if self.root.as_ref().is_some_and(|old| Arc::ptr_eq(old, root)) {
799 return;
800 }
801
802 let mut cursor = Some(Arc::clone(root));
807 let mut suffix = Vec::new();
808 let mut reaches_old = false;
809 while let Some(node) = cursor {
810 if self
811 .root
812 .as_ref()
813 .is_some_and(|old| Arc::ptr_eq(old, &node))
814 {
815 reaches_old = true;
816 break;
817 }
818 match node.as_ref() {
819 History::Batch { parent, records } => {
820 suffix.push(Arc::clone(records));
821 cursor = parent.clone();
822 }
823 History::Prefix { .. } => break,
824 }
825 }
826 if !reaches_old {
827 self.rebuild(Some(root), tail);
828 return;
829 }
830
831 suffix.reverse();
832 let mut added = suffix
833 .into_iter()
834 .filter_map(|records| history_window(records, tail))
835 .collect::<Vec<_>>();
836 self.windows.append(&mut added);
837 self.root = Some(Arc::clone(root));
838 }
839
840 fn seek_next(&mut self) {
841 while let Some(window) = self.windows.get(self.window) {
842 while self.record < window.end.saturating_sub(window.start) {
843 let index = window.start + self.record;
844 let Some(record) = window.records.get(index) else {
845 break;
846 };
847 if record.sequence >= self.next {
848 return;
849 }
850 self.record += 1;
851 }
852 self.window += 1;
853 self.record = 0;
854 }
855 }
856
857 fn next_record(&mut self) -> Option<Record> {
858 self.seek_next();
859 let window = self.windows.get(self.window)?;
860 let index = window.start + self.record;
861 let record = window.records.get(index)?.clone();
862 self.record += 1;
863 self.next = record.sequence.checked_add(1)?;
864 Some(record)
865 }
866}
867
868fn collect_history_windows(history: Option<&Arc<History>>, tail: u64) -> Vec<HistoryWindow> {
869 let mut cursor = history.cloned();
870 let mut ceiling = tail;
871 let mut batches = Vec::new();
872 while let Some(history) = cursor {
873 match history.as_ref() {
874 History::Batch { parent, records } => {
875 batches.push((Arc::clone(records), ceiling));
876 cursor = parent.clone();
877 }
878 History::Prefix { source, tail } => {
879 ceiling = ceiling.min(*tail);
880 cursor = source.clone();
881 }
882 }
883 }
884 batches.reverse();
885 batches
886 .into_iter()
887 .filter_map(|(records, ceiling)| history_window(records, ceiling))
888 .collect()
889}
890
891fn history_window(records: Arc<[Record]>, ceiling: u64) -> Option<HistoryWindow> {
892 let first = records.first()?.sequence;
893 let batch_end = records
894 .len()
895 .try_into()
896 .ok()
897 .and_then(|length: u64| first.checked_add(length))
898 .unwrap_or(u64::MAX);
899 let end_sequence = ceiling.min(batch_end);
900 if end_sequence <= first {
901 return None;
902 }
903 let end = usize::try_from(end_sequence - first).ok()?;
904 Some(HistoryWindow {
905 records,
906 start: 0,
907 end,
908 })
909}
910
911fn read_history(history: Option<&Arc<History>>, tail: u64, from: u64, limit: usize) -> Vec<Record> {
912 if limit == 1 {
917 return read_one_history(history, tail, from).into_iter().collect();
918 }
919 let mut cursor = history.cloned();
920 let mut ceiling = tail;
921 let mut batches = Vec::new();
922 while let Some(history) = cursor {
923 match history.as_ref() {
924 History::Batch { parent, records } => {
925 batches.push((Arc::clone(records), ceiling));
926 cursor = parent.clone();
927 }
928 History::Prefix { source, tail } => {
929 ceiling = ceiling.min(*tail);
930 cursor = source.clone();
931 }
932 }
933 }
934 let mut result = Vec::with_capacity(limit);
935 for (records, ceiling) in batches.into_iter().rev() {
936 for record in records.iter() {
937 if record.sequence >= from && record.sequence < ceiling {
938 result.push(record.clone());
939 if result.len() == limit {
940 return result;
941 }
942 }
943 }
944 }
945 result
946}
947
948fn read_one_history(history: Option<&Arc<History>>, tail: u64, from: u64) -> Option<Record> {
949 let mut cursor = history.cloned();
950 let mut ceiling = tail;
951 while let Some(history) = cursor {
952 match history.as_ref() {
953 History::Batch { parent, records } => {
954 let Some(first) = records.first() else {
955 cursor = parent.clone();
956 continue;
957 };
958 let batch_end = records
959 .len()
960 .try_into()
961 .ok()
962 .and_then(|length: u64| first.sequence.checked_add(length))
963 .unwrap_or(u64::MAX);
964 let visible_end = ceiling.min(batch_end);
965 if from >= first.sequence && from < visible_end {
966 let offset = usize::try_from(from - first.sequence).ok()?;
967 return records.get(offset).cloned();
968 }
969 ceiling = ceiling.min(first.sequence);
970 cursor = parent.clone();
971 }
972 History::Prefix { source, tail } => {
973 ceiling = ceiling.min(*tail);
974 cursor = source.clone();
975 }
976 }
977 }
978 None
979}
980
981fn is_direct_child(parent: &StreamPath, candidate: &StreamPath) -> bool {
982 candidate.parent().as_ref() == Some(parent)
983}
984
985fn is_descendant(parent: &StreamPath, candidate: &StreamPath) -> bool {
986 candidate
987 .as_str()
988 .strip_prefix(parent.as_str())
989 .is_some_and(|suffix| suffix.starts_with('/'))
990}
991
992fn next_commit_id(state: &mut State, digest: [u8; 32]) -> Result<CommitId, StreamError> {
993 state.decision = state.decision.checked_add(1).ok_or(StreamError::Capacity)?;
994 let mut hash = Sha256::new();
995 hash.update(b"acyclic.stream.commit.v1\0");
996 hash.update(state.decision.to_le_bytes());
997 hash.update(digest);
998 Ok(CommitId::from_bytes(hash.finalize().into()))
999}
1000
1001fn replay(
1002 state: &State,
1003 key: Option<&crate::IdempotencyKey>,
1004 digest: [u8; 32],
1005) -> Result<Option<IdempotencyOutcome>, StreamError> {
1006 let Some(key) = key else { return Ok(None) };
1007 let Some(replay) = state.replays.get(key.as_bytes()) else {
1008 return Ok(None);
1009 };
1010 if replay.digest != digest {
1011 return Err(StreamError::IdempotencyMismatch);
1012 }
1013 Ok(Some(replay.result.clone()))
1014}
1015
1016fn replay_append(
1017 state: &State,
1018 key: Option<&crate::IdempotencyKey>,
1019 digest: [u8; 32],
1020) -> Result<Option<AppendOutcome>, StreamError> {
1021 match replay(state, key, digest)? {
1022 Some(IdempotencyOutcome::Append(result)) => Ok(Some(result)),
1023 Some(_) => Err(StreamError::IdempotencyMismatch),
1024 None => Ok(None),
1025 }
1026}
1027
1028fn replay_fork(
1029 state: &State,
1030 key: Option<&crate::IdempotencyKey>,
1031 digest: [u8; 32],
1032) -> Result<Option<ForkReceipt>, StreamError> {
1033 match replay(state, key, digest)? {
1034 Some(IdempotencyOutcome::Fork(result)) => Ok(Some(result)),
1035 Some(_) => Err(StreamError::IdempotencyMismatch),
1036 None => Ok(None),
1037 }
1038}
1039
1040fn replay_trim(
1041 state: &State,
1042 key: &IdempotencyKey,
1043 digest: [u8; 32],
1044) -> Result<Option<TrimReceipt>, StreamError> {
1045 match replay(state, Some(key), digest)? {
1046 Some(IdempotencyOutcome::Trim(result)) => Ok(Some(result)),
1047 Some(_) => Err(StreamError::IdempotencyMismatch),
1048 None => Ok(None),
1049 }
1050}
1051
1052fn replay_delete(
1053 state: &State,
1054 key: &IdempotencyKey,
1055 digest: [u8; 32],
1056) -> Result<Option<DeleteReceipt>, StreamError> {
1057 match replay(state, Some(key), digest)? {
1058 Some(IdempotencyOutcome::Delete(result)) => Ok(Some(result)),
1059 Some(_) => Err(StreamError::IdempotencyMismatch),
1060 None => Ok(None),
1061 }
1062}
1063
1064fn replay_commit(
1065 state: &State,
1066 key: &crate::IdempotencyKey,
1067 digest: [u8; 32],
1068) -> Result<Option<CommitOutcome>, StreamError> {
1069 match replay(state, Some(key), digest)? {
1070 Some(IdempotencyOutcome::Commit(result)) => Ok(Some(result)),
1071 Some(_) => Err(StreamError::IdempotencyMismatch),
1072 None => Ok(None),
1073 }
1074}
1075
1076fn admit_replay(
1077 state: &State,
1078 key: Option<&crate::IdempotencyKey>,
1079 limits: MemoryLimits,
1080) -> Result<(), StreamError> {
1081 if key.is_some() && state.replays.len() >= limits.idempotency_results {
1082 Err(StreamError::Capacity)
1083 } else {
1084 Ok(())
1085 }
1086}
1087
1088fn retain_replay(
1089 state: &mut State,
1090 key: Option<crate::IdempotencyKey>,
1091 digest: [u8; 32],
1092 result: IdempotencyOutcome,
1093) {
1094 if let Some(key) = key {
1095 state.replays.insert(
1096 Bytes::copy_from_slice(key.as_bytes()),
1097 Replay { digest, result },
1098 );
1099 }
1100}
1101
1102fn append_digest(request: &AppendRequest) -> [u8; 32] {
1103 let mut hash = request_hasher(b"append");
1104 hash_path(&mut hash, &request.path);
1105 hash.update(request.if_tail.unwrap_or(u64::MAX).to_le_bytes());
1106 hash_records(&mut hash, &request.records);
1107 hash.finalize().into()
1108}
1109
1110fn fork_digest(request: &ForkRequest) -> [u8; 32] {
1111 let mut hash = request_hasher(b"fork");
1112 hash_path(&mut hash, &request.source);
1113 hash_path(&mut hash, &request.destination);
1114 hash.update(request.at_tail.unwrap_or(u64::MAX).to_le_bytes());
1115 hash.finalize().into()
1116}
1117
1118fn trim_digest(path: &StreamPath, before: u64) -> [u8; 32] {
1119 let mut hash = request_hasher(b"trim");
1120 hash_path(&mut hash, path);
1121 hash.update(before.to_le_bytes());
1122 hash.finalize().into()
1123}
1124
1125fn delete_digest(path: &StreamPath) -> [u8; 32] {
1126 let mut hash = request_hasher(b"delete");
1127 hash_path(&mut hash, path);
1128 hash.finalize().into()
1129}
1130
1131fn commit_digest(request: &CommitRequest) -> [u8; 32] {
1132 let mut hash = request_hasher(b"commit");
1133 for condition in &request.conditions {
1134 match condition {
1135 CommitCondition::Tail { path, expected } => {
1136 hash.update([1]);
1137 hash_path(&mut hash, path);
1138 hash.update(expected.to_le_bytes());
1139 }
1140 CommitCondition::Absent { path } => {
1141 hash.update([2]);
1142 hash_path(&mut hash, path);
1143 }
1144 }
1145 }
1146 for mutation in &request.mutations {
1147 match mutation {
1148 CommitMutation::Append { path, records } => {
1149 hash.update([1]);
1150 hash_path(&mut hash, path);
1151 hash_records(&mut hash, records);
1152 }
1153 CommitMutation::Fork {
1154 source,
1155 destination,
1156 at_tail,
1157 } => {
1158 hash.update([2]);
1159 hash_path(&mut hash, source);
1160 hash_path(&mut hash, destination);
1161 hash.update(at_tail.to_le_bytes());
1162 }
1163 CommitMutation::Trim { path, before } => {
1164 hash.update([3]);
1165 hash_path(&mut hash, path);
1166 hash.update(before.to_le_bytes());
1167 }
1168 CommitMutation::Delete { path } => {
1169 hash.update([4]);
1170 hash_path(&mut hash, path);
1171 }
1172 }
1173 }
1174 hash.finalize().into()
1175}
1176
1177fn request_hasher(kind: &[u8]) -> Sha256 {
1178 let mut hash = Sha256::new();
1179 hash.update(b"acyclic.stream.request.v1\0");
1180 hash.update(kind);
1181 hash
1182}
1183
1184fn hash_path(hash: &mut Sha256, path: &StreamPath) {
1185 hash.update((path.as_str().len() as u64).to_le_bytes());
1186 hash.update(path.as_str().as_bytes());
1187}
1188
1189fn hash_records(hash: &mut Sha256, records: &[Bytes]) {
1190 hash.update((records.len() as u64).to_le_bytes());
1191 for record in records {
1192 hash.update((record.len() as u64).to_le_bytes());
1193 hash.update(record);
1194 }
1195}
1196
1197fn condition_path(condition: &CommitCondition) -> &StreamPath {
1198 match condition {
1199 CommitCondition::Tail { path, .. } | CommitCondition::Absent { path } => path,
1200 }
1201}
1202
1203fn mutation_path(mutation: &CommitMutation) -> &StreamPath {
1204 match mutation {
1205 CommitMutation::Fork { destination, .. } => destination,
1206 CommitMutation::Append { path, .. }
1207 | CommitMutation::Trim { path, .. }
1208 | CommitMutation::Delete { path } => path,
1209 }
1210}
1211
1212fn normalize_commit(request: &mut CommitRequest) -> Result<(), StreamError> {
1213 if request.conditions.is_empty()
1214 || request.conditions.len() > MAX_ITEMS
1215 || request.mutations.is_empty()
1216 || request.mutations.len() > MAX_ITEMS
1217 {
1218 return Err(StreamError::LimitExceeded);
1219 }
1220 for mutation in &request.mutations {
1221 if let CommitMutation::Append { records, .. } = mutation {
1222 validate_records(records)?;
1223 }
1224 }
1225 validate_commit_size(request)?;
1226 request.conditions.sort_by(|left, right| {
1227 condition_path(left)
1228 .cmp(condition_path(right))
1229 .then_with(|| {
1230 matches!(left, CommitCondition::Absent { .. })
1231 .cmp(&matches!(right, CommitCondition::Absent { .. }))
1232 })
1233 });
1234 request
1235 .mutations
1236 .sort_by(|left, right| mutation_path(left).cmp(mutation_path(right)));
1237 if request
1238 .conditions
1239 .windows(2)
1240 .any(|pair| matches!(pair, [left, right] if condition_path(left) == condition_path(right)))
1241 || request.mutations.windows(2).any(
1242 |pair| matches!(pair, [left, right] if mutation_path(left) == mutation_path(right)),
1243 )
1244 {
1245 return Err(StreamError::InvalidArgument);
1246 }
1247 Ok(())
1248}
1249
1250fn validate_commit_size(request: &CommitRequest) -> Result<(), StreamError> {
1251 let mut total = 1 + 8 + 8 + 8 + request.idempotency_key.as_bytes().len();
1252 for condition in &request.conditions {
1253 add_size(&mut total, 1 + 8)?;
1254 add_path_size(&mut total, condition_path(condition))?;
1255 }
1256 for mutation in &request.mutations {
1257 add_size(&mut total, 1)?;
1258 match mutation {
1259 CommitMutation::Append { path, records } => {
1260 add_path_size(&mut total, path)?;
1261 add_records_size(&mut total, records)?;
1262 }
1263 CommitMutation::Fork {
1264 source,
1265 destination,
1266 ..
1267 } => {
1268 add_path_size(&mut total, source)?;
1269 add_path_size(&mut total, destination)?;
1270 add_size(&mut total, 8)?;
1271 }
1272 CommitMutation::Trim { path, .. } => {
1273 add_path_size(&mut total, path)?;
1274 add_size(&mut total, 8)?;
1275 }
1276 CommitMutation::Delete { path } => add_path_size(&mut total, path)?,
1277 }
1278 }
1279 Ok(())
1280}
1281
1282fn validate_commit_shape(request: &CommitRequest) -> Result<(), StreamError> {
1283 let conditions = request
1284 .conditions
1285 .iter()
1286 .map(|condition| (condition_path(condition), condition))
1287 .collect::<BTreeMap<_, _>>();
1288 for mutation in &request.mutations {
1289 match mutation {
1290 CommitMutation::Append { path, .. } => match conditions.get(path) {
1291 Some(CommitCondition::Tail { .. } | CommitCondition::Absent { .. }) => {}
1292 _ => return Err(StreamError::InvalidArgument),
1293 },
1294 CommitMutation::Fork {
1295 source: _,
1296 destination,
1297 at_tail: _,
1298 } => {
1299 if !matches!(
1300 conditions.get(destination),
1301 Some(CommitCondition::Absent { .. })
1302 ) {
1303 return Err(StreamError::InvalidArgument);
1304 }
1305 }
1306 CommitMutation::Trim { path, before: _ } | CommitMutation::Delete { path } => {
1307 if !matches!(conditions.get(path), Some(CommitCondition::Tail { .. })) {
1308 return Err(StreamError::InvalidArgument);
1309 }
1310 }
1311 }
1312 }
1313 Ok(())
1314}
1315
1316fn validate_commit_authority(state: &State, request: &CommitRequest) -> Result<(), StreamError> {
1317 for mutation in &request.mutations {
1318 match mutation {
1319 CommitMutation::Append { .. } => {}
1320 CommitMutation::Fork {
1321 source, at_tail, ..
1322 } => {
1323 let Some(source_state) = state.paths.get(source) else {
1324 return Err(StreamError::NotFound);
1325 };
1326 if *at_tail > source_state.tail {
1327 return Err(StreamError::InvalidArgument);
1328 }
1329 }
1330 CommitMutation::Trim { path, before } => {
1331 let Some(stream) = state.paths.get(path) else {
1332 return Err(StreamError::NotFound);
1333 };
1334 if *before > stream.tail {
1335 return Err(StreamError::InvalidArgument);
1336 }
1337 }
1338 CommitMutation::Delete { path } => {
1339 if !state.paths.contains_key(path)
1340 || state
1341 .paths
1342 .keys()
1343 .any(|candidate| is_descendant(path, candidate))
1344 {
1345 return Err(StreamError::InvalidArgument);
1346 }
1347 }
1348 }
1349 }
1350 Ok(())
1351}
1352
1353fn commit_conflicts(state: &State, conditions: &[CommitCondition]) -> Vec<CommitConflict> {
1354 conditions
1355 .iter()
1356 .filter_map(|condition| match condition {
1357 CommitCondition::Tail { path, expected } => match state.paths.get(path) {
1358 Some(stream) if stream.tail == *expected => None,
1359 Some(stream) => Some(CommitConflict::Tail {
1360 path: path.clone(),
1361 expected: *expected,
1362 actual: Some(stream.tail),
1363 }),
1364 None => Some(CommitConflict::Tail {
1365 path: path.clone(),
1366 expected: *expected,
1367 actual: None,
1368 }),
1369 },
1370 CommitCondition::Absent { path } => {
1371 if state.paths.contains_key(path) {
1372 Some(CommitConflict::Exists { path: path.clone() })
1373 } else if reject_retired(state, path).is_err() {
1374 Some(CommitConflict::Retired { path: path.clone() })
1375 } else {
1376 None
1377 }
1378 }
1379 })
1380 .collect()
1381}
1382
1383fn reserve_coordinated(
1384 state: &State,
1385 request: &CommitRequest,
1386 limits: MemoryLimits,
1387) -> Result<(), StreamError> {
1388 reserve_commit(state, limits)?;
1389 let mut new_paths = BTreeSet::new();
1390 let mut records = 0_usize;
1391 let mut bytes = 0_usize;
1392 for mutation in &request.mutations {
1393 let destination = mutation_path(mutation);
1394 let mut current = Some(destination.clone());
1395 while let Some(path) = current {
1396 if !state.paths.contains_key(&path) {
1397 new_paths.insert(path.clone());
1398 }
1399 current = path.parent();
1400 }
1401 if let CommitMutation::Append { records: batch, .. } = mutation {
1402 records = records.saturating_add(batch.len());
1403 bytes = bytes.saturating_add(batch.iter().map(Bytes::len).sum::<usize>());
1404 }
1405 }
1406 if state.paths.len().saturating_add(new_paths.len()) > limits.paths
1407 || state
1408 .path_bytes
1409 .saturating_add(new_paths.iter().map(|path| path.as_str().len()).sum())
1410 > limits.path_bytes
1411 || state.record_count.saturating_add(records) > limits.records
1412 || state.payload_bytes.saturating_add(bytes) > limits.payload_bytes
1413 {
1414 return Err(StreamError::Capacity);
1415 }
1416 Ok(())
1417}
1418
1419fn apply_coordinated(
1420 state: &mut State,
1421 mutations: Vec<CommitMutation>,
1422 before: &BTreeMap<StreamPath, (Option<Arc<History>>, u64)>,
1423 commit_id: CommitId,
1424) -> Result<Vec<CommittedMutation>, StreamError> {
1425 let mut committed = Vec::with_capacity(mutations.len());
1426 for mutation in mutations {
1427 match mutation {
1428 CommitMutation::Append { path, records } => {
1429 let start = state.paths.get(&path).map_or(0, |stream| stream.tail);
1430 let records = build_records(start, records, commit_id)?;
1431 ensure_path(state, &path);
1432 let resulting_tail = {
1433 let stream = state.paths.get_mut(&path).ok_or(StreamError::Unavailable)?;
1434 stream.history = Some(Arc::new(History::Batch {
1435 parent: stream.history.clone(),
1436 records: Arc::from(records.clone()),
1437 }));
1438 stream.tail = records.last().map_or(start, |record| record.sequence + 1);
1439 stream.changed.send_replace(stream.tail);
1440 stream.tail
1441 };
1442 state.record_count += records.len();
1443 state.payload_bytes += records
1444 .iter()
1445 .map(|record| record.value.len())
1446 .sum::<usize>();
1447 committed.push(CommittedMutation::Append(CommittedAppend {
1448 path,
1449 start,
1450 end: resulting_tail,
1451 tail: resulting_tail,
1452 records,
1453 }));
1454 }
1455 CommitMutation::Fork {
1456 source,
1457 destination,
1458 at_tail,
1459 } => {
1460 let (history, source_tail) = before.get(&source).ok_or(StreamError::NotFound)?;
1461 if at_tail > *source_tail {
1462 return Err(StreamError::InvalidArgument);
1463 }
1464 ensure_path(state, &destination);
1465 let stream = state
1466 .paths
1467 .get_mut(&destination)
1468 .ok_or(StreamError::Unavailable)?;
1469 stream.history = Some(Arc::new(History::Prefix {
1470 source: history.clone(),
1471 tail: at_tail,
1472 }));
1473 stream.tail = at_tail;
1474 stream.changed.send_replace(at_tail);
1475 committed.push(CommittedMutation::Fork(CommittedFork {
1476 source,
1477 destination,
1478 forked_at: at_tail,
1479 tail: at_tail,
1480 }));
1481 }
1482 CommitMutation::Trim { path, before } => {
1483 let stream = state.paths.get_mut(&path).ok_or(StreamError::NotFound)?;
1484 stream.trim_point = stream.trim_point.max(before);
1485 stream.changed.send_replace(stream.tail);
1486 committed.push(CommittedMutation::Trim(CommittedTrim {
1487 path,
1488 trim_point: stream.trim_point,
1489 }));
1490 }
1491 CommitMutation::Delete { path } => {
1492 state.paths.remove(&path).ok_or(StreamError::NotFound)?;
1493 state.retired.insert(path.clone());
1494 committed.push(CommittedMutation::Delete(CommittedDelete { path }));
1495 }
1496 }
1497 }
1498 Ok(committed)
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503 use super::*;
1504 use std::sync::atomic::{AtomicU64, Ordering};
1505
1506 #[derive(Default)]
1507 struct TestClock(AtomicU64);
1508
1509 impl UnixMillisClock for TestClock {
1510 fn now_unix_millis(&self) -> u64 {
1511 self.0.load(Ordering::SeqCst)
1512 }
1513 }
1514
1515 fn path(value: &str) -> Result<StreamPath, StreamError> {
1516 StreamPath::new(value)
1517 }
1518
1519 fn key(value: &'static [u8]) -> Result<crate::IdempotencyKey, StreamError> {
1520 crate::IdempotencyKey::new(Bytes::from_static(value))
1521 }
1522
1523 #[tokio::test]
1524 async fn provider_deadline_is_exclusive_and_replay_precedes_time() -> Result<(), StreamError> {
1525 let clock = Arc::new(TestClock::default());
1526 clock.0.store(100, Ordering::SeqCst);
1527 let provider = MemoryStream::new_with_clock(MemoryLimits::default(), clock.clone());
1528 let request = CommitRequest {
1529 conditions: vec![CommitCondition::Absent {
1530 path: path("deadline/accepted")?,
1531 }],
1532 mutations: vec![CommitMutation::Append {
1533 path: path("deadline/accepted")?,
1534 records: vec![Bytes::from_static(b"accepted")],
1535 }],
1536 idempotency_key: key(b"deadline-accepted")?,
1537 };
1538 assert_eq!(
1539 provider.commit_before(request.clone(), 100).await,
1540 Err(StreamError::DeadlineElapsed)
1541 );
1542 assert_eq!(
1543 provider.commit_before(request.clone(), 99).await,
1544 Err(StreamError::DeadlineElapsed)
1545 );
1546 let committed = provider.commit_before(request.clone(), 101).await?;
1547 assert!(matches!(committed, CommitOutcome::Committed(_)));
1548 clock.0.store(1_000, Ordering::SeqCst);
1549 assert_eq!(provider.commit_before(request, 101).await?, committed);
1550
1551 let retryable = CommitRequest {
1552 conditions: vec![CommitCondition::Absent {
1553 path: path("deadline/retry")?,
1554 }],
1555 mutations: vec![CommitMutation::Append {
1556 path: path("deadline/retry")?,
1557 records: vec![Bytes::from_static(b"retry")],
1558 }],
1559 idempotency_key: key(b"deadline-retry")?,
1560 };
1561 assert_eq!(
1562 provider.commit_before(retryable.clone(), 1_000).await,
1563 Err(StreamError::DeadlineElapsed)
1564 );
1565 assert!(matches!(
1566 provider.commit_before(retryable, 1_001).await?,
1567 CommitOutcome::Committed(_)
1568 ));
1569 Ok(())
1570 }
1571
1572 #[tokio::test]
1573 #[allow(
1574 clippy::indexing_slicing,
1575 reason = "indices are preceded by an assert_eq!(read.len(), 2), proving them in-bounds"
1576 )]
1577 async fn append_fork_follow_and_replay_preserve_exact_lineage() -> Result<(), StreamError> {
1578 let provider = MemoryStream::default();
1579 let source = path("runs/a")?;
1580 let first = AppendRequest {
1581 path: source.clone(),
1582 records: vec![Bytes::from_static(b"one"), Bytes::from_static(b"two")],
1583 if_tail: Some(0),
1584 idempotency_key: Some(key(b"append")?),
1585 };
1586 let committed = provider.append(first.clone()).await;
1587 assert!(committed.is_ok());
1588 assert_eq!(provider.append(first).await, committed);
1589 let forked = provider
1590 .fork(ForkRequest {
1591 source: source.clone(),
1592 destination: path("runs/b")?,
1593 at_tail: Some(1),
1594 idempotency_key: Some(key(b"fork")?),
1595 })
1596 .await?;
1597 assert_eq!(forked.tail, 1);
1598 let child = forked.destination.clone();
1599 provider
1600 .append(AppendRequest {
1601 path: child.clone(),
1602 records: vec![Bytes::from_static(b"child")],
1603 if_tail: Some(1),
1604 idempotency_key: None,
1605 })
1606 .await?;
1607 let read = provider
1608 .read(ReadRequest {
1609 path: child.clone(),
1610 from: 0,
1611 limit: 10,
1612 })
1613 .await?
1614 .collect::<Vec<_>>()
1615 .await;
1616 assert_eq!(read.len(), 2);
1617 assert_eq!(
1618 read[0].as_ref().map(|record| record.value.as_ref()),
1619 Ok(b"one".as_slice())
1620 );
1621 assert_eq!(
1622 read[1].as_ref().map(|record| record.value.as_ref()),
1623 Ok(b"child".as_slice())
1624 );
1625 let mut follow = provider.follow(child.clone(), 2).await?;
1626 provider
1627 .append(AppendRequest {
1628 path: child,
1629 records: vec![Bytes::from_static(b"live")],
1630 if_tail: Some(2),
1631 idempotency_key: None,
1632 })
1633 .await?;
1634 let live = tokio::time::timeout(std::time::Duration::from_secs(1), follow.next()).await;
1635 assert!(matches!(live, Ok(Some(Ok(Record { sequence: 2, .. })))));
1636 Ok(())
1637 }
1638
1639 #[tokio::test]
1640 async fn single_record_reads_seek_deep_history_and_prefixes() -> Result<(), StreamError> {
1641 let provider = MemoryStream::default();
1642 let source = path("seek/source")?;
1643 for index in 0..256_u64 {
1644 provider
1645 .append(AppendRequest {
1646 path: source.clone(),
1647 records: vec![Bytes::from(index.to_string())],
1648 if_tail: Some(index),
1649 idempotency_key: None,
1650 })
1651 .await?;
1652 }
1653 let destination = path("seek/prefix")?;
1654 provider
1655 .fork(ForkRequest {
1656 source: source.clone(),
1657 destination: destination.clone(),
1658 at_tail: Some(128),
1659 idempotency_key: None,
1660 })
1661 .await?;
1662
1663 for (path, expected_tail, expected) in [
1664 (&source, 256_u64, b"255".as_slice()),
1665 (&destination, 128_u64, b"127".as_slice()),
1666 ] {
1667 let record = provider
1668 .read(ReadRequest {
1669 path: path.clone(),
1670 from: expected_tail - 1,
1671 limit: 1,
1672 })
1673 .await?
1674 .next()
1675 .await
1676 .ok_or(StreamError::Unavailable)??;
1677 assert_eq!(record.sequence, expected_tail - 1);
1678 assert_eq!(record.value, expected);
1679 }
1680 Ok(())
1681 }
1682
1683 #[tokio::test]
1684 #[ignore = "bounded scaling benchmark; run with --ignored --nocapture"]
1685 async fn follow_scales_with_history_depth() -> Result<(), StreamError> {
1686 for count in [1_000_u64, 10_000] {
1687 let provider = MemoryStream::default();
1688 let stream = path("bench/follow")?;
1689 for index in 0..count {
1690 provider
1691 .append(AppendRequest {
1692 path: stream.clone(),
1693 records: vec![Bytes::from_static(b"x")],
1694 if_tail: Some(index),
1695 idempotency_key: None,
1696 })
1697 .await?;
1698 }
1699 let started = std::time::Instant::now();
1700 let mut follow = provider.follow(stream, 0).await?;
1701 for expected in 0..count {
1702 let record = follow.next().await.ok_or(StreamError::Unavailable)??;
1703 assert_eq!(record.sequence, expected);
1704 }
1705 eprintln!("follow history={count} read={:?}", started.elapsed());
1706 }
1707 Ok(())
1708 }
1709
1710 #[tokio::test]
1711 #[allow(
1712 clippy::indexing_slicing,
1713 reason = "index is preceded by an assert_eq!(forked.len(), 1), proving it in-bounds"
1714 )]
1715 async fn coordinated_conflict_changes_nothing_and_success_has_one_envelope()
1716 -> Result<(), StreamError> {
1717 let provider = MemoryStream::default();
1718 let source = path("jobs")?;
1719 provider
1720 .append(AppendRequest {
1721 path: source.clone(),
1722 records: vec![Bytes::from_static(b"ready")],
1723 if_tail: Some(0),
1724 idempotency_key: None,
1725 })
1726 .await?;
1727 let request = CommitRequest {
1728 conditions: vec![CommitCondition::Absent {
1729 path: path("runs/a")?,
1730 }],
1731 mutations: vec![CommitMutation::Fork {
1732 source: source.clone(),
1733 destination: path("runs/a")?,
1734 at_tail: 1,
1735 }],
1736 idempotency_key: key(b"commit")?,
1737 };
1738 provider
1739 .append(AppendRequest {
1740 path: source,
1741 records: vec![Bytes::from_static(b"source-progress")],
1742 if_tail: Some(1),
1743 idempotency_key: None,
1744 })
1745 .await?;
1746 let outcome = provider.commit(request.clone()).await;
1747 let Ok(CommitOutcome::Committed(envelope)) = &outcome else {
1748 return Err(StreamError::Unavailable);
1749 };
1750 assert_eq!(provider.commit(request).await, outcome);
1751 assert_eq!(
1752 provider.read_commit(envelope.commit_id).await.as_ref(),
1753 Ok(envelope)
1754 );
1755 let forked = provider
1756 .read(ReadRequest {
1757 path: path("runs/a")?,
1758 from: 0,
1759 limit: 10,
1760 })
1761 .await?
1762 .collect::<Vec<_>>()
1763 .await;
1764 assert_eq!(forked.len(), 1);
1765 assert_eq!(
1766 forked[0].as_ref().map(|record| record.value.as_ref()),
1767 Ok(b"ready".as_slice())
1768 );
1769 Ok(())
1770 }
1771
1772 #[tokio::test]
1773 async fn exact_replay_precedes_capacity_and_changed_arguments_never_reclassify()
1774 -> Result<(), StreamError> {
1775 let provider = MemoryStream::new(MemoryLimits {
1776 paths: 2,
1777 path_bytes: 4,
1778 records: 1,
1779 payload_bytes: 3,
1780 commits: 1,
1781 idempotency_results: 1,
1782 });
1783 let request = AppendRequest {
1784 path: path("a/b")?,
1785 records: vec![Bytes::from_static(b"one")],
1786 if_tail: Some(0),
1787 idempotency_key: Some(key(b"only-key")?),
1788 };
1789 let committed = provider.append(request.clone()).await?;
1790 assert_eq!(
1791 provider.append(request.clone()).await,
1792 Ok(committed.clone())
1793 );
1794 let observation = provider
1795 .inspect_idempotency(key(b"only-key")?)
1796 .await?
1797 .ok_or(StreamError::Unavailable)?;
1798 assert_ne!(observation.request_digest, [0; 32]);
1799 assert_eq!(observation.outcome, IdempotencyOutcome::Append(committed));
1800 assert_eq!(
1801 provider
1802 .append(AppendRequest {
1803 records: vec![Bytes::from_static(b"two")],
1804 ..request
1805 })
1806 .await,
1807 Err(StreamError::IdempotencyMismatch)
1808 );
1809 Ok(())
1810 }
1811
1812 #[tokio::test]
1813 async fn every_fork_cut_reads_the_exact_prefix_and_never_aliases_suffixes()
1814 -> Result<(), StreamError> {
1815 let provider = MemoryStream::default();
1816 let source = path("lineage/source")?;
1817 let bodies = (0_u8..32)
1818 .map(|value| Bytes::from(vec![value]))
1819 .collect::<Vec<_>>();
1820 provider
1821 .append(AppendRequest {
1822 path: source.clone(),
1823 records: bodies,
1824 if_tail: Some(0),
1825 idempotency_key: None,
1826 })
1827 .await?;
1828 for cut in 0_u64..=32 {
1829 let destination = path(&format!("lineage/forks/{cut}"))?;
1830 provider
1831 .fork(ForkRequest {
1832 source: source.clone(),
1833 destination: destination.clone(),
1834 at_tail: Some(cut),
1835 idempotency_key: None,
1836 })
1837 .await?;
1838 let records = provider
1839 .read(ReadRequest {
1840 path: destination,
1841 from: 0,
1842 limit: 64,
1843 })
1844 .await?
1845 .collect::<Vec<_>>()
1846 .await;
1847 assert_eq!(
1848 u64::try_from(records.len()).map_err(|_| StreamError::LimitExceeded)?,
1849 cut
1850 );
1851 }
1852 assert_eq!(provider.tail(source).await?, 32);
1853 Ok(())
1854 }
1855
1856 #[test]
1857 fn permanent_path_validation_rejects_ambiguous_names() {
1858 for invalid in [
1859 "", "/a", "a/", "a//b", ".", "..", "a/./b", "a/../b", "a\n", "a b", "a\\b",
1860 ] {
1861 assert_eq!(StreamPath::new(invalid), Err(StreamError::InvalidPath));
1862 }
1863 assert!(StreamPath::new("a".repeat(crate::MAX_PATH_BYTES)).is_ok());
1864 assert_eq!(
1865 StreamPath::new("a".repeat(crate::MAX_PATH_BYTES + 1)),
1866 Err(StreamError::InvalidPath)
1867 );
1868 assert!(StreamPath::new("runs/run_42/agents/researcher").is_ok());
1869 }
1870}