Skip to main content

ozzy_journal/operation/
schema.rs

1//! Canonical identities, operation bodies, and admission metadata.
2
3use super::{AppendRecordList, 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}
55
56impl TryFrom<u16> for OperationKind {
57    type Error = OperationCodecError;
58
59    fn try_from(value: u16) -> Result<Self, Self::Error> {
60        match value {
61            1 => Ok(Self::CreatePartition),
62            2 => Ok(Self::OpenProducer),
63            3 => Ok(Self::Append),
64            4 => Ok(Self::Progress),
65            5 => Ok(Self::Assign),
66            6 => Ok(Self::Trim),
67            7 => Ok(Self::PartitionPolicy),
68            8 => Ok(Self::Barrier),
69            9 => Ok(Self::ProducerResultFloor),
70            other => Err(OperationCodecError::UnsupportedOperationKind(other)),
71        }
72    }
73}
74
75/// Expected logical chain position before an operation.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct ChainPosition {
78    next_op_number: u64,
79    previous_digest: Digest,
80}
81
82impl ChainPosition {
83    /// Configured genesis position. Journal operations begin at one.
84    pub const GENESIS: Self = Self {
85        next_op_number: 1,
86        previous_digest: Digest::ZERO,
87    };
88
89    /// Bind the next canonical operation number to its predecessor digest.
90    pub const fn new(next_op_number: u64, previous_digest: Digest) -> Self {
91        Self {
92            next_op_number,
93            previous_digest,
94        }
95    }
96
97    /// Next expected canonical operation number.
98    pub const fn next_op_number(self) -> u64 {
99        self.next_op_number
100    }
101
102    /// Canonical chain digest immediately before the next operation.
103    pub const fn previous_digest(self) -> Digest {
104        self.previous_digest
105    }
106}
107
108/// One canonical operation ready for wire or physical-disk framing.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct CanonicalOperation<'a> {
111    /// Persistent partition replication-group identity.
112    pub group_id: GroupId,
113    /// Membership/configuration fence at admission.
114    pub configuration_epoch: u64,
115    /// Original admission view, preserved through replication and repair.
116    pub original_view: u64,
117    /// Partition-local canonical operation number.
118    pub op_number: u64,
119    /// Canonical chain digest immediately before this operation.
120    pub previous_digest: Digest,
121    /// Canonical operation discriminator.
122    pub kind: OperationKind,
123    /// Exact canonical body bytes covered by logical integrity.
124    pub body: &'a [u8],
125}
126
127/// Logical identity without any assumption about body storage or contiguity.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct OperationHeader {
130    /// Persistent partition replication-group identity.
131    pub group_id: GroupId,
132    /// Membership/configuration fence at admission.
133    pub configuration_epoch: u64,
134    /// Original admission view, preserved through replication and repair.
135    pub original_view: u64,
136    /// Partition-local canonical operation number.
137    pub op_number: u64,
138    /// Canonical chain digest immediately before this operation.
139    pub previous_digest: Digest,
140    /// Canonical operation discriminator.
141    pub kind: OperationKind,
142}
143
144impl CanonicalOperation<'_> {
145    /// Borrow-independent canonical metadata for this exact operation.
146    pub const fn header(&self) -> OperationHeader {
147        OperationHeader {
148            group_id: self.group_id,
149            configuration_epoch: self.configuration_epoch,
150            original_view: self.original_view,
151            op_number: self.op_number,
152            previous_digest: self.previous_digest,
153            kind: self.kind,
154        }
155    }
156}
157
158/// Independent age/size retention targets. Neither target means keep forever.
159#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
160pub struct RetentionPolicy {
161    /// Optional maximum retained record age in milliseconds.
162    pub max_age_millis: Option<NonZeroU64>,
163    /// Optional target for selected segment capacities, including active space.
164    pub max_bytes: Option<NonZeroU64>,
165}
166
167/// Partition creation. Policy revision starts at one implicitly.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct CreatePartition<'a> {
170    /// Exact partition record incarnation.
171    pub partition: PartitionIncarnation,
172    /// Logical stream namespace.
173    pub stream: &'a str,
174    /// Logical topic name.
175    pub topic: &'a str,
176    /// Partition number within its topic.
177    pub partition_id: PartitionId,
178    /// Expected partition ownership fence.
179    pub owner_epoch: OwnerEpoch,
180    /// Retention policy stored in canonical state.
181    pub retention: RetentionPolicy,
182}
183
184/// Idempotent producer-session open or fencing transition.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct OpenProducer {
187    /// Exact partition record incarnation.
188    pub partition: PartitionIncarnation,
189    /// Producer identity scoped to this partition.
190    pub producer_id: ProducerId,
191    /// Expected existing session epoch; none requires no session.
192    pub expected_epoch: Option<ProducerEpoch>,
193    /// New producer fence, strictly newer than an existing session.
194    pub new_epoch: ProducerEpoch,
195    /// Exact control-operation retry identity.
196    pub operation_id: OperationId,
197}
198
199/// One record within a canonical append sub-batch.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct AppendRecord<'a> {
202    /// Payload representation, preserved through persistence and replay.
203    pub encoding: ozzy_proto::data::Encoding,
204    /// Application record identity preserved through retry and replay.
205    pub message_id: MessageId,
206    /// Opaque payload parts in record order.
207    pub parts: SmallVec<[&'a [u8]; 2]>,
208}
209
210/// One partition-contiguous append within an atomic group operation.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct AppendBatch<'a> {
213    /// Exact partition record incarnation.
214    pub partition: PartitionIncarnation,
215    /// Expected partition ownership fence.
216    pub owner_epoch: OwnerEpoch,
217    /// Producer identity scoped to this partition.
218    pub producer_id: ProducerId,
219    /// Expected producer-session fence.
220    pub producer_epoch: ProducerEpoch,
221    /// First contiguous producer-local sequence.
222    pub first_sequence: ProducerSequence,
223    /// First assigned partition-global record offset.
224    pub first_offset: Offset,
225    /// Primary-resolved Unix timestamp in milliseconds.
226    pub append_timestamp_millis: u64,
227    /// Consecutive records in this partition batch.
228    pub records: AppendRecordList<'a>,
229}
230
231/// One journal append operation containing independently addressed partitions.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct Append<'a> {
234    /// Independently addressed batches in canonical operation order.
235    pub batches: Vec<AppendBatch<'a>>,
236}
237
238/// Metadata needed to validate one append's state transition, without records.
239/// This is not a hash proof or authorization to skip canonical body validation.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct AppendBatchSummary {
242    /// Destination partition incarnation.
243    pub partition: PartitionIncarnation,
244    /// Expected partition ownership epoch.
245    pub owner_epoch: OwnerEpoch,
246    /// Producer bound to this partition.
247    pub producer_id: ProducerId,
248    /// Expected producer session epoch.
249    pub producer_epoch: ProducerEpoch,
250    /// First contiguous producer sequence in this batch.
251    pub first_sequence: ProducerSequence,
252    /// First assigned partition offset in this batch.
253    pub first_offset: Offset,
254    /// Nonzero record count checked against schema bounds.
255    pub record_count: usize,
256    /// Whether every descriptor contains a nonzero message identity.
257    /// State admission must reject false; syntax validation alone permits it.
258    pub nonzero_message_ids: bool,
259}
260
261/// One record's byte position inside its batch, recorded while the schema
262/// walker validates descriptors. Offsets are relative to the batch's
263/// descriptor table and payload start; the final entry per batch is an end
264/// checkpoint with zero parts.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct RecordPosition {
267    /// Byte offset relative to the batch descriptor table.
268    pub descriptor: u32,
269    /// Byte offset relative to the batch payload start.
270    pub payload: u32,
271    /// Part count; zero marks the batch end checkpoint.
272    pub parts: u32,
273}
274
275/// Schema-validated append metadata. Up to four partitions stay on the stack.
276/// Record descriptors and all part lengths use the ordinary schema walker;
277/// application state must still check epochs, positions, and nonzero IDs.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct AppendSummary {
280    pub(super) batches: SmallVec<[AppendBatchSummary; 4]>,
281}
282
283impl AppendSummary {
284    /// Construct a one-batch summary from metadata carried by a validated wire APPEND.
285    pub fn single(batch: AppendBatchSummary) -> Self {
286        Self {
287            batches: smallvec::smallvec![batch],
288        }
289    }
290
291    /// Partition batches in canonical order, without materialized record tables.
292    pub fn batches(&self) -> &[AppendBatchSummary] {
293        &self.batches
294    }
295}
296
297/// Stable owner of one processing-progress cursor.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum ProgressOwner {
300    /// Progress declared by one standalone subscription.
301    Subscription(SubscriptionId),
302    /// Progress declared by a fenced consumer-group assignment.
303    ConsumerGroup(ConsumerGroupId),
304}
305
306/// Durable contiguous consumer-processing progress transition.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub struct Progress {
309    /// Standalone subscription or fenced consumer group declaring progress.
310    pub owner: ProgressOwner,
311    /// Exact partition record incarnation.
312    pub partition: PartitionIncarnation,
313    /// Assignment fence required for consumer-group progress.
314    pub assignment_epoch: Option<u64>,
315    /// Expected prior exclusive offset; none requires no progress entry.
316    pub expected_progress: Option<Offset>,
317    /// New exclusive progress offset within confirmed record state.
318    pub new_progress: Offset,
319    /// Exact control-operation retry identity.
320    pub operation_id: OperationId,
321}
322
323/// Consumer-group partition assignment transition.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub struct Assign {
326    /// Consumer group whose assignment changes.
327    pub consumer_group_id: ConsumerGroupId,
328    /// Exact partition record incarnation.
329    pub partition: PartitionIncarnation,
330    /// Expected current assignment fence.
331    pub expected_assignment_epoch: u64,
332    /// New assignment fence, strictly newer than the current one.
333    pub new_assignment_epoch: u64,
334    /// New assigned member; none revokes ownership.
335    pub new_member: Option<ConsumerMemberId>,
336    /// Exact control-operation retry identity.
337    pub operation_id: OperationId,
338}
339
340/// Advance one partition's committed earliest-retained offset.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub struct Trim {
343    /// Exact partition record incarnation.
344    pub partition: PartitionIncarnation,
345    /// Expected current retention floor.
346    pub expected_floor: Offset,
347    /// New monotonic retention floor.
348    pub new_floor: Offset,
349    /// Exact control-operation retry identity.
350    pub operation_id: OperationId,
351}
352
353/// Replace one partition's retention policy by revision.
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub struct PartitionPolicy {
356    /// Exact partition record incarnation.
357    pub partition: PartitionIncarnation,
358    /// Expected current retention-policy revision.
359    pub expected_revision: u64,
360    /// New retention-policy revision.
361    pub new_revision: u64,
362    /// Retention policy stored in canonical state.
363    pub retention: RetentionPolicy,
364    /// Exact control-operation retry identity.
365    pub operation_id: OperationId,
366}
367
368/// Ordered no-op used to establish an authority or read boundary.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub struct Barrier {
371    /// Exact control-operation retry identity.
372    pub operation_id: OperationId,
373}
374
375/// Advance the exclusive floor below which producer retry results expired.
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub struct ProducerResultFloor {
378    /// Exact partition record incarnation.
379    pub partition: PartitionIncarnation,
380    /// Producer identity scoped to this partition.
381    pub producer_id: ProducerId,
382    /// Expected producer-session fence.
383    pub producer_epoch: ProducerEpoch,
384    /// Expected retained producer retry-result floor.
385    pub expected_floor: ProducerSequence,
386    /// New monotonic producer retry-result floor.
387    pub new_floor: ProducerSequence,
388    /// Exact control-operation retry identity.
389    pub operation_id: OperationId,
390}
391
392/// One typed canonical operation body.
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub enum OperationBody<'a> {
395    /// Create an incarnation and bind its logical address and owner fence.
396    CreatePartition(CreatePartition<'a>),
397    /// Open or fence one partition-local producer session.
398    OpenProducer(OpenProducer),
399    /// Assign consecutive records to producer sequences and partition offsets.
400    Append(Append<'a>),
401    /// Declare exclusive individual or consumer-group progress.
402    Progress(Progress),
403    /// Install or revoke a fenced consumer-group partition assignment.
404    Assign(Assign),
405    /// Advance the partition retention floor.
406    Trim(Trim),
407    /// Install a newer retention-policy revision.
408    PartitionPolicy(PartitionPolicy),
409    /// Record a control-operation identity without changing record state.
410    Barrier(Barrier),
411    /// Advance the retained producer retry-result floor.
412    ProducerResultFloor(ProducerResultFloor),
413}
414
415impl OperationBody<'_> {
416    /// Explicit retry identity for control operations. APPEND identity belongs
417    /// to its producer sequences; partition creation uses its incarnation.
418    pub const fn operation_id(&self) -> Option<OperationId> {
419        match self {
420            Self::CreatePartition(_) | Self::Append(_) => None,
421            Self::OpenProducer(value) => Some(value.operation_id),
422            Self::Progress(value) => Some(value.operation_id),
423            Self::Assign(value) => Some(value.operation_id),
424            Self::Trim(value) => Some(value.operation_id),
425            Self::PartitionPolicy(value) => Some(value.operation_id),
426            Self::Barrier(value) => Some(value.operation_id),
427            Self::ProducerResultFloor(value) => Some(value.operation_id),
428        }
429    }
430
431    /// Return the canonical operation discriminator.
432    pub const fn kind(&self) -> OperationKind {
433        match self {
434            Self::CreatePartition(_) => OperationKind::CreatePartition,
435            Self::OpenProducer(_) => OperationKind::OpenProducer,
436            Self::Append(_) => OperationKind::Append,
437            Self::Progress(_) => OperationKind::Progress,
438            Self::Assign(_) => OperationKind::Assign,
439            Self::Trim(_) => OperationKind::Trim,
440            Self::PartitionPolicy(_) => OperationKind::PartitionPolicy,
441            Self::Barrier(_) => OperationKind::Barrier,
442            Self::ProducerResultFloor(_) => OperationKind::ProducerResultFloor,
443        }
444    }
445}
446
447/// Resource bounds applied to canonical operation bodies.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct OperationLimits {
450    /// Maximum encoded canonical operation-body bytes.
451    pub max_body_bytes: usize,
452    /// Maximum UTF-8 bytes per stream or topic name.
453    pub max_name_bytes: usize,
454    /// Maximum independently addressed batches per APPEND.
455    pub max_append_batches: usize,
456    /// Maximum combined records per APPEND.
457    pub max_records: usize,
458    /// Maximum combined payload parts per APPEND.
459    pub max_parts: usize,
460    /// Maximum combined APPEND payload bytes.
461    pub max_payload_bytes: usize,
462}
463
464impl Default for OperationLimits {
465    fn default() -> Self {
466        Self {
467            max_body_bytes: 64 * 1024 * 1024,
468            max_name_bytes: 255,
469            max_append_batches: 1_024,
470            max_records: 1_024,
471            max_parts: 65_536,
472            max_payload_bytes: 64 * 1024 * 1024,
473        }
474    }
475}