ozzy_journal/operation/schema.rs
1//! Canonical identities, operation bodies, and admission metadata.
2
3use super::{AppendRecordList, AppendResults, OperationCodecError};
4use ozzy_proto::{
5 ConsumerGroupId, ConsumerMemberId, GroupId, MessageId, Offset, OperationId, OwnerEpoch,
6 PartitionId, PartitionIncarnation, ProducerEpoch, ProducerId, ProducerSequence, SubscriptionId,
7};
8use smallvec::SmallVec;
9use std::num::NonZeroU64;
10
11/// A fixed-width digest or externally supplied identity fingerprint.
12/// Computed integrity digests contain XXH3-128 followed by 16 zero bytes;
13/// see [`crate::integrity`]. External principal fingerprints remain opaque.
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
15pub struct Digest([u8; 32]);
16
17impl Digest {
18 /// Digest used by the configured genesis operation prefix.
19 pub const ZERO: Self = Self([0; 32]);
20
21 /// Construct a digest from its canonical bytes.
22 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
23 Self(bytes)
24 }
25
26 /// Return canonical digest bytes.
27 pub const fn as_bytes(&self) -> &[u8; 32] {
28 &self.0
29 }
30}
31
32/// One operation kind in the canonical journal schema.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u16)]
35pub enum OperationKind {
36 /// Create an incarnation and bind its logical address and owner fence.
37 CreatePartition = 1,
38 /// Open or fence one partition-local producer session.
39 OpenProducer = 2,
40 /// Assign consecutive records to producer sequences and partition offsets.
41 Append = 3,
42 /// Declare exclusive individual or consumer-group progress.
43 Progress = 4,
44 /// Install or revoke a fenced consumer-group partition assignment.
45 Assign = 5,
46 /// Advance the partition retention floor.
47 Trim = 6,
48 /// Install a newer retention-policy revision.
49 PartitionPolicy = 7,
50 /// Record a control-operation identity without changing record state.
51 Barrier = 8,
52 /// Advance the retained producer retry-result floor.
53 ProducerResultFloor = 9,
54 /// Resolve producer attempts independently of newly stored records.
55 ResolvedAppend = 10,
56 /// Advance replicated logical time while no unique records are arriving.
57 DedupWatermark = 11,
58 /// Persist coordinator membership and a source ownership plan.
59 GroupPlan = 12,
60 /// Retire one coordinator group incarnation and free its name.
61 GroupDelete = 13,
62}
63
64impl TryFrom<u16> for OperationKind {
65 type Error = OperationCodecError;
66
67 fn try_from(value: u16) -> Result<Self, Self::Error> {
68 match value {
69 1 => Ok(Self::CreatePartition),
70 2 => Ok(Self::OpenProducer),
71 3 => Ok(Self::Append),
72 4 => Ok(Self::Progress),
73 5 => Ok(Self::Assign),
74 6 => Ok(Self::Trim),
75 7 => Ok(Self::PartitionPolicy),
76 8 => Ok(Self::Barrier),
77 9 => Ok(Self::ProducerResultFloor),
78 10 => Ok(Self::ResolvedAppend),
79 11 => Ok(Self::DedupWatermark),
80 12 => Ok(Self::GroupPlan),
81 13 => Ok(Self::GroupDelete),
82 other => Err(OperationCodecError::UnsupportedOperationKind(other)),
83 }
84 }
85}
86
87impl OperationKind {
88 /// Whether every valid body of this kind returns an explicit control
89 /// identity from `OperationBody::operation_id`. Admission uses it to skip
90 /// decoding APPEND-like bodies that canonical validation decodes anyway,
91 /// and reserves identity capacity only for these kinds.
92 pub const fn has_operation_id(self) -> bool {
93 !matches!(
94 self,
95 Self::CreatePartition
96 | Self::Append
97 | Self::Progress
98 | Self::ResolvedAppend
99 | Self::DedupWatermark
100 )
101 }
102}
103
104/// Expected logical chain position before an operation.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct ChainPosition {
107 next_op_number: u64,
108 previous_digest: Digest,
109}
110
111impl ChainPosition {
112 /// Configured genesis position. Journal operations begin at one.
113 pub const GENESIS: Self = Self {
114 next_op_number: 1,
115 previous_digest: Digest::ZERO,
116 };
117
118 /// Bind the next canonical operation number to its predecessor digest.
119 pub const fn new(next_op_number: u64, previous_digest: Digest) -> Self {
120 Self {
121 next_op_number,
122 previous_digest,
123 }
124 }
125
126 /// Next expected canonical operation number.
127 pub const fn next_op_number(self) -> u64 {
128 self.next_op_number
129 }
130
131 /// Canonical chain digest immediately before the next operation.
132 pub const fn previous_digest(self) -> Digest {
133 self.previous_digest
134 }
135}
136
137/// One canonical operation ready for wire or physical-disk framing.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct CanonicalOperation<'a> {
140 /// Persistent partition replication-group identity.
141 pub group_id: GroupId,
142 /// Membership/configuration fence at admission.
143 pub configuration_epoch: u64,
144 /// Original admission view, preserved through replication and repair.
145 pub original_view: u64,
146 /// Partition-local canonical operation number.
147 pub op_number: u64,
148 /// Canonical chain digest immediately before this operation.
149 pub previous_digest: Digest,
150 /// Canonical operation discriminator.
151 pub kind: OperationKind,
152 /// Exact canonical body bytes covered by logical integrity.
153 pub body: &'a [u8],
154}
155
156/// Logical identity without any assumption about body storage or contiguity.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct OperationHeader {
159 /// Persistent partition replication-group identity.
160 pub group_id: GroupId,
161 /// Membership/configuration fence at admission.
162 pub configuration_epoch: u64,
163 /// Original admission view, preserved through replication and repair.
164 pub original_view: u64,
165 /// Partition-local canonical operation number.
166 pub op_number: u64,
167 /// Canonical chain digest immediately before this operation.
168 pub previous_digest: Digest,
169 /// Canonical operation discriminator.
170 pub kind: OperationKind,
171}
172
173impl CanonicalOperation<'_> {
174 /// Borrow-independent canonical metadata for this exact operation.
175 pub const fn header(&self) -> OperationHeader {
176 OperationHeader {
177 group_id: self.group_id,
178 configuration_epoch: self.configuration_epoch,
179 original_view: self.original_view,
180 op_number: self.op_number,
181 previous_digest: self.previous_digest,
182 kind: self.kind,
183 }
184 }
185}
186
187/// Independent age/size retention targets. Neither target means keep forever.
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
189pub struct RetentionPolicy {
190 /// Optional maximum retained record age in milliseconds.
191 pub max_age_millis: Option<NonZeroU64>,
192 /// Optional target for selected segment capacities, including active space.
193 pub max_bytes: Option<NonZeroU64>,
194}
195
196/// Partition creation. Policy revision starts at one implicitly.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct CreatePartition<'a> {
199 /// Exact partition record incarnation.
200 pub partition: PartitionIncarnation,
201 /// Logical stream namespace.
202 pub stream: &'a str,
203 /// Logical topic name.
204 pub topic: &'a str,
205 /// Partition number within its topic.
206 pub partition_id: PartitionId,
207 /// Expected partition ownership fence.
208 pub owner_epoch: OwnerEpoch,
209 /// Immutable first-wins deduplication window in replicated log milliseconds.
210 /// Absent disables event deduplication for this partition incarnation.
211 pub dedup_window_millis: Option<NonZeroU64>,
212 /// Retention policy stored in canonical state.
213 pub retention: RetentionPolicy,
214}
215
216/// Idempotent producer-session open or fencing transition.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct OpenProducer {
219 /// Exact partition record incarnation.
220 pub partition: PartitionIncarnation,
221 /// Producer identity scoped to this partition.
222 pub producer_id: ProducerId,
223 /// Expected existing session epoch; none requires no session.
224 pub expected_epoch: Option<ProducerEpoch>,
225 /// New producer fence, strictly newer than an existing session.
226 pub new_epoch: ProducerEpoch,
227 /// Exact control-operation retry identity.
228 pub operation_id: OperationId,
229}
230
231/// One record within a canonical append sub-batch.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct AppendRecord<'a> {
234 /// Payload representation, preserved through persistence and replay.
235 pub encoding: ozzy_proto::data::Encoding,
236 /// Application record identity preserved through retry and replay.
237 pub message_id: MessageId,
238 /// Opaque payload parts in record order.
239 pub parts: SmallVec<[&'a [u8]; 2]>,
240}
241
242/// One partition-contiguous append within an atomic group operation.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct AppendBatch<'a> {
245 /// Exact partition record incarnation.
246 pub partition: PartitionIncarnation,
247 /// Expected partition ownership fence.
248 pub owner_epoch: OwnerEpoch,
249 /// Producer identity scoped to this partition.
250 pub producer_id: ProducerId,
251 /// Expected producer-session fence.
252 pub producer_epoch: ProducerEpoch,
253 /// First contiguous producer-local sequence.
254 pub first_sequence: ProducerSequence,
255 /// First assigned partition-global record offset.
256 pub first_offset: Offset,
257 /// Primary-resolved Unix timestamp in milliseconds.
258 pub append_timestamp_millis: u64,
259 /// Consecutive records in this partition batch.
260 pub records: AppendRecordList<'a>,
261}
262
263/// One journal append operation containing independently addressed partitions.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct Append<'a> {
266 /// Independently addressed batches in canonical operation order.
267 pub batches: Vec<AppendBatch<'a>>,
268}
269
270/// One partition's stored records and outcomes of every producer attempt.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct ResolvedAppend<'a> {
273 /// New records in consecutive partition-offset order; may be empty.
274 pub batch: AppendBatch<'a>,
275 /// Attempt order, logical time, and exact discarded-payload fingerprints.
276 pub results: AppendResults<'a>,
277}
278
279/// Metadata needed to validate one append's state transition, without records.
280/// This is not a hash proof or authorization to skip canonical body validation.
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct AppendBatchSummary {
283 /// Destination partition incarnation.
284 pub partition: PartitionIncarnation,
285 /// Expected partition ownership epoch.
286 pub owner_epoch: OwnerEpoch,
287 /// Producer bound to this partition.
288 pub producer_id: ProducerId,
289 /// Expected producer session epoch.
290 pub producer_epoch: ProducerEpoch,
291 /// First contiguous producer sequence in this batch.
292 pub first_sequence: ProducerSequence,
293 /// First assigned partition offset in this batch.
294 pub first_offset: Offset,
295 /// Stored record count checked against schema bounds. A resolved append
296 /// containing only duplicate attempts has zero stored records.
297 pub record_count: usize,
298 /// Whether every descriptor contains a nonzero message identity.
299 /// State admission must reject false; syntax validation alone permits it.
300 pub nonzero_message_ids: bool,
301}
302
303/// One record's byte position inside its batch, recorded while the schema
304/// walker validates descriptors. Offsets are relative to the batch's
305/// descriptor table and payload start; the final entry per batch is an end
306/// checkpoint with zero parts.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub struct RecordPosition {
309 /// Byte offset relative to the batch descriptor table.
310 pub descriptor: u32,
311 /// Byte offset relative to the batch payload start.
312 pub payload: u32,
313 /// Part count; zero marks the batch end checkpoint.
314 pub parts: u32,
315}
316
317/// Schema-validated append metadata. Up to four partitions stay on the stack.
318/// Record descriptors and all part lengths use the ordinary schema walker;
319/// application state must still check epochs, positions, and nonzero IDs.
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct AppendSummary {
322 pub(super) batches: SmallVec<[AppendBatchSummary; 4]>,
323}
324
325impl AppendSummary {
326 /// Construct a one-batch summary from metadata carried by a validated wire APPEND.
327 pub fn single(batch: AppendBatchSummary) -> Self {
328 Self {
329 batches: smallvec::smallvec![batch],
330 }
331 }
332
333 /// Partition batches in canonical order, without materialized record tables.
334 pub fn batches(&self) -> &[AppendBatchSummary] {
335 &self.batches
336 }
337}
338
339/// Stable owner of one processing-progress cursor.
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum ProgressOwner {
342 /// Progress declared by one standalone subscription.
343 Subscription(SubscriptionId),
344 /// Progress declared by a fenced consumer-group assignment.
345 ConsumerGroup(ConsumerGroupId),
346}
347
348/// Source-partition ownership required to declare consumer-group progress.
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct ProgressAssignment {
351 /// Exact nonzero source-partition assignment epoch.
352 pub epoch: u64,
353 /// Member whose confirmed assignment permits processing progress.
354 pub member: ConsumerMemberId,
355}
356
357/// Durable contiguous consumer-processing progress transition.
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub struct Progress {
360 /// Standalone subscription or fenced consumer group declaring progress.
361 pub owner: ProgressOwner,
362 /// Exact partition record incarnation.
363 pub partition: PartitionIncarnation,
364 /// Member and assignment fence required for consumer-group progress.
365 pub assignment: Option<ProgressAssignment>,
366 /// Expected prior exclusive offset; none requires no progress entry.
367 pub expected_progress: Option<Offset>,
368 /// New exclusive progress offset within confirmed record state.
369 pub new_progress: Offset,
370 /// Exact control-operation retry identity.
371 pub operation_id: OperationId,
372}
373
374/// Consumer-group partition assignment transition.
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub struct Assign {
377 /// Consumer group whose assignment changes.
378 pub consumer_group_id: ConsumerGroupId,
379 /// Exact partition record incarnation.
380 pub partition: PartitionIncarnation,
381 /// Expected current assignment fence.
382 pub expected_assignment_epoch: u64,
383 /// New assignment fence, strictly newer than the current one.
384 pub new_assignment_epoch: u64,
385 /// Committed coordinator plan that authorizes this transition.
386 pub plan_revision: u64,
387 /// Initial group cursor, or the exact existing progress captured at handoff.
388 pub resume_cursor: Offset,
389 /// New assigned member; none revokes ownership.
390 pub new_member: Option<ConsumerMemberId>,
391 /// Exact control-operation retry identity.
392 pub operation_id: OperationId,
393}
394
395/// Advance one partition's committed earliest-retained offset.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub struct Trim {
398 /// Exact partition record incarnation.
399 pub partition: PartitionIncarnation,
400 /// Expected current retention floor.
401 pub expected_floor: Offset,
402 /// New monotonic retention floor.
403 pub new_floor: Offset,
404 /// Exact control-operation retry identity.
405 pub operation_id: OperationId,
406}
407
408/// Replace one partition's retention policy by revision.
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct PartitionPolicy {
411 /// Exact partition record incarnation.
412 pub partition: PartitionIncarnation,
413 /// Expected current retention-policy revision.
414 pub expected_revision: u64,
415 /// New retention-policy revision.
416 pub new_revision: u64,
417 /// Retention policy stored in canonical state.
418 pub retention: RetentionPolicy,
419 /// Exact control-operation retry identity.
420 pub operation_id: OperationId,
421}
422
423/// Ordered no-op used to establish an authority or read boundary.
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub struct Barrier {
426 /// Exact control-operation retry identity.
427 pub operation_id: OperationId,
428}
429
430/// Advance the exclusive floor below which producer retry results expired.
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub struct ProducerResultFloor {
433 /// Exact partition record incarnation.
434 pub partition: PartitionIncarnation,
435 /// Producer identity scoped to this partition.
436 pub producer_id: ProducerId,
437 /// Expected producer-session fence.
438 pub producer_epoch: ProducerEpoch,
439 /// Expected retained producer retry-result floor.
440 pub expected_floor: ProducerSequence,
441 /// New monotonic producer retry-result floor.
442 pub new_floor: ProducerSequence,
443 /// Exact control-operation retry identity.
444 pub operation_id: OperationId,
445}
446
447/// Primary-resolved monotonic deduplication time, independent of wall clocks.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct DedupWatermark {
450 /// Exact partition record incarnation.
451 pub partition: PartitionIncarnation,
452 /// Expected partition ownership fence.
453 pub owner_epoch: OwnerEpoch,
454 /// Replicated logical time in milliseconds, strictly newer than current time.
455 pub logical_time: u64,
456}
457
458/// One typed canonical operation body.
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub enum OperationBody<'a> {
461 /// Create an incarnation and bind its logical address and owner fence.
462 CreatePartition(CreatePartition<'a>),
463 /// Open or fence one partition-local producer session.
464 OpenProducer(OpenProducer),
465 /// Assign consecutive records to producer sequences and partition offsets.
466 Append(Append<'a>),
467 /// Resolve producer attempts independently of newly stored records.
468 ResolvedAppend(ResolvedAppend<'a>),
469 /// Declare exclusive individual or consumer-group progress.
470 Progress(Progress),
471 /// Install or revoke a fenced consumer-group partition assignment.
472 Assign(Assign),
473 /// Advance the partition retention floor.
474 Trim(Trim),
475 /// Install a newer retention-policy revision.
476 PartitionPolicy(PartitionPolicy),
477 /// Record a control-operation identity without changing record state.
478 Barrier(Barrier),
479 /// Advance the retained producer retry-result floor.
480 ProducerResultFloor(ProducerResultFloor),
481 /// Advance replicated logical time without changing record coordinates.
482 DedupWatermark(DedupWatermark),
483 /// Persist coordinator membership before changing source ownership.
484 GroupPlan(super::GroupPlan<'a>),
485 /// Retire one coordinator group incarnation and free its name.
486 GroupDelete(super::GroupDelete),
487}
488
489impl OperationBody<'_> {
490 /// Explicit retry identity for control operations. APPEND identity belongs
491 /// to its producer sequences; partition creation uses its incarnation.
492 /// Progress retries resolve by their cumulative cursor, so its request ID
493 /// is not an identity.
494 pub const fn operation_id(&self) -> Option<OperationId> {
495 match self {
496 Self::CreatePartition(_)
497 | Self::Append(_)
498 | Self::Progress(_)
499 | Self::ResolvedAppend(_)
500 | Self::DedupWatermark(_) => None,
501 Self::OpenProducer(value) => Some(value.operation_id),
502 Self::Assign(value) => Some(value.operation_id),
503 Self::Trim(value) => Some(value.operation_id),
504 Self::PartitionPolicy(value) => Some(value.operation_id),
505 Self::Barrier(value) => Some(value.operation_id),
506 Self::ProducerResultFloor(value) => Some(value.operation_id),
507 Self::GroupPlan(value) => Some(value.operation_id),
508 Self::GroupDelete(value) => Some(value.operation_id),
509 }
510 }
511
512 /// Return the canonical operation discriminator.
513 pub const fn kind(&self) -> OperationKind {
514 match self {
515 Self::CreatePartition(_) => OperationKind::CreatePartition,
516 Self::OpenProducer(_) => OperationKind::OpenProducer,
517 Self::Append(_) => OperationKind::Append,
518 Self::ResolvedAppend(_) => OperationKind::ResolvedAppend,
519 Self::Progress(_) => OperationKind::Progress,
520 Self::Assign(_) => OperationKind::Assign,
521 Self::Trim(_) => OperationKind::Trim,
522 Self::PartitionPolicy(_) => OperationKind::PartitionPolicy,
523 Self::Barrier(_) => OperationKind::Barrier,
524 Self::ProducerResultFloor(_) => OperationKind::ProducerResultFloor,
525 Self::DedupWatermark(_) => OperationKind::DedupWatermark,
526 Self::GroupPlan(_) => OperationKind::GroupPlan,
527 Self::GroupDelete(_) => OperationKind::GroupDelete,
528 }
529 }
530}
531
532/// Resource bounds applied to canonical operation bodies.
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub struct OperationLimits {
535 /// Maximum encoded canonical operation-body bytes.
536 pub max_body_bytes: usize,
537 /// Maximum UTF-8 bytes per stream or topic name.
538 pub max_name_bytes: usize,
539 /// Maximum independently addressed batches per APPEND.
540 pub max_append_batches: usize,
541 /// Maximum combined records per APPEND.
542 pub max_records: usize,
543 /// Maximum combined payload parts per APPEND.
544 pub max_parts: usize,
545 /// Maximum combined APPEND payload bytes.
546 pub max_payload_bytes: usize,
547 /// Maximum members in one coordinator intent.
548 pub max_group_members: usize,
549 /// Maximum source partitions in one coordinator intent.
550 pub max_group_partitions: usize,
551}
552
553impl Default for OperationLimits {
554 fn default() -> Self {
555 Self {
556 max_body_bytes: 64 * 1024 * 1024,
557 max_name_bytes: 255,
558 max_append_batches: 1_024,
559 max_records: 1_024,
560 max_parts: 65_536,
561 max_payload_bytes: 64 * 1024 * 1024,
562 max_group_members: 256,
563 max_group_partitions: 1024,
564 }
565 }
566}