Skip to content

Node

The low-level participant: one transition at a time, no I/O.

Node is the Python face of the Odin effect machine. It performs no I/O, owns no thread and reads no clock. A transition mutates the node and leaves a :class:PendingBatch describing what the host must now do -- persist these records, send these messages, release these entries -- in that order.

Most applications want :class:paxodin.Session, which runs that order for them. Reach for Node when the host needs to own scheduling or batching itself.

AbandonedWritesWarning

Bases: UserWarning

A node was closed while it still held unconfirmed writes.

Those records were handed out but never acknowledged durable, so the node must be rebuilt by journal replay rather than resumed.

PendingBatch

One retained effects batch: a durability obligation, not a result.

The order is fixed and the type enforces it::

writes -> append and sync -> persisted() -> messages / committed
-> requests -> finish()

Every probe and copy is idempotent, so a failed allocation can be retried without rerunning the transition that produced it.

Note

status is the protocol outcome and is independent of the effects. A transition can report an error and leave writes the host must still persist, so checking the status is never a substitute for discharging the batch.

status property

status: int

The protocol status of the transition, independent of its effects.

assigned_slot property

assigned_slot: Slot

The slot a proposal was admitted to, or zero.

requires_barrier property

requires_barrier: bool

True when the batch holds a promise or a vote.

A decision or a trim record is derived state a host may persist behind a cheaper barrier; a promise or a vote is the indelible ink whose loss lets a crash choose two values for one slot.

phase property

phase: BatchPhase

Where the batch has got to, so an interrupted caller can resume.

refresh

refresh() -> None

Re-read the batch report from the node.

writes

writes() -> list[WriteRecord]

Return the durable records, in journal order.

Returns:

Name Type Description
list[WriteRecord]

Every record of this transition. Append them in order and sync before

calling list[WriteRecord]

meth:persisted.

persisted

persisted() -> None

Declare every record from :meth:writes appended and synced.

This is the only call that unblocks the outputs, because transmitting a reply or releasing a decision before its record is on stable storage is exactly what lets a crash choose two values for one slot.

Raises:

Type Description
WritesNotCopied

If :meth:writes never returned the whole batch. The bridge refuses to acknowledge records the host never got.

StaleToken

If a later transition superseded this batch.

messages

messages() -> list[Envelope]

Return the outbound envelopes.

Returns:

Type Description
list[Envelope]

Owned envelopes, each stamped with its configuration.

Raises:

Type Description
WritesUnconfirmed

If the writes are not yet confirmed durable.

committed

committed() -> list[Committed]

Return the entries released by this transition, in slot order.

Returns:

Type Description
list[Committed]

Owned entries, contiguous with everything released before them.

Raises:

Type Description
WritesUnconfirmed

If the writes are not yet confirmed durable.

requests

requests() -> list[ServeRange]

Return history ranges peers asked for below this node's memory floor.

Serving one means transmitting commits from retained history, which is a send like any other, so it waits on the same confirmation.

Returns:

Type Description
list[ServeRange]

Owned request records.

Raises:

Type Description
WritesUnconfirmed

If the writes are not yet confirmed durable.

assigned_slots

assigned_slots() -> list[Slot]

Return the slots a batch proposal was admitted to.

Returns:

Type Description
list[Slot]

One slot per submitted value, in submission order.

finish

finish() -> None

Release the batch so the next transition may run.

Raises:

Type Description
WritesUnconfirmed

If the batch still holds unconfirmed writes. Releasing here would discard records the journal never took.

Node

One local participant, driven one transition at a time.

A node owns an opaque native handle. Exactly one call may be inside that handle at a time: a second thread blocks, and the same thread re-entering raises :class:ReentrantCall rather than deadlocking. Every value a node returns is an owned Python object whose lifetime is independent of the native window.

Example

node = Node(node_id=1, members=[1, 2, 3], configuration_id=1) with node.campaign() as batch: ... records = batch.writes() # append and sync these ... batch.persisted() ... outbound = batch.messages() node.close()

profile property

profile: Profile

The capacities this build was compiled with.

closed property

closed: bool

True once the native handle has been released.

restore classmethod

restore(
    *,
    node_id: NodeId,
    members: Sequence[NodeId],
    configuration_id: int,
    records: Sequence[WriteRecord],
    floor: Slot = 0,
    **options: Unpack[NodeOptions],
) -> Node

Rebuild a participant from its journal.

The records are folded with the lifetime fold, which tolerates the out-of-order promises a journal legitimately accumulates across restarts. The ledger never crosses the ABI: the bridge owns it for the duration of the replay and hands the core a finished one.

Parameters:

Name Type Description Default
node_id NodeId

This member's identity.

required
members Sequence[NodeId]

Every voting member of the configuration.

required
configuration_id int

The configuration this journal belongs to.

required
records Sequence[WriteRecord]

Every durable record, in append order.

required
floor Slot

The last slot the host durably consumed. Cells at or below it that hold only an open vote are cleared.

0
**options Unpack[NodeOptions]

The same tuning arguments :meth:__init__ accepts.

{}

Returns:

Type Description
Node

The restored node.

Raises:

Type Description
PaxodinError

If a record contradicts the ledger, which means the journal is corrupt or was replayed out of order.

state

state() -> NodeState

Return a snapshot of this participant.

Returns:

Type Description
NodeState

The role, ballot, leader, released prefix, memory floor and seal

NodeState

state, gathered in one call so they cannot disagree with each other.

Note

decided_through is this participant's released prefix. It is not a freshness guarantee: the core has no lease, so a local read can be arbitrarily stale.

campaign

campaign() -> PendingBatch

Start a campaign for leadership.

Returns:

Type Description
PendingBatch

The batch this transition produced.

tick

tick() -> PendingBatch

Advance the logical clock by one tick.

A tick is a logical unit, not a duration: the core counts election and heartbeat timeouts in ticks and leaves their real-time meaning to the host. :class:paxodin.Session binds it to a wall clock.

Returns:

Type Description
PendingBatch

The batch this transition produced.

propose

propose(value: bytes) -> PendingBatch

Submit one command.

Parameters:

Name Type Description Default
value bytes

The command bytes, at most profile.max_value_bytes. An empty command is legal and stays distinguishable from a no-op.

required

Returns:

Type Description
PendingBatch

The batch this transition produced. Its assigned_slot names the

PendingBatch

slot the command was admitted to when the status is success.

Raises:

Type Description
ValueTooLarge

If the command exceeds the profile. No proposal was admitted and the node is unchanged.

propose_batch

propose_batch(values: Sequence[bytes]) -> PendingBatch

Submit several commands, admitted whole or not at all.

Parameters:

Name Type Description Default
values Sequence[bytes]

Between one and profile.chunk_slots commands.

required

Returns:

Type Description
PendingBatch

The batch this transition produced. Read the slots with

PendingBatch

assigned_slots().

Raises:

Type Description
ValueTooLarge

If any command exceeds the profile.

InvalidArgument

If the count is zero or above chunk_slots.

step

step(envelope: Envelope) -> PendingBatch

Deliver one received message.

The configuration stamp is checked before the core sees anything: a mismatch resets the batch and reports it, with no writes, no messages and no state change. Stale traffic is refused, never relabelled.

Parameters:

Name Type Description Default
envelope Envelope

The decoded message, as the caller's codec produced it.

required

Returns:

Type Description
PendingBatch

The batch this transition produced.

reconnected

reconnected(peer: NodeId) -> PendingBatch

Tell the node a peer is reachable again.

Parameters:

Name Type Description Default
peer NodeId

The peer's identity.

required

Returns:

Type Description
PendingBatch

The batch this transition produced.

request_catch_up

request_catch_up(
    peer: NodeId, from_slot: Slot
) -> PendingBatch

Ask a peer for decided history from from_slot.

Parameters:

Name Type Description Default
peer NodeId

The peer to ask.

required
from_slot Slot

The first slot wanted.

required

Returns:

Type Description
PendingBatch

The batch this transition produced.

install_chosen_trim

install_chosen_trim(
    trim_id: int, chosen_trim_slot: Slot
) -> PendingBatch

Adopt a trim anchor certified by the host.

Parameters:

Name Type Description Default
trim_id int

The host's identity for the state image.

required
chosen_trim_slot Slot

The slot the image folds in, inclusive.

required

Returns:

Type Description
PendingBatch

The batch this transition produced.

advance_memory_floor

advance_memory_floor(through: Slot) -> None

Record that released entries through through are durably consumed.

This frees window cells for reuse, which is why it refuses while a batch is live: the batch's released entries still point into those cells, and losing them would silently break a contiguously released prefix.

Parameters:

Name Type Description Default
through Slot

The last durably consumed slot.

required

Raises:

Type Description
BatchPending

If a batch has not been finished.

set_campaign_enabled

set_campaign_enabled(enabled: bool) -> None

Allow or forbid this participant from starting elections.

Parameters:

Name Type Description Default
enabled bool

Whether campaigning is permitted.

required

decided_span

decided_span(from_slot: Slot) -> int

Return how many decided entries sit at or above from_slot.

Parameters:

Name Type Description Default
from_slot Slot

The first slot of interest.

required

Returns:

Type Description
int

The count, so a caller can size a buffer before reading.

Raises:

Type Description
Trimmed

If from_slot has fallen below the memory floor. Read it from retained history instead; the node no longer holds it.

read_decided

read_decided(
    from_slot: Slot, limit: int
) -> list[Committed]

Read a bounded window of the decided prefix.

Parameters:

Name Type Description Default
from_slot Slot

The first slot to read.

required
limit int

The most entries to return.

required

Returns:

Type Description
list[Committed]

Up to limit entries, in slot order.

Raises:

Type Description
Trimmed

If from_slot has fallen below the memory floor.

close

close() -> None

Release the native handle. Calling this again does nothing.

Closing while a batch still holds unconfirmed writes abandons them and warns: those records were handed out but never acknowledged durable, so the node must be rebuilt by journal replay rather than resumed.

Raises:

Type Description
ReentrantCall

If called from inside another call on this handle.