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