Skip to main content

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}