Skip to main content

ozzy_journal/operation/
schema.rs

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