1pub mod in_memory;
2
3use std::borrow::Cow;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use serde::Deserialize;
9use serde::Serialize;
10use ursula_shard::BucketStreamId;
11use ursula_shard::ShardPlacement;
12use ursula_stream::BucketUsageSnapshot;
13use ursula_stream::ColdFlushCandidate;
14use ursula_stream::ColdGcEntry;
15use ursula_stream::StreamCommand;
16use ursula_stream::StreamErrorCode;
17use ursula_stream::StreamErrorContext;
18
19use crate::command::GroupSnapshot;
20use crate::command::GroupWriteCommand;
21use crate::metrics::RaftSnapshotBuildSample;
22use crate::metrics::RaftWriteManySample;
23use crate::metrics::RuntimeMetricsInner;
24use crate::request::AckColdGcResponse;
25use crate::request::AdvanceRetentionRequest;
26use crate::request::AdvanceRetentionResponse;
27use crate::request::AppendBatchRequest;
28use crate::request::AppendExternalRequest;
29use crate::request::AppendRequest;
30use crate::request::AppendResponse;
31use crate::request::AppendTransactionRequest;
32use crate::request::AppendTransactionResponse;
33use crate::request::BootstrapStreamRequest;
34use crate::request::BootstrapStreamResponse;
35use crate::request::CloseStreamRequest;
36use crate::request::CloseStreamResponse;
37use crate::request::ColdHotBacklog;
38use crate::request::ColdWriteAdmission;
39use crate::request::CompactColdRequest;
40use crate::request::CompactColdResponse;
41use crate::request::CreateStreamExternalRequest;
42use crate::request::CreateStreamRequest;
43use crate::request::CreateStreamResponse;
44use crate::request::DeleteSnapshotRequest;
45use crate::request::DeleteStreamRequest;
46use crate::request::DeleteStreamResponse;
47use crate::request::FlushColdRequest;
48use crate::request::FlushColdResponse;
49use crate::request::GetStreamAttrsRequest;
50use crate::request::GetStreamAttrsResponse;
51use crate::request::GroupReadStreamParts;
52use crate::request::HeadStreamRequest;
53use crate::request::HeadStreamResponse;
54use crate::request::ImportGroupStateRequest;
55use crate::request::PlanColdFlushRequest;
56use crate::request::PlanGroupColdFlushRequest;
57use crate::request::PublishSnapshotRequest;
58use crate::request::PublishSnapshotResponse;
59use crate::request::PurgeBucketResponse;
60use crate::request::ReadSnapshotRequest;
61use crate::request::ReadSnapshotResponse;
62use crate::request::ReadStreamRequest;
63use crate::request::ReadStreamResponse;
64use crate::request::SetBucketQuotaRequest;
65use crate::request::SetBucketQuotaResponse;
66use crate::request::TouchStreamAccessResponse;
67use crate::request::UpdateStreamAttrsRequest;
68use crate::request::UpdateStreamAttrsResponse;
69
70pub type GroupAppendFuture<'a> =
71 Pin<Box<dyn Future<Output = Result<AppendResponse, GroupEngineError>> + Send + 'a>>;
72pub type GroupAppendBatchFuture<'a> =
73 Pin<Box<dyn Future<Output = Result<GroupAppendBatchResponse, GroupEngineError>> + Send + 'a>>;
74pub type GroupAppendTransactionFuture<'a> =
75 Pin<Box<dyn Future<Output = Result<AppendTransactionResponse, GroupEngineError>> + Send + 'a>>;
76pub type GroupFlushColdFuture<'a> =
77 Pin<Box<dyn Future<Output = Result<FlushColdResponse, GroupEngineError>> + Send + 'a>>;
78pub type GroupCompactColdFuture<'a> =
79 Pin<Box<dyn Future<Output = Result<CompactColdResponse, GroupEngineError>> + Send + 'a>>;
80pub type GroupPlanColdFlushFuture<'a> =
81 Pin<Box<dyn Future<Output = Result<Option<ColdFlushCandidate>, GroupEngineError>> + Send + 'a>>;
82pub type GroupPlanNextColdFlushBatchFuture<'a> =
83 Pin<Box<dyn Future<Output = Result<Vec<ColdFlushCandidate>, GroupEngineError>> + Send + 'a>>;
84pub type GroupColdHotBacklogFuture<'a> =
85 Pin<Box<dyn Future<Output = Result<ColdHotBacklog, GroupEngineError>> + Send + 'a>>;
86pub type GroupBucketUsageFuture<'a> =
87 Pin<Box<dyn Future<Output = Result<Vec<BucketUsageSnapshot>, GroupEngineError>> + Send + 'a>>;
88pub type GroupCreateStreamFuture<'a> =
89 Pin<Box<dyn Future<Output = Result<CreateStreamResponse, GroupEngineError>> + Send + 'a>>;
90pub type GroupHeadStreamFuture<'a> =
91 Pin<Box<dyn Future<Output = Result<HeadStreamResponse, GroupEngineError>> + Send + 'a>>;
92pub type GroupGetStreamAttrsFuture<'a> =
93 Pin<Box<dyn Future<Output = Result<GetStreamAttrsResponse, GroupEngineError>> + Send + 'a>>;
94pub type GroupReadStreamFuture<'a> =
95 Pin<Box<dyn Future<Output = Result<ReadStreamResponse, GroupEngineError>> + Send + 'a>>;
96pub type GroupReadStreamPartsFuture<'a> =
97 Pin<Box<dyn Future<Output = Result<GroupReadStreamParts, GroupEngineError>> + Send + 'a>>;
98pub type GroupRequireLiveReadOwnerFuture<'a> =
99 Pin<Box<dyn Future<Output = Result<(), GroupEngineError>> + Send + 'a>>;
100pub type GroupPublishSnapshotFuture<'a> =
101 Pin<Box<dyn Future<Output = Result<PublishSnapshotResponse, GroupEngineError>> + Send + 'a>>;
102pub type GroupAdvanceRetentionFuture<'a> =
103 Pin<Box<dyn Future<Output = Result<AdvanceRetentionResponse, GroupEngineError>> + Send + 'a>>;
104pub type GroupSetBucketQuotaFuture<'a> =
105 Pin<Box<dyn Future<Output = Result<SetBucketQuotaResponse, GroupEngineError>> + Send + 'a>>;
106pub type GroupReadSnapshotFuture<'a> =
107 Pin<Box<dyn Future<Output = Result<ReadSnapshotResponse, GroupEngineError>> + Send + 'a>>;
108pub type GroupDeleteSnapshotFuture<'a> =
109 Pin<Box<dyn Future<Output = Result<(), GroupEngineError>> + Send + 'a>>;
110pub type GroupBootstrapStreamFuture<'a> =
111 Pin<Box<dyn Future<Output = Result<BootstrapStreamResponse, GroupEngineError>> + Send + 'a>>;
112pub type GroupTouchStreamAccessFuture<'a> =
113 Pin<Box<dyn Future<Output = Result<TouchStreamAccessResponse, GroupEngineError>> + Send + 'a>>;
114pub type GroupUpdateStreamAttrsFuture<'a> =
115 Pin<Box<dyn Future<Output = Result<UpdateStreamAttrsResponse, GroupEngineError>> + Send + 'a>>;
116pub type GroupCloseStreamFuture<'a> =
117 Pin<Box<dyn Future<Output = Result<CloseStreamResponse, GroupEngineError>> + Send + 'a>>;
118pub type GroupDeleteStreamFuture<'a> =
119 Pin<Box<dyn Future<Output = Result<DeleteStreamResponse, GroupEngineError>> + Send + 'a>>;
120pub type GroupAckColdGcFuture<'a> =
121 Pin<Box<dyn Future<Output = Result<AckColdGcResponse, GroupEngineError>> + Send + 'a>>;
122pub type GroupPurgeBucketFuture<'a> =
123 Pin<Box<dyn Future<Output = Result<PurgeBucketResponse, GroupEngineError>> + Send + 'a>>;
124pub type GroupPlanColdGcFuture<'a> =
125 Pin<Box<dyn Future<Output = Result<Vec<ColdGcEntry>, GroupEngineError>> + Send + 'a>>;
126pub type GroupImportGroupStateFuture<'a> = Pin<
127 Box<
128 dyn Future<Output = Result<crate::request::ImportGroupStateResponse, GroupEngineError>>
129 + Send
130 + 'a,
131 >,
132>;
133pub type GroupSnapshotFuture<'a> =
134 Pin<Box<dyn Future<Output = Result<GroupSnapshot, GroupEngineError>> + Send + 'a>>;
135pub type GroupInstallSnapshotFuture<'a> =
136 Pin<Box<dyn Future<Output = Result<(), GroupEngineError>> + Send + 'a>>;
137pub type GroupShutdownFuture<'a> =
138 Pin<Box<dyn Future<Output = Result<(), GroupEngineError>> + Send + 'a>>;
139pub type GroupWriteFuture<'a> =
140 Pin<Box<dyn Future<Output = Result<GroupWriteResponse, GroupEngineError>> + Send + 'a>>;
141pub type GroupWriteBatchFuture<'a> = Pin<
142 Box<
143 dyn Future<
144 Output = Result<
145 Vec<Result<GroupWriteResponse, GroupEngineError>>,
146 GroupEngineError,
147 >,
148 > + Send
149 + 'a,
150 >,
151>;
152pub type GroupEngineCreateFuture<'a> =
153 Pin<Box<dyn Future<Output = Result<Box<dyn GroupEngine>, GroupEngineError>> + Send + 'a>>;
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct GroupAppendBatchResponse {
157 pub placement: ShardPlacement,
158 pub items: Vec<Result<AppendResponse, GroupEngineError>>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub enum GroupWriteResponse {
163 CreateStream(CreateStreamResponse),
164 Append(AppendResponse),
165 AppendBatch(GroupAppendBatchResponse),
166 PublishSnapshot(PublishSnapshotResponse),
167 AdvanceRetention(AdvanceRetentionResponse),
168 SetBucketQuota(SetBucketQuotaResponse),
169 TouchStreamAccess(TouchStreamAccessResponse),
170 UpdateStreamAttrs(UpdateStreamAttrsResponse),
171 FlushCold(FlushColdResponse),
172 CompactCold(CompactColdResponse),
173 CloseStream(CloseStreamResponse),
174 DeleteStream(DeleteStreamResponse),
175 AckColdGc(AckColdGcResponse),
176 PurgeBucket(PurgeBucketResponse),
177 ImportGroupState(crate::request::ImportGroupStateResponse),
178 Batch(Vec<Result<GroupWriteResponse, GroupEngineError>>),
179}
180
181pub trait GroupEngine: Send + 'static {
182 fn accepts_local_writes(&self) -> bool {
183 true
184 }
185
186 fn create_stream<'a>(
187 &'a mut self,
188 request: CreateStreamRequest,
189 placement: ShardPlacement,
190 admission: ColdWriteAdmission,
191 ) -> GroupCreateStreamFuture<'a>;
192
193 fn create_stream_external<'a>(
194 &'a mut self,
195 request: CreateStreamExternalRequest,
196 _placement: ShardPlacement,
197 ) -> GroupCreateStreamFuture<'a> {
198 Box::pin(async move {
199 Err(GroupEngineError::new(format!(
200 "external stream create is not supported for stream '{}'",
201 request.stream_id
202 )))
203 })
204 }
205
206 fn head_stream<'a>(
207 &'a mut self,
208 request: HeadStreamRequest,
209 placement: ShardPlacement,
210 ) -> GroupHeadStreamFuture<'a>;
211
212 fn bucket_usage<'a>(&'a mut self, placement: ShardPlacement) -> GroupBucketUsageFuture<'a>;
220
221 fn get_stream_attrs<'a>(
222 &'a mut self,
223 request: GetStreamAttrsRequest,
224 _placement: ShardPlacement,
225 ) -> GroupGetStreamAttrsFuture<'a> {
226 Box::pin(async move {
227 Err(GroupEngineError::new(format!(
228 "stream attrs read is not supported for stream '{}'",
229 request.stream_id
230 )))
231 })
232 }
233
234 fn read_stream<'a>(
235 &'a mut self,
236 request: ReadStreamRequest,
237 placement: ShardPlacement,
238 ) -> GroupReadStreamFuture<'a>;
239
240 fn read_stream_parts<'a>(
241 &'a mut self,
242 request: ReadStreamRequest,
243 placement: ShardPlacement,
244 ) -> GroupReadStreamPartsFuture<'a> {
245 Box::pin(async move {
246 let response = self.read_stream(request, placement).await?;
247 Ok(GroupReadStreamParts::from_response(response))
248 })
249 }
250
251 fn require_local_live_read_owner<'a>(
252 &'a mut self,
253 _placement: ShardPlacement,
254 ) -> GroupRequireLiveReadOwnerFuture<'a> {
255 Box::pin(async { Ok(()) })
256 }
257
258 fn publish_snapshot<'a>(
259 &'a mut self,
260 request: PublishSnapshotRequest,
261 _placement: ShardPlacement,
262 ) -> GroupPublishSnapshotFuture<'a> {
263 Box::pin(async move {
264 Err(GroupEngineError::new(format!(
265 "snapshot publish is not supported for stream '{}'",
266 request.stream_id
267 )))
268 })
269 }
270
271 fn advance_retention<'a>(
272 &'a mut self,
273 request: AdvanceRetentionRequest,
274 _placement: ShardPlacement,
275 ) -> GroupAdvanceRetentionFuture<'a> {
276 Box::pin(async move {
277 Err(GroupEngineError::new(format!(
278 "retention advance is not supported for stream '{}'",
279 request.stream_id
280 )))
281 })
282 }
283
284 fn import_group_state<'a>(
287 &'a mut self,
288 _request: ImportGroupStateRequest,
289 placement: ShardPlacement,
290 ) -> GroupImportGroupStateFuture<'a> {
291 Box::pin(async move {
292 Err(GroupEngineError::new(format!(
293 "group state import is not supported for group {}",
294 placement.raft_group_id.0
295 )))
296 })
297 }
298
299 fn set_bucket_quota<'a>(
300 &'a mut self,
301 request: SetBucketQuotaRequest,
302 _placement: ShardPlacement,
303 ) -> GroupSetBucketQuotaFuture<'a> {
304 Box::pin(async move {
305 Err(GroupEngineError::new(format!(
306 "bucket quotas are not supported for bucket '{}'",
307 request.bucket_id
308 )))
309 })
310 }
311
312 fn read_snapshot<'a>(
313 &'a mut self,
314 request: ReadSnapshotRequest,
315 _placement: ShardPlacement,
316 ) -> GroupReadSnapshotFuture<'a> {
317 Box::pin(async move {
318 Err(GroupEngineError::new(format!(
319 "snapshot read is not supported for stream '{}'",
320 request.stream_id
321 )))
322 })
323 }
324
325 fn delete_snapshot<'a>(
326 &'a mut self,
327 request: DeleteSnapshotRequest,
328 _placement: ShardPlacement,
329 ) -> GroupDeleteSnapshotFuture<'a> {
330 Box::pin(async move {
331 Err(GroupEngineError::new(format!(
332 "snapshot delete is not supported for stream '{}'",
333 request.stream_id
334 )))
335 })
336 }
337
338 fn bootstrap_stream<'a>(
339 &'a mut self,
340 request: BootstrapStreamRequest,
341 _placement: ShardPlacement,
342 ) -> GroupBootstrapStreamFuture<'a> {
343 Box::pin(async move {
344 Err(GroupEngineError::new(format!(
345 "bootstrap is not supported for stream '{}'",
346 request.stream_id
347 )))
348 })
349 }
350
351 fn touch_stream_access<'a>(
352 &'a mut self,
353 stream_id: BucketStreamId,
354 now_ms: u64,
355 renew_ttl: bool,
356 placement: ShardPlacement,
357 ) -> GroupTouchStreamAccessFuture<'a>;
358
359 fn update_stream_attrs<'a>(
360 &'a mut self,
361 request: UpdateStreamAttrsRequest,
362 _placement: ShardPlacement,
363 ) -> GroupUpdateStreamAttrsFuture<'a> {
364 Box::pin(async move {
365 Err(GroupEngineError::new(format!(
366 "stream attrs update is not supported for stream '{}'",
367 request.stream_id
368 )))
369 })
370 }
371
372 fn close_stream<'a>(
373 &'a mut self,
374 request: CloseStreamRequest,
375 placement: ShardPlacement,
376 ) -> GroupCloseStreamFuture<'a>;
377
378 fn delete_stream<'a>(
379 &'a mut self,
380 request: DeleteStreamRequest,
381 placement: ShardPlacement,
382 ) -> GroupDeleteStreamFuture<'a>;
383
384 fn ack_cold_gc<'a>(
387 &'a mut self,
388 _up_to_seq: u64,
389 _placement: ShardPlacement,
390 ) -> GroupAckColdGcFuture<'a> {
391 Box::pin(async { Err(GroupEngineError::new("cold GC ack is not supported")) })
392 }
393
394 fn purge_bucket<'a>(
398 &'a mut self,
399 _bucket_id: String,
400 _placement: ShardPlacement,
401 ) -> GroupPurgeBucketFuture<'a> {
402 Box::pin(async { Err(GroupEngineError::new("bucket purge is not supported")) })
403 }
404
405 fn plan_cold_gc<'a>(
408 &'a mut self,
409 _max: usize,
410 _placement: ShardPlacement,
411 ) -> GroupPlanColdGcFuture<'a> {
412 Box::pin(async { Ok(Vec::new()) })
413 }
414
415 fn append<'a>(
416 &'a mut self,
417 request: AppendRequest,
418 placement: ShardPlacement,
419 admission: ColdWriteAdmission,
420 ) -> GroupAppendFuture<'a>;
421
422 fn append_external<'a>(
423 &'a mut self,
424 request: AppendExternalRequest,
425 _placement: ShardPlacement,
426 ) -> GroupAppendFuture<'a> {
427 Box::pin(async move {
428 Err(GroupEngineError::new(format!(
429 "external append is not supported for stream '{}'",
430 request.stream_id
431 )))
432 })
433 }
434
435 fn append_batch<'a>(
436 &'a mut self,
437 request: AppendBatchRequest,
438 placement: ShardPlacement,
439 admission: ColdWriteAdmission,
440 ) -> GroupAppendBatchFuture<'a>;
441
442 fn append_batch_many<'a>(
443 &'a mut self,
444 requests: Vec<AppendBatchRequest>,
445 placement: ShardPlacement,
446 admission: ColdWriteAdmission,
447 ) -> GroupWriteBatchFuture<'a> {
448 Box::pin(async move {
449 let mut responses = Vec::with_capacity(requests.len());
450 for request in requests {
451 let response = self
452 .append_batch(request, placement, admission)
453 .await
454 .map(GroupWriteResponse::AppendBatch);
455 responses.push(response);
456 }
457 Ok(responses)
458 })
459 }
460
461 fn append_transaction<'a>(
462 &'a mut self,
463 _request: AppendTransactionRequest,
464 _placement: ShardPlacement,
465 _admission: ColdWriteAdmission,
466 ) -> GroupAppendTransactionFuture<'a> {
467 Box::pin(async {
468 Err(GroupEngineError::new(
469 "append transactions are not supported by this group engine",
470 ))
471 })
472 }
473
474 fn flush_cold<'a>(
475 &'a mut self,
476 request: FlushColdRequest,
477 _placement: ShardPlacement,
478 ) -> GroupFlushColdFuture<'a> {
479 Box::pin(async move {
480 Err(GroupEngineError::new(format!(
481 "cold flush is not supported for stream '{}'",
482 request.stream_id
483 )))
484 })
485 }
486
487 fn compact_cold<'a>(
488 &'a mut self,
489 request: CompactColdRequest,
490 _placement: ShardPlacement,
491 ) -> GroupCompactColdFuture<'a> {
492 Box::pin(async move {
493 Err(GroupEngineError::new(format!(
494 "cold compaction is not supported for stream '{}'",
495 request.stream_id
496 )))
497 })
498 }
499
500 fn plan_cold_flush<'a>(
501 &'a mut self,
502 request: PlanColdFlushRequest,
503 _placement: ShardPlacement,
504 ) -> GroupPlanColdFlushFuture<'a> {
505 Box::pin(async move {
506 Err(GroupEngineError::new(format!(
507 "cold flush planning is not supported for stream '{}'",
508 request.stream_id
509 )))
510 })
511 }
512
513 fn plan_next_cold_flush_batch<'a>(
514 &'a mut self,
515 _request: PlanGroupColdFlushRequest,
516 _placement: ShardPlacement,
517 _max_candidates: usize,
518 ) -> GroupPlanNextColdFlushBatchFuture<'a> {
519 Box::pin(async move {
520 Err(GroupEngineError::new(
521 "group cold flush planning is not supported",
522 ))
523 })
524 }
525
526 fn cold_hot_backlog<'a>(
527 &'a mut self,
528 stream_id: BucketStreamId,
529 _placement: ShardPlacement,
530 ) -> GroupColdHotBacklogFuture<'a> {
531 Box::pin(async move {
532 Err(GroupEngineError::new(format!(
533 "cold hot backlog is not supported for stream '{stream_id}'"
534 )))
535 })
536 }
537
538 fn snapshot<'a>(&'a mut self, placement: ShardPlacement) -> GroupSnapshotFuture<'a>;
539
540 fn install_snapshot<'a>(
541 &'a mut self,
542 snapshot: GroupSnapshot,
543 ) -> GroupInstallSnapshotFuture<'a>;
544
545 fn shutdown<'a>(&'a mut self) -> GroupShutdownFuture<'a> {
546 Box::pin(async { Ok(()) })
547 }
548
549 fn write_batch<'a>(
550 &'a mut self,
551 commands: Vec<GroupWriteCommand>,
552 placement: ShardPlacement,
553 ) -> GroupWriteBatchFuture<'a> {
554 Box::pin(async move {
555 let mut responses = Vec::with_capacity(commands.len());
556 for command in commands {
557 let response = match command {
558 GroupWriteCommand::Stream(command) => {
559 self.dispatch_stream_command(command, placement).await
560 }
561 GroupWriteCommand::Batch { commands } => {
562 let mut batched = Vec::with_capacity(commands.len());
563 for command in commands {
564 batched.push(self.dispatch_stream_command(command, placement).await);
565 }
566 Ok(GroupWriteResponse::Batch(batched))
567 }
568 GroupWriteCommand::Transaction { .. } => Err(GroupEngineError::new(
569 "append transactions require an atomic group engine",
570 )),
571 };
572 responses.push(response);
573 }
574 Ok(responses)
575 })
576 }
577
578 fn dispatch_stream_command<'a>(
582 &'a mut self,
583 command: StreamCommand,
584 placement: ShardPlacement,
585 ) -> GroupWriteFuture<'a> {
586 Box::pin(async move {
587 match command {
588 StreamCommand::CreateStream {
589 stream_id,
590 content_type,
591 initial_payload,
592 close_after,
593 stream_seq,
594 producer,
595 stream_ttl_seconds,
596 stream_expires_at_ms,
597 attrs,
598 now_ms,
599 } => self
600 .create_stream(
601 CreateStreamRequest {
602 stream_id,
603 content_type,
604 content_type_explicit: true,
605 initial_payload,
606 close_after,
607 stream_seq,
608 producer,
609 stream_ttl_seconds,
610 stream_expires_at_ms,
611 attrs,
612 now_ms,
613 },
614 placement,
615 ColdWriteAdmission::default(),
616 )
617 .await
618 .map(GroupWriteResponse::CreateStream),
619 StreamCommand::CreateExternal {
620 stream_id,
621 content_type,
622 initial_payload,
623 record_ends,
624 close_after,
625 stream_seq,
626 producer,
627 stream_ttl_seconds,
628 stream_expires_at_ms,
629 attrs,
630 now_ms,
631 } => self
632 .create_stream_external(
633 CreateStreamExternalRequest {
634 stream_id,
635 content_type,
636 initial_payload,
637 record_ends,
638 close_after,
639 stream_seq,
640 producer,
641 stream_ttl_seconds,
642 stream_expires_at_ms,
643 attrs,
644 now_ms,
645 },
646 placement,
647 )
648 .await
649 .map(GroupWriteResponse::CreateStream),
650 StreamCommand::Append {
651 stream_id,
652 content_type,
653 payload,
654 close_after,
655 stream_seq,
656 producer,
657 now_ms,
658 record_match,
659 } => self
660 .append(
661 AppendRequest {
662 stream_id,
663 content_type: content_type.unwrap_or_default(),
664 payload,
665 close_after,
666 stream_seq,
667 producer,
668 now_ms,
669 record_match,
670 },
671 placement,
672 ColdWriteAdmission::default(),
673 )
674 .await
675 .map(GroupWriteResponse::Append),
676 StreamCommand::AppendExternal {
677 stream_id,
678 content_type,
679 payload,
680 record_ends,
681 close_after,
682 stream_seq,
683 producer,
684 now_ms,
685 record_match,
686 } => self
687 .append_external(
688 AppendExternalRequest {
689 stream_id,
690 content_type: content_type.unwrap_or_default(),
691 payload,
692 record_ends,
693 close_after,
694 stream_seq,
695 producer,
696 now_ms,
697 record_match,
698 },
699 placement,
700 )
701 .await
702 .map(GroupWriteResponse::Append),
703 StreamCommand::AppendBatch {
704 stream_id,
705 content_type,
706 payloads,
707 producer,
708 now_ms,
709 } => self
710 .append_batch(
711 AppendBatchRequest {
712 stream_id,
713 content_type: content_type.unwrap_or_default(),
714 payloads,
715 producer,
716 now_ms,
717 },
718 placement,
719 ColdWriteAdmission::default(),
720 )
721 .await
722 .map(GroupWriteResponse::AppendBatch),
723 StreamCommand::PublishSnapshot {
724 stream_id,
725 snapshot_offset,
726 content_type,
727 payload,
728 expected_digest,
729 now_ms,
730 } => self
731 .publish_snapshot(
732 PublishSnapshotRequest {
733 stream_id,
734 snapshot_offset,
735 content_type,
736 payload,
737 expected_digest,
738 now_ms,
739 },
740 placement,
741 )
742 .await
743 .map(GroupWriteResponse::PublishSnapshot),
744 StreamCommand::AdvanceRetention {
745 stream_id,
746 retained_offset,
747 now_ms,
748 } => self
749 .advance_retention(
750 AdvanceRetentionRequest {
751 stream_id,
752 retained_offset,
753 now_ms,
754 },
755 placement,
756 )
757 .await
758 .map(GroupWriteResponse::AdvanceRetention),
759 StreamCommand::TouchStreamAccess {
760 stream_id,
761 now_ms,
762 renew_ttl,
763 } => self
764 .touch_stream_access(stream_id, now_ms, renew_ttl, placement)
765 .await
766 .map(GroupWriteResponse::TouchStreamAccess),
767 StreamCommand::UpdateStreamAttrs {
768 stream_id,
769 attrs,
770 now_ms,
771 } => self
772 .update_stream_attrs(
773 UpdateStreamAttrsRequest {
774 stream_id,
775 attrs,
776 now_ms,
777 },
778 placement,
779 )
780 .await
781 .map(GroupWriteResponse::UpdateStreamAttrs),
782 StreamCommand::FlushCold { stream_id, chunk } => self
783 .flush_cold(FlushColdRequest { stream_id, chunk }, placement)
784 .await
785 .map(GroupWriteResponse::FlushCold),
786 StreamCommand::CompactCold {
787 stream_id,
788 old_chunks,
789 replacement,
790 gc_not_before_ms,
791 } => self
792 .compact_cold(
793 CompactColdRequest {
794 stream_id,
795 old_chunks,
796 replacement,
797 gc_not_before_ms,
798 },
799 placement,
800 )
801 .await
802 .map(GroupWriteResponse::CompactCold),
803 StreamCommand::Close {
804 stream_id,
805 stream_seq,
806 producer,
807 now_ms,
808 } => self
809 .close_stream(
810 CloseStreamRequest {
811 stream_id,
812 stream_seq,
813 producer,
814 now_ms,
815 },
816 placement,
817 )
818 .await
819 .map(GroupWriteResponse::CloseStream),
820 StreamCommand::DeleteStream { stream_id } => self
821 .delete_stream(DeleteStreamRequest { stream_id }, placement)
822 .await
823 .map(GroupWriteResponse::DeleteStream),
824 StreamCommand::AckColdGc { up_to_seq } => self
825 .ack_cold_gc(up_to_seq, placement)
826 .await
827 .map(GroupWriteResponse::AckColdGc),
828 StreamCommand::PurgeBucket { bucket_id } => self
829 .purge_bucket(bucket_id, placement)
830 .await
831 .map(GroupWriteResponse::PurgeBucket),
832 StreamCommand::ImportSnapshot { snapshot } => self
833 .import_group_state(ImportGroupStateRequest { snapshot }, placement)
834 .await
835 .map(GroupWriteResponse::ImportGroupState),
836 StreamCommand::SetBucketQuota {
837 bucket_id,
838 max_streams,
839 max_retained_bytes,
840 } => self
841 .set_bucket_quota(
842 SetBucketQuotaRequest {
843 bucket_id,
844 max_streams,
845 max_retained_bytes,
846 },
847 placement,
848 )
849 .await
850 .map(GroupWriteResponse::SetBucketQuota),
851 StreamCommand::CreateBucket { .. } | StreamCommand::DeleteBucket { .. } => Err(
852 GroupEngineError::new("bucket commands are not valid group writes"),
853 ),
854 }
855 })
856 }
857}
858
859pub trait GroupEngineFactory: Send + Sync + 'static {
860 fn hosts_group(&self, _placement: ShardPlacement) -> bool {
861 true
862 }
863
864 fn create<'a>(
865 &'a self,
866 placement: ShardPlacement,
867 metrics: GroupEngineMetrics,
868 ) -> GroupEngineCreateFuture<'a>;
869}
870
871#[derive(Debug, Clone)]
872pub struct GroupEngineMetrics {
873 pub(crate) inner: Arc<RuntimeMetricsInner>,
874}
875
876impl GroupEngineMetrics {
877 pub fn record_wal_batch(
878 &self,
879 placement: ShardPlacement,
880 record_count: usize,
881 write_ns: u64,
882 sync_ns: u64,
883 ) {
884 self.inner.record_wal_batch(
885 placement.core_id,
886 placement.raft_group_id,
887 u64::try_from(record_count).expect("record count fits u64"),
888 write_ns,
889 sync_ns,
890 );
891 }
892
893 pub fn record_wal_storage(
894 &self,
895 placement: ShardPlacement,
896 fsyncs: u64,
897 fsync_records: u64,
898 reclaims: u64,
899 reclaimed_bytes: u64,
900 reclaim_ns: u64,
901 physical_bytes: u64,
902 ) {
903 self.inner.record_wal_storage(
904 placement.core_id,
905 placement.raft_group_id,
906 fsyncs,
907 fsync_records,
908 reclaims,
909 reclaimed_bytes,
910 reclaim_ns,
911 physical_bytes,
912 );
913 }
914
915 pub fn record_wal_recovery(
916 &self,
917 placement: ShardPlacement,
918 recovery_ns: u64,
919 records: u64,
920 bytes: u64,
921 live_entries: u64,
922 ) {
923 self.inner.record_wal_recovery(
924 placement.core_id,
925 recovery_ns,
926 records,
927 bytes,
928 live_entries,
929 );
930 }
931
932 pub fn record_raft_write_many(
933 &self,
934 placement: ShardPlacement,
935 command_count: usize,
936 logical_command_count: usize,
937 response_count: usize,
938 submit_ns: u64,
939 response_ns: u64,
940 ) {
941 self.inner.record_raft_write_many(
942 placement.core_id,
943 placement.raft_group_id,
944 RaftWriteManySample {
945 command_count: u64::try_from(command_count).expect("command count fits u64"),
946 logical_command_count: u64::try_from(logical_command_count)
947 .expect("logical command count fits u64"),
948 response_count: u64::try_from(response_count).expect("response count fits u64"),
949 submit_ns,
950 response_ns,
951 },
952 );
953 }
954
955 pub fn record_raft_apply_batch(
956 &self,
957 placement: ShardPlacement,
958 entry_count: usize,
959 apply_ns: u64,
960 ) {
961 self.inner.record_raft_apply_batch(
962 placement.core_id,
963 placement.raft_group_id,
964 u64::try_from(entry_count).expect("entry count fits u64"),
965 apply_ns,
966 );
967 }
968
969 pub fn record_raft_snapshot_build(
970 &self,
971 placement: ShardPlacement,
972 stream_count: usize,
973 body_bytes: usize,
974 pointer_bytes: usize,
975 build_ns: u64,
976 external_upload: bool,
977 inline_fallback: bool,
978 ) {
979 self.inner
980 .record_raft_snapshot_build(placement.raft_group_id, RaftSnapshotBuildSample {
981 streams: u64::try_from(stream_count).expect("stream count fits u64"),
982 body_bytes: u64::try_from(body_bytes).expect("snapshot body bytes fits u64"),
983 pointer_bytes: u64::try_from(pointer_bytes)
984 .expect("snapshot pointer bytes fits u64"),
985 build_ns,
986 external_upload,
987 inline_fallback,
988 });
989 }
990}
991
992#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993pub struct GroupLeaderHint {
994 pub node_id: Option<u64>,
995 pub address: Option<String>,
996}
997
998#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
999pub struct StreamEngineError {
1000 message: String,
1001 code: StreamErrorCode,
1002 next_offset: Option<u64>,
1003 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1004 context: Vec<StreamErrorContext>,
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1012pub enum GroupInfraError {
1013 #[error("{message}")]
1014 Internal { message: String },
1015 #[error("ProtoDecode: protobuf raft payload missing {field}")]
1016 ProtoDecode { field: String },
1017 #[error(
1018 "ColdBackpressure: stream '{stream_id}' would raise group hot bytes from {before_group_hot_bytes} to {after_group_hot_bytes}, above limit {limit}"
1019 )]
1020 ColdBackpressure {
1021 stream_id: BucketStreamId,
1022 before_group_hot_bytes: u64,
1023 after_group_hot_bytes: u64,
1024 limit: u64,
1025 },
1026 #[error(
1027 "RaftUncommittedBackpressure: group uncommitted bytes {current} plus incoming {incoming} would exceed limit {limit}"
1028 )]
1029 RaftUncommittedBackpressure {
1030 current: u64,
1031 incoming: u64,
1032 limit: u64,
1033 },
1034}
1035
1036impl GroupInfraError {
1037 pub fn internal(message: impl Into<String>) -> Self {
1038 Self::Internal {
1039 message: message.into(),
1040 }
1041 }
1042
1043 pub fn proto_decode(field: impl Into<String>) -> Self {
1044 Self::ProtoDecode {
1045 field: field.into(),
1046 }
1047 }
1048
1049 pub fn cold_backpressure(
1050 stream_id: BucketStreamId,
1051 before_group_hot_bytes: u64,
1052 after_group_hot_bytes: u64,
1053 limit: u64,
1054 ) -> Self {
1055 Self::ColdBackpressure {
1056 stream_id,
1057 before_group_hot_bytes,
1058 after_group_hot_bytes,
1059 limit,
1060 }
1061 }
1062
1063 pub fn raft_uncommitted_backpressure(current: u64, incoming: u64, limit: u64) -> Self {
1064 Self::RaftUncommittedBackpressure {
1065 current,
1066 incoming,
1067 limit,
1068 }
1069 }
1070
1071 pub fn message(&self) -> Cow<'_, str> {
1072 match self {
1073 Self::Internal { message } => Cow::Borrowed(message),
1074 other => Cow::Owned(other.to_string()),
1075 }
1076 }
1077
1078 pub fn is_cold_backpressure(&self) -> bool {
1079 matches!(self, Self::ColdBackpressure { .. })
1080 }
1081
1082 pub fn is_backpressure(&self) -> bool {
1083 matches!(
1084 self,
1085 Self::ColdBackpressure { .. } | Self::RaftUncommittedBackpressure { .. }
1086 )
1087 }
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1091pub enum GroupEngineError {
1092 #[error("{}", .0.message)]
1093 Stream(StreamEngineError),
1094 #[error("{}", .0.message())]
1095 Infra(GroupInfraError),
1096 #[error("{message}")]
1097 ForwardToLeader {
1098 message: String,
1099 leader_hint: GroupLeaderHint,
1100 },
1101}
1102
1103impl GroupEngineError {
1104 pub fn new(message: impl Into<String>) -> Self {
1105 Self::Infra(GroupInfraError::internal(message))
1106 }
1107
1108 pub fn cold_backpressure(
1109 stream_id: BucketStreamId,
1110 before_group_hot_bytes: u64,
1111 after_group_hot_bytes: u64,
1112 limit: u64,
1113 ) -> Self {
1114 Self::Infra(GroupInfraError::cold_backpressure(
1115 stream_id,
1116 before_group_hot_bytes,
1117 after_group_hot_bytes,
1118 limit,
1119 ))
1120 }
1121
1122 pub fn raft_uncommitted_backpressure(current: u64, incoming: u64, limit: u64) -> Self {
1123 Self::Infra(GroupInfraError::raft_uncommitted_backpressure(
1124 current, incoming, limit,
1125 ))
1126 }
1127
1128 pub fn stream(code: StreamErrorCode, message: impl Into<String>) -> Self {
1129 Self::stream_with_next_offset(code, message, None)
1130 }
1131
1132 pub fn stream_with_next_offset(
1133 code: StreamErrorCode,
1134 message: impl Into<String>,
1135 next_offset: Option<u64>,
1136 ) -> Self {
1137 Self::stream_with_context(code, message, next_offset, vec![])
1138 }
1139
1140 pub fn stream_with_context(
1141 code: StreamErrorCode,
1142 message: impl Into<String>,
1143 next_offset: Option<u64>,
1144 context: Vec<StreamErrorContext>,
1145 ) -> Self {
1146 Self::Stream(StreamEngineError {
1147 message: format!("{code:?}: {}", message.into()),
1148 code,
1149 next_offset,
1150 context,
1151 })
1152 }
1153
1154 pub fn stream_from_replicated(
1155 message: impl Into<String>,
1156 code: StreamErrorCode,
1157 next_offset: Option<u64>,
1158 context: Vec<StreamErrorContext>,
1159 ) -> Self {
1160 Self::Stream(StreamEngineError {
1161 message: message.into(),
1162 code,
1163 next_offset,
1164 context,
1165 })
1166 }
1167
1168 pub fn forward_to_leader(
1169 message: impl Into<String>,
1170 node_id: Option<u64>,
1171 address: Option<String>,
1172 ) -> Self {
1173 Self::ForwardToLeader {
1174 message: message.into(),
1175 leader_hint: GroupLeaderHint { node_id, address },
1176 }
1177 }
1178
1179 pub fn message(&self) -> Cow<'_, str> {
1180 match self {
1181 Self::Stream(err) => Cow::Borrowed(&err.message),
1182 Self::Infra(err) => err.message(),
1183 Self::ForwardToLeader { message, .. } => Cow::Borrowed(message),
1184 }
1185 }
1186
1187 pub fn code(&self) -> Option<StreamErrorCode> {
1188 match self {
1189 Self::Stream(err) => Some(err.code),
1190 Self::Infra(_) | Self::ForwardToLeader { .. } => None,
1191 }
1192 }
1193
1194 pub fn stream_parts(
1195 &self,
1196 ) -> Option<(&str, StreamErrorCode, Option<u64>, &[StreamErrorContext])> {
1197 match self {
1198 Self::Stream(err) => Some((&err.message, err.code, err.next_offset, &err.context)),
1199 Self::Infra(_) | Self::ForwardToLeader { .. } => None,
1200 }
1201 }
1202
1203 pub fn next_offset(&self) -> Option<u64> {
1204 match self {
1205 Self::Stream(err) => err.next_offset,
1206 Self::Infra(_) | Self::ForwardToLeader { .. } => None,
1207 }
1208 }
1209
1210 pub fn context(&self) -> &[StreamErrorContext] {
1211 match self {
1212 Self::Stream(err) => &err.context,
1213 Self::Infra(_) | Self::ForwardToLeader { .. } => &[],
1214 }
1215 }
1216
1217 pub fn leader_hint(&self) -> Option<&GroupLeaderHint> {
1218 match self {
1219 Self::ForwardToLeader { leader_hint, .. } => Some(leader_hint),
1220 Self::Stream(_) | Self::Infra(_) => None,
1221 }
1222 }
1223
1224 pub fn infra(&self) -> Option<&GroupInfraError> {
1225 match self {
1226 Self::Infra(err) => Some(err),
1227 Self::Stream(_) | Self::ForwardToLeader { .. } => None,
1228 }
1229 }
1230
1231 pub fn is_cold_backpressure(&self) -> bool {
1232 self.infra()
1233 .is_some_and(GroupInfraError::is_cold_backpressure)
1234 }
1235
1236 pub fn is_backpressure(&self) -> bool {
1237 self.infra().is_some_and(GroupInfraError::is_backpressure)
1238 }
1239}