Session
The durable participant: one order, no mechanism.
:class:Session runs the sequence the core's contract requires -- persist the
writes, confirm them, release the decisions, send the messages, serve the
catch-up requests, finish the batch -- and nothing else. Every byte it moves
goes through an adapter the caller supplied, so this module contains no socket,
no filesystem assumption beyond the journal protocol, and no retry policy.
It reads a clock for exactly one reason: the core counts its election and
heartbeat timeouts in logical ticks and deliberately leaves their duration to the
host. tick_interval is where that decision is made, once, visibly.
Session
One participant, driven through a caller-supplied journal and transport.
A session represents this member. The other members must also be running; nothing here starts them, forwards to them, or discovers them.
Example
from paxodin import Session from paxodin.storage import FileHistory, FileJournal with Session( # doctest: +SKIP ... node_id=1, ... members=[1, 2, 3], ... configuration_id=1, ... journal=FileJournal("state/node-1"), ... history=FileHistory("state/node-1"), ... transport=transport, ... ) as session: ... receipt = session.append(b"set counter 41", timeout=5.0) ... print(receipt.slot)
Note
There is no background thread. Between appends, call :meth:poll to
drive incoming traffic, ticks and retransmission.
node
property
node: Node
The underlying participant, for hosts that need direct control.
node_id
property
node_id: NodeId
This participant's identity.
is_leader
property
is_leader: bool
Whether this participant currently believes it leads.
A belief, not a lease: the engine has no lease, so this can be stale the moment it is read. It says who will try to sequence a command.
leader
property
leader: NodeId | None
The member this participant thinks leads, if it has heard from one.
last_committed
property
last_committed: Slot
The last slot this participant has released, in order. Zero if none.
state
state() -> NodeState
Return a snapshot of this participant.
Returns:
| Type | Description |
|---|---|
NodeState
|
Its role, ballot, released prefix and seal state. |
Note
This is what this member knows. The core has no lease, so it is never a statement about the cluster right now.
discharge
discharge(batch: PendingBatch) -> None
Run the durability order for one batch through this session's adapters.
Persist, confirm, release, send, serve, finish. The order is the whole point: a message that left before its record was durable can be reverted by a crash, and two different values can then be chosen for one slot.
This is public so a host that drives :attr:node directly -- to batch
proposals, or to sequence its own transitions -- can still hand each
batch to the session for the part it must not get wrong.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
PendingBatch
|
A pending batch produced by :attr: |
required |
Raises:
| Type | Description |
|---|---|
StorageError
|
If the journal could not persist the records. The batch is left unconfirmed and the node blocked; reopen and replay. |
poll
poll(*, timeout: float = 0.0) -> int
Drive incoming traffic, ticks and retransmission.
There is no background worker, so a participant only makes progress
while someone calls this or :meth:append.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Seconds to wait for traffic, on the monotonic clock. Zero processes everything already queued and returns without waiting. |
0.0
|
Returns:
| Type | Description |
|---|---|
int
|
How many frames were processed. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
campaign
campaign() -> None
Stand for leadership immediately, without waiting for a timeout.
append
append(value: bytes, *, timeout: float = 5.0) -> Receipt
Submit a command and drive progress until it is durably released.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
bytes
|
The command bytes, at most |
required |
timeout
|
float
|
Seconds to wait, on a monotonic clock. |
5.0
|
Returns:
| Type | Description |
|---|---|
Receipt
|
A receipt naming the configuration and slot that hold the command. |
Raises:
| Type | Description |
|---|---|
NotLeader
|
This participant is a follower. No forwarding is performed; route the request to the leader yourself. |
ValueTooLarge
|
The command exceeds the profile. Nothing was admitted. |
ProposalLost
|
A different value was decided in the slot this command was admitted to. In single-leader mode there is no resubmission, so a leader that loses its ballot mid-flight drops the command. |
CommitTimeout
|
The wait ended. This did not cancel anything: the value may still be chosen. Keep polling and inspect local history; retry only behind an application-level command id. |
ValueError
|
If |
Note
Success means agreement and release at this participant. It does not mean every peer received the command, and it never means the application applied it. Exactly-once application is an application protocol, not something a receipt can establish.
entries
entries(
start: Slot = 1, *, limit: int | None = None
) -> Iterator[Committed]
Iterate this participant's released entries from start, in order.
This reports what this member knows; it is not a freshness guarantee and may lag the cluster arbitrarily.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
Slot
|
The first slot to yield. |
1
|
limit
|
int | None
|
Stop after this many, or run to the end of the released prefix. |
None
|
Yields:
| Type | Description |
|---|---|
Committed
|
Entries in slot order, each an owned object. |
Example
from paxodin.testing import Cluster with Cluster(3) as cluster: ... _ = cluster.append(b"a") ... _ = cluster.append(b"b") ... [e.entry.body for e in cluster.session(1).entries()] == [b"a", b"b"] True
committed_since
committed_since(
slot: Slot, *, limit: int = 128
) -> list[Committed]
close
close() -> None
Close the node, the journal, the transport and the history.
Closing stops local work. It does not withdraw a proposal that peers may still choose.