Adapters
The interfaces a host supplies, and the reference implementations that ship with the package. These live outside the core modules deliberately: they are hosts, not library internals.
The interfaces a host supplies.
The library owns the order of the durability contract. It never owns the
mechanism. Storage, transport and the clock are all supplied by the caller
through the structural protocols below, which is what keeps this package free of
sockets, TLS policy, retry loops and filesystem assumptions -- exactly as the
Odin core keeps them out of src/.
None of these are base classes. They are :class:typing.Protocol definitions, so
any object with the right shape satisfies them without importing anything.
Journal
Bases: Protocol
Ordered, durable storage for the records a transition produces.
The contract is the one the core states: persist every record of a transition, in order, and make it durable before any message of that transition leaves. A journal that reordered or dropped a record could let a crash revert a promise a peer has already acted on.
open
open(*, node_id: NodeId, configuration_id: int) -> None
Bind the journal to one participant before anything is read or written.
The session calls this with its own identity, so a caller states it once. A durable journal uses it to refuse a file that belongs to another member or another configuration: adopting a foreign journal would import its promises as this node's own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
The member this journal belongs to. |
required |
configuration_id
|
int
|
The configuration it belongs to. |
required |
Raises:
| Type | Description |
|---|---|
InvalidArgument
|
If existing storage names a different identity. |
StorageError
|
If the storage cannot be claimed. |
append
append(records: Sequence[WriteRecord]) -> None
Append records in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
Sequence[WriteRecord]
|
The batch, in the order the core produced it. |
required |
Raises:
| Type | Description |
|---|---|
StorageError
|
If the records could not be written. |
sync
sync() -> None
Make every appended record durable.
Raises:
| Type | Description |
|---|---|
StorageError
|
If durability could not be established. The caller must then treat persistence as uncertain and replay, never confirm. |
replay
replay() -> Sequence[WriteRecord]
Return every durable record, in the order it was appended.
Returns:
| Type | Description |
|---|---|
Sequence[WriteRecord]
|
The records needed to rebuild the node. |
Raises:
| Type | Description |
|---|---|
JournalCorrupt
|
If a complete record fails its checksum. That is corruption, not a truncated tail, and must stop recovery. |
close
close() -> None
Release the journal's resources.
Transport
Bases: Protocol
Moves opaque frames between peers.
The adapter owns framing, connection management and, critically, authentication: a sender id inside a frame is a claim, not proof. It must preserve frame boundaries and bound the bytes it queues.
A failed send may still have reached the peer. That is safe here, because protocol retransmission tolerates duplicates -- but it means a send failure must never roll back a durable transition.
send
send(*, peer: NodeId, frame: bytes) -> None
Deliver one frame to one peer, best effort.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
peer
|
NodeId
|
The recipient's identity. |
required |
frame
|
bytes
|
The encoded envelope. |
required |
Raises:
| Type | Description |
|---|---|
TransportError
|
If the frame could not be queued. |
receive
receive(*, timeout: float) -> tuple[NodeId, bytes] | None
Wait for one frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Seconds to wait, on a monotonic clock. |
required |
Returns:
| Type | Description |
|---|---|
tuple[NodeId, bytes] | None
|
The authenticated sender and the frame, or |
close
close() -> None
Release the transport's resources.
Clock
Bases: Protocol
A monotonic time source.
Monotonic, not wall-clock: a timeout measured against a clock that can step backwards is not a timeout. The core itself reads no clock at all; this exists only so the session can give its logical ticks a duration.
monotonic
monotonic() -> float
Return a monotonically non-decreasing time in seconds.
sleep
sleep(seconds: float) -> None
Pause for approximately seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
float
|
How long to wait. Never negative. |
required |
History
Bases: Protocol
Durable storage for entries released past the node's memory window.
Advancing the memory floor transfers responsibility for those entries from the bounded node to the host. They must already be here when that happens, because the node will not hold them again and a catch-up peer may ask.
record
record(slot: Slot, payload: bytes, kind: int) -> None
Durably retain one released entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slot
|
Slot
|
Its position in the log. |
required |
payload
|
bytes
|
Its bytes. |
required |
kind
|
int
|
Its entry kind, so a no-op stays distinguishable from a command that carried no bytes. |
required |
read
read(
first: Slot, count: int
) -> Sequence[tuple[Slot, bytes, int]]
cursor
cursor() -> Slot
Return the contiguous prefix this host has durably retained.
This is the application cursor, and it is deliberately separate from the node's released prefix. It is what a restart resumes from: a node told a floor it cannot actually serve would advertise history it has lost.
Returns:
| Type | Description |
|---|---|
Slot
|
The last slot retained with no gap before it, or zero. |
close
close() -> None
Release the history's resources.
Reference storage adapters.
These are hosts, not library internals. They live here the way
examples/counter.odin and bench/durable.odin live outside the Odin
src/: useful, replaceable, and deliberately not the only way to satisfy the
:class:~paxodin.protocols.Journal contract.
:class:FileJournal is the durable one. :class:MemoryJournal is for tests and
examples and says so loudly -- it survives nothing.
MemoryJournal
A journal that keeps records in memory.
Warning
This is not durable. It exists for tests and in-process examples. A
process that exits loses everything it held, so a node backed by one can
never be restarted -- use :class:FileJournal for anything real.
open
open(*, node_id: int, configuration_id: int) -> None
Accept any identity; there is no storage to bind it to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
int
|
Ignored. |
required |
configuration_id
|
int
|
Ignored. |
required |
append
append(records: Sequence[WriteRecord]) -> None
Append records in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
Sequence[WriteRecord]
|
The batch, in the order the core produced it. |
required |
sync
sync() -> None
Mark everything appended as "durable", which here means nothing.
replay
replay() -> Sequence[WriteRecord]
Return every record synced so far.
Returns:
| Type | Description |
|---|---|
Sequence[WriteRecord]
|
The records, in append order. Anything appended but never synced is |
Sequence[WriteRecord]
|
excluded, mirroring what a crash would have left behind. |
close
close() -> None
Drop the records.
FileJournal
A durable, checksummed, append-only journal in one node directory.
The directory is locked exclusively for the journal's lifetime, so two processes cannot both believe they are the same node. The header binds the file to a node identity, a configuration and a capacity profile: opening it as a different node, or with a differently sized build, is refused rather than silently misread.
Recovery distinguishes two things that look alike. A trailing record that is demonstrably incomplete is a torn tail from a crash mid-write, and is discarded. A complete record whose checksum fails is corruption, and stops recovery with the offset -- because skipping it would silently drop a promise or a vote that a peer already acted on.
open
open(*, node_id: int, configuration_id: int) -> None
Claim the directory and validate or write the header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
int
|
This member's identity, bound into the header. |
required |
configuration_id
|
int
|
The configuration, bound into the header. |
required |
Raises:
| Type | Description |
|---|---|
StorageError
|
If the directory is already locked by another process. |
InvalidArgument
|
If the header names a different node, configuration or capacity profile. |
JournalCorrupt
|
If the header itself is unreadable. |
append
append(records: Sequence[WriteRecord]) -> None
Append records in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
Sequence[WriteRecord]
|
The batch, in the order the core produced it. |
required |
Raises:
| Type | Description |
|---|---|
StorageError
|
If the write fails. Persistence is then uncertain and the caller must replay rather than confirm. |
sync
sync() -> None
Flush and fsync, making every appended record durable.
Raises:
| Type | Description |
|---|---|
StorageError
|
If durability could not be established. |
replay
replay() -> Sequence[WriteRecord]
Return every intact record, in append order.
Returns:
| Type | Description |
|---|---|
Sequence[WriteRecord]
|
The records needed to rebuild the node. |
Raises:
| Type | Description |
|---|---|
JournalCorrupt
|
If a complete record fails its checksum. The offset is reported; recovery stops rather than skipping it. |
close
close() -> None
Sync, release the lock, and close the file. Safe before open.
MemoryHistory
Retained history kept in memory, for tests and in-process examples.
Warning
Not durable. A node backed by one cannot serve catch-up after a restart.
record
record(slot: Slot, payload: bytes, kind: int) -> None
Retain one released entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slot
|
Slot
|
Its position in the log. |
required |
payload
|
bytes
|
Its bytes. |
required |
kind
|
int
|
Its entry kind. |
required |
read
read(
first: Slot, count: int
) -> Sequence[tuple[Slot, bytes, int]]
cursor
cursor() -> Slot
Return the contiguous prefix retained.
Returns:
| Type | Description |
|---|---|
Slot
|
The last slot retained with no gap before it, or zero. |
close
close() -> None
Drop the retained entries.
FileHistory
Durable retained history in one append-only file.
Advancing the node's memory floor hands an entry over to the host. This is where it lands, so a restarted member can still answer a catch-up peer for slots its bounded window let go.
V1 never trims this file. Bounded memory is not bounded disk, and saying so plainly is better than a compaction scheme with no trim-anchor contract behind it.
record
record(slot: Slot, payload: bytes, kind: int) -> None
Durably retain one released entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slot
|
Slot
|
Its position in the log. |
required |
payload
|
bytes
|
Its bytes. |
required |
kind
|
int
|
Its entry kind. |
required |
Raises:
| Type | Description |
|---|---|
StorageError
|
If the write fails. |
read
read(
first: Slot, count: int
) -> Sequence[tuple[Slot, bytes, int]]
cursor
cursor() -> Slot
Return the contiguous prefix durably retained.
Returns:
| Type | Description |
|---|---|
Slot
|
The last slot retained with no gap before it, or zero. |
close
close() -> None
Close the history file.