1use std::sync::Arc;
2
3use bytes::Bytes;
4use serde::Deserialize;
5use serde::Serialize;
6use ursula_shard::BucketStreamId;
7use ursula_shard::ShardPlacement;
8use ursula_stream::ColdChunkRef;
9use ursula_stream::ExternalPayloadRef;
10use ursula_stream::ProducerRequest;
11use ursula_stream::StreamAttrs;
12use ursula_stream::StreamIntegritySnapshot;
13use ursula_stream::StreamReadPlan;
14use ursula_stream::StreamReadSegment;
15use ursula_stream::StreamRecordRange;
16
17use crate::cold_index::ColdIndexPageCache;
18use crate::cold_index::ColdStoreColdIndexPageStore;
19use crate::cold_store::ColdStoreHandle;
20use crate::cold_store::DEFAULT_CONTENT_TYPE;
21use crate::engine::GroupEngineError;
22use crate::engine::in_memory::InMemoryGroupEngine;
23use crate::error::RuntimeError;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CreateStreamRequest {
27 pub stream_id: BucketStreamId,
28 pub content_type: String,
29 pub content_type_explicit: bool,
30 pub initial_payload: Bytes,
31 pub close_after: bool,
32 pub stream_seq: Option<String>,
33 pub producer: Option<ProducerRequest>,
34 pub stream_ttl_seconds: Option<u64>,
35 pub stream_expires_at_ms: Option<u64>,
36 pub attrs: Option<StreamAttrs>,
37 pub now_ms: u64,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct CreateStreamExternalRequest {
42 pub stream_id: BucketStreamId,
43 pub content_type: String,
44 pub initial_payload: ExternalPayloadRef,
45 #[serde(default)]
46 pub record_ends: Vec<u64>,
47 pub close_after: bool,
48 pub stream_seq: Option<String>,
49 pub producer: Option<ProducerRequest>,
50 pub stream_ttl_seconds: Option<u64>,
51 pub stream_expires_at_ms: Option<u64>,
52 pub attrs: Option<StreamAttrs>,
53 pub now_ms: u64,
54}
55
56impl CreateStreamExternalRequest {
57 pub fn from_create_request(
58 request: CreateStreamRequest,
59 initial_payload: ExternalPayloadRef,
60 record_ends: Vec<u64>,
61 ) -> Self {
62 Self {
63 stream_id: request.stream_id,
64 content_type: request.content_type,
65 initial_payload,
66 record_ends,
67 close_after: request.close_after,
68 stream_seq: request.stream_seq,
69 producer: request.producer,
70 stream_ttl_seconds: request.stream_ttl_seconds,
71 stream_expires_at_ms: request.stream_expires_at_ms,
72 attrs: request.attrs,
73 now_ms: request.now_ms,
74 }
75 }
76}
77
78impl CreateStreamRequest {
79 pub fn canonical_record_ends(&self) -> Vec<u64> {
80 ursula_stream::canonical_json_record_ends(&self.content_type, &self.initial_payload)
81 .unwrap_or_default()
82 }
83
84 pub fn new(stream_id: BucketStreamId, content_type: impl Into<String>) -> Self {
85 Self {
86 stream_id,
87 content_type: content_type.into(),
88 content_type_explicit: true,
89 initial_payload: Bytes::new(),
90 close_after: false,
91 stream_seq: None,
92 producer: None,
93 stream_ttl_seconds: None,
94 stream_expires_at_ms: None,
95 attrs: None,
96 now_ms: 0,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct CreateStreamResponse {
103 pub placement: ShardPlacement,
104 pub next_offset: u64,
105 pub closed: bool,
106 pub already_exists: bool,
107 pub group_commit_index: u64,
108 pub record_range: Option<StreamRecordRange>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct HeadStreamRequest {
113 pub stream_id: BucketStreamId,
114 pub now_ms: u64,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct HeadStreamResponse {
119 pub placement: ShardPlacement,
120 pub content_type: String,
121 pub tail_offset: u64,
122 pub cold_hot_start_offset: u64,
123 pub closed: bool,
124 pub stream_ttl_seconds: Option<u64>,
125 pub stream_expires_at_ms: Option<u64>,
126 pub snapshot_offset: Option<u64>,
127 pub snapshot_digest: Option<String>,
128 pub retained_offset: u64,
129 pub integrity: StreamIntegritySnapshot,
130 pub record_range: Option<StreamRecordRange>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct GetStreamAttrsRequest {
135 pub stream_id: BucketStreamId,
136 pub now_ms: u64,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct GetStreamAttrsResponse {
141 pub placement: ShardPlacement,
142 pub attrs: Option<StreamAttrs>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct UpdateStreamAttrsRequest {
147 pub stream_id: BucketStreamId,
148 pub attrs: Option<StreamAttrs>,
149 pub now_ms: u64,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153pub struct UpdateStreamAttrsResponse {
154 pub placement: ShardPlacement,
155 pub changed: bool,
156 pub group_commit_index: u64,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ReadStreamRequest {
161 pub stream_id: BucketStreamId,
162 pub offset: u64,
163 pub max_len: usize,
164 pub now_ms: u64,
165 pub record: Option<u64>,
166 pub max_records: Option<u64>,
167}
168
169impl ReadStreamRequest {
170 pub(crate) fn same_wait_plan(&self, other: &Self) -> bool {
171 self.stream_id == other.stream_id
172 && self.offset == other.offset
173 && self.max_len == other.max_len
174 && self.record == other.record
175 && self.max_records == other.max_records
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct ReadStreamResponse {
181 pub placement: ShardPlacement,
182 pub offset: u64,
183 pub next_offset: u64,
184 pub content_type: String,
185 #[serde(with = "serde_bytes")]
186 pub payload: Vec<u8>,
187 pub up_to_date: bool,
188 pub closed: bool,
189 pub retained_record_range: Option<StreamRecordRange>,
190 pub record_range: Option<StreamRecordRange>,
191}
192
193pub enum GroupReadStreamBody {
194 Materialized(Vec<u8>),
195 Planned {
196 stream_id: BucketStreamId,
197 plan: StreamReadPlan,
198 cold_store: Option<ColdStoreHandle>,
199 cold_index_cache: Option<Arc<ColdIndexPageCache<ColdStoreColdIndexPageStore>>>,
200 },
201 #[cfg(test)]
202 Blocking {
203 entered: Arc<crate::rt::sync::Notify>,
204 materialized: Arc<crate::rt::sync::Notify>,
205 release: Arc<crate::rt::sync::Notify>,
206 payload: Vec<u8>,
207 },
208}
209
210pub struct GroupReadStreamParts {
211 pub placement: ShardPlacement,
212 pub offset: u64,
213 pub next_offset: u64,
214 pub content_type: String,
215 pub up_to_date: bool,
216 pub closed: bool,
217 pub retained_record_range: Option<StreamRecordRange>,
218 pub record_range: Option<StreamRecordRange>,
219 pub body: GroupReadStreamBody,
220}
221
222impl GroupReadStreamParts {
223 pub fn from_response(response: ReadStreamResponse) -> Self {
224 Self {
225 placement: response.placement,
226 offset: response.offset,
227 next_offset: response.next_offset,
228 content_type: response.content_type,
229 up_to_date: response.up_to_date,
230 closed: response.closed,
231 retained_record_range: response.retained_record_range,
232 record_range: response.record_range,
233 body: GroupReadStreamBody::Materialized(response.payload),
234 }
235 }
236
237 pub fn from_plan(
238 placement: ShardPlacement,
239 stream_id: BucketStreamId,
240 plan: StreamReadPlan,
241 cold_store: Option<ColdStoreHandle>,
242 cold_index_cache: Option<Arc<ColdIndexPageCache<ColdStoreColdIndexPageStore>>>,
243 ) -> Self {
244 Self {
245 placement,
246 offset: plan.offset,
247 next_offset: plan.next_offset,
248 content_type: plan.content_type.clone(),
249 up_to_date: plan.up_to_date,
250 closed: plan.closed,
251 retained_record_range: plan.retained_record_range,
252 record_range: plan.record_range,
253 body: GroupReadStreamBody::Planned {
254 stream_id,
255 plan,
256 cold_store,
257 cold_index_cache,
258 },
259 }
260 }
261
262 pub async fn into_response(self) -> Result<ReadStreamResponse, GroupEngineError> {
263 let payload = match &self.body {
264 GroupReadStreamBody::Materialized(payload) => payload.clone(),
265 GroupReadStreamBody::Planned {
266 stream_id,
267 plan,
268 cold_store,
269 cold_index_cache,
270 } => {
271 InMemoryGroupEngine::read_payload_from_plan(
272 cold_store.as_ref(),
273 cold_index_cache.as_ref(),
274 stream_id,
275 plan,
276 )
277 .await?
278 }
279 #[cfg(test)]
280 GroupReadStreamBody::Blocking {
281 entered,
282 materialized,
283 release,
284 payload,
285 } => {
286 entered.notify_one();
287 materialized.notify_one();
288 release.notified().await;
289 payload.clone()
290 }
291 };
292 Ok(ReadStreamResponse {
293 placement: self.placement,
294 offset: self.offset,
295 next_offset: self.next_offset,
296 content_type: self.content_type,
297 payload,
298 up_to_date: self.up_to_date,
299 closed: self.closed,
300 retained_record_range: self.retained_record_range,
301 record_range: self.record_range,
302 })
303 }
304
305 pub fn payload_is_empty(&self) -> bool {
306 match &self.body {
307 GroupReadStreamBody::Materialized(payload) => payload.is_empty(),
308 GroupReadStreamBody::Planned { plan, .. } => {
309 plan.segments.iter().all(|segment| match segment {
310 StreamReadSegment::Hot(payload) => payload.is_empty(),
311 StreamReadSegment::ColdIndex(segment) => segment.len == 0,
312 StreamReadSegment::Object(segment) => segment.len == 0,
313 })
314 }
315 #[cfg(test)]
316 GroupReadStreamBody::Blocking { payload, .. } => payload.is_empty(),
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct PublishSnapshotRequest {
323 pub stream_id: BucketStreamId,
324 pub snapshot_offset: u64,
325 pub content_type: String,
326 pub payload: Bytes,
327 pub expected_digest: Option<String>,
328 pub now_ms: u64,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct PublishSnapshotResponse {
333 pub placement: ShardPlacement,
334 pub snapshot_offset: u64,
335 pub snapshot_digest: String,
336 pub group_commit_index: u64,
337 pub record_range: Option<StreamRecordRange>,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct AdvanceRetentionRequest {
342 pub stream_id: BucketStreamId,
343 pub retained_offset: u64,
344 pub now_ms: u64,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct AdvanceRetentionResponse {
349 pub placement: ShardPlacement,
350 pub retained_offset: u64,
351 pub group_commit_index: u64,
352 pub record_range: Option<StreamRecordRange>,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct SetBucketQuotaRequest {
359 pub bucket_id: String,
360 pub max_streams: Option<u64>,
361 pub max_retained_bytes: Option<u64>,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365pub struct SetBucketQuotaResponse {
366 pub placement: ShardPlacement,
367 pub group_commit_index: u64,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct ImportGroupStateRequest {
372 pub snapshot: Box<ursula_stream::StreamSnapshot>,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
376pub struct ImportGroupStateResponse {
377 pub placement: ShardPlacement,
378 pub buckets: u64,
379 pub streams: u64,
380 pub group_commit_index: u64,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct ReadSnapshotRequest {
385 pub stream_id: BucketStreamId,
386 pub snapshot_offset: Option<u64>,
387 pub now_ms: u64,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct ReadSnapshotResponse {
392 pub placement: ShardPlacement,
393 pub snapshot_offset: u64,
394 pub next_offset: u64,
395 pub content_type: String,
396 pub snapshot_digest: String,
397 pub payload: Vec<u8>,
398 pub up_to_date: bool,
399 pub record_range: Option<StreamRecordRange>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct DeleteSnapshotRequest {
404 pub stream_id: BucketStreamId,
405 pub snapshot_offset: u64,
406 pub now_ms: u64,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct BootstrapStreamRequest {
411 pub stream_id: BucketStreamId,
412 pub now_ms: u64,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct BootstrapUpdate {
417 pub start_offset: u64,
418 pub next_offset: u64,
419 pub content_type: String,
420 pub payload: Vec<u8>,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct BootstrapStreamResponse {
425 pub placement: ShardPlacement,
426 pub snapshot_offset: Option<u64>,
427 pub snapshot_content_type: String,
428 pub snapshot_payload: Vec<u8>,
429 pub updates: Vec<BootstrapUpdate>,
430 pub next_offset: u64,
431 pub up_to_date: bool,
432 pub closed: bool,
433 pub record_range: Option<StreamRecordRange>,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct CloseStreamRequest {
438 pub stream_id: BucketStreamId,
439 pub stream_seq: Option<String>,
440 pub producer: Option<ProducerRequest>,
441 pub now_ms: u64,
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445pub struct CloseStreamResponse {
446 pub placement: ShardPlacement,
447 pub next_offset: u64,
448 pub group_commit_index: u64,
449 pub deduplicated: bool,
450 pub record_range: Option<StreamRecordRange>,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct DeleteStreamRequest {
455 pub stream_id: BucketStreamId,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459pub struct DeleteStreamResponse {
460 pub placement: ShardPlacement,
461 pub group_commit_index: u64,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct AckColdGcResponse {
466 pub placement: ShardPlacement,
467 pub removed: u64,
468 pub group_commit_index: u64,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472pub struct PurgeBucketResponse {
473 pub placement: ShardPlacement,
474 pub removed_streams: u64,
475 pub group_commit_index: u64,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct FlushColdRequest {
480 pub stream_id: BucketStreamId,
481 pub chunk: ColdChunkRef,
482}
483
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
485pub struct FlushColdResponse {
486 pub placement: ShardPlacement,
487 pub hot_start_offset: u64,
488 pub group_commit_index: u64,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub struct CompactColdRequest {
493 pub stream_id: BucketStreamId,
494 pub old_chunks: Vec<ColdChunkRef>,
495 pub replacement: ColdChunkRef,
496 pub gc_not_before_ms: u64,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500pub struct CompactColdResponse {
501 pub placement: ShardPlacement,
502 pub compacted_chunks: u64,
503 pub compacted_bytes: u64,
504 pub group_commit_index: u64,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508pub struct TouchStreamAccessResponse {
509 pub placement: ShardPlacement,
510 pub changed: bool,
511 pub expired: bool,
512 pub group_commit_index: u64,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
516pub struct PlanColdFlushRequest {
517 pub stream_id: BucketStreamId,
518 pub min_hot_bytes: usize,
519 pub max_flush_bytes: usize,
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
523pub struct PlanGroupColdFlushRequest {
524 pub min_hot_bytes: usize,
525 pub max_flush_bytes: usize,
526 pub max_batch_bytes: usize,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq)]
531pub struct ColdHotBacklog {
532 pub stream_id: BucketStreamId,
533 pub stream_hot_bytes: u64,
534 pub group_hot_bytes: u64,
535}
536
537#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
538pub struct ColdWriteAdmission {
539 pub max_hot_bytes_per_group: Option<u64>,
540}
541
542impl ColdWriteAdmission {
543 pub(crate) fn is_enabled(self) -> bool {
544 self.max_hot_bytes_per_group.is_some()
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Eq)]
549pub struct AppendRequest {
550 pub stream_id: BucketStreamId,
551 pub content_type: String,
552 pub payload: Bytes,
553 pub close_after: bool,
554 pub stream_seq: Option<String>,
555 pub producer: Option<ProducerRequest>,
556 pub now_ms: u64,
557 pub record_match: Option<u64>,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub struct AppendExternalRequest {
562 pub stream_id: BucketStreamId,
563 pub content_type: String,
564 pub payload: ExternalPayloadRef,
565 #[serde(default)]
566 pub record_ends: Vec<u64>,
567 pub close_after: bool,
568 pub stream_seq: Option<String>,
569 pub producer: Option<ProducerRequest>,
570 pub now_ms: u64,
571 pub record_match: Option<u64>,
572}
573
574impl AppendExternalRequest {
575 pub fn from_append_request(
576 request: AppendRequest,
577 payload: ExternalPayloadRef,
578 record_ends: Vec<u64>,
579 ) -> Self {
580 Self {
581 stream_id: request.stream_id,
582 content_type: request.content_type,
583 payload,
584 record_ends,
585 close_after: request.close_after,
586 stream_seq: request.stream_seq,
587 producer: request.producer,
588 now_ms: request.now_ms,
589 record_match: request.record_match,
590 }
591 }
592}
593
594impl AppendRequest {
595 pub fn canonical_record_ends(&self) -> Vec<u64> {
596 ursula_stream::canonical_json_record_ends(&self.content_type, &self.payload)
597 .unwrap_or_default()
598 }
599
600 pub fn new(stream_id: BucketStreamId, payload_len: u64) -> Self {
601 Self {
602 stream_id,
603 content_type: DEFAULT_CONTENT_TYPE.to_owned(),
604 payload: Bytes::from(vec![
605 0;
606 usize::try_from(payload_len)
607 .expect("payload_len fits usize")
608 ]),
609 close_after: false,
610 stream_seq: None,
611 producer: None,
612 now_ms: 0,
613 record_match: None,
614 }
615 }
616
617 pub fn from_bytes(stream_id: BucketStreamId, payload: impl Into<Bytes>) -> Self {
618 Self {
619 stream_id,
620 content_type: DEFAULT_CONTENT_TYPE.to_owned(),
621 payload: payload.into(),
622 close_after: false,
623 stream_seq: None,
624 producer: None,
625 now_ms: 0,
626 record_match: None,
627 }
628 }
629
630 pub fn payload_len(&self) -> u64 {
631 u64::try_from(self.payload.len()).expect("payload len fits u64")
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct AppendBatchRequest {
637 pub stream_id: BucketStreamId,
638 pub content_type: String,
639 pub payloads: Vec<Bytes>,
640 pub producer: Option<ProducerRequest>,
641 pub now_ms: u64,
642}
643
644impl AppendBatchRequest {
645 pub fn new<P>(stream_id: BucketStreamId, payloads: Vec<P>) -> Self
646 where P: Into<Bytes> {
647 Self {
648 stream_id,
649 content_type: DEFAULT_CONTENT_TYPE.to_owned(),
650 payloads: payloads.into_iter().map(Into::into).collect(),
651 producer: None,
652 now_ms: 0,
653 }
654 }
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658pub struct AppendResponse {
659 pub placement: ShardPlacement,
660 pub start_offset: u64,
661 pub next_offset: u64,
662 pub stream_append_count: u64,
663 pub group_commit_index: u64,
664 pub closed: bool,
665 pub deduplicated: bool,
666 pub producer: Option<ProducerRequest>,
667 pub record_range: Option<StreamRecordRange>,
668 #[serde(default)]
669 pub stream_hot_bytes: u64,
670 #[serde(default)]
671 pub group_hot_bytes: u64,
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
675pub struct AppendBatchResponse {
676 pub placement: ShardPlacement,
677 pub items: Vec<Result<AppendResponse, RuntimeError>>,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681pub struct StreamAppendCount {
682 pub stream_id: BucketStreamId,
683 pub append_count: u64,
684}