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}