videoflow.messaging package
Submodules
videoflow.messaging.grouping module
Input-group assembly for multi-parent (join) nodes, factored out of the messenger so the two grouping strategies are testable without a broker:
TraceGroupAssembler— groups by exacttrace_idmatch: inputs that descend from the same originating message of a single upstream producer (diamond topologies). This is the historical behavior.TimeGroupAssembler— groups by event time: inputs whoseevent_tsfall within a tolerance of each other, regardless of lineage. This is how streams from independent producers (multiple cameras, sensors) are fused, with optional quorum emission (>= k of N parents at timeout) and per-parent “collect” windows for high-rate sensor parents that join many-to-one.
An assembler is fed decoded envelope entries (see
videoflow.wire.serialization.decode_envelope) paired with their broker ack
handles. It owns the pending buffers and resolves the handles of anything it
discards (evicted, expired) and supersedes (retired locally, no broker
settlement); handles of everything it emits
travel out unresolved inside the ReadyGroup for the task loop to ack/fail
after processing — preserving ack-after-process semantics end to end.
This module also owns the internal record types for those entries.
decode_envelope returns a plain dict and must keep doing so — it is
re-exported through the frozen videoflow.serialization shim, where a
dead-lettered payload recorded under the old module path still has to decode.
So the dict is adapted once, where messaging first receives it
(NATSMessenger._pull_loop), into an EnvelopeEntry, and everything from
there down the join/EOS path uses typed attributes. A collected window is a
genuinely different record — its message/metadata/event_ts are
lists over a parent’s samples and it has no lineage of its own — so it is a
separate CollectEntry rather than an EnvelopeEntry with lying types.
All methods are called from the single task thread; no locking is needed.
- class videoflow.messaging.grouping.CollectEntry(message: list = <factory>, metadata: list = <factory>, event_ts: list = <factory>, is_stop_signal: bool = False)[source]
Bases:
objectThe window of samples a collect parent contributes to one time group: every sample within that parent’s window of the group’s time, oldest first.
Deliberately not an
EnvelopeEntry—message,metadataandevent_tsare lists here, and the window has no single trace id or seq of its own (which is whylast_input_inforeportsNonefor both).- event_ts: list
- is_stop_signal: bool
- message: list
- metadata: list
- class videoflow.messaging.grouping.EnvelopeEntry(trace_id: str, seq: int, event_ts: float | None, message: Any, metadata: dict | None, is_stop_signal: bool, type: str = '', producer_name: str = '', flow_id: str = '', run_id: str = '', span_id: Any = None, parent_span_id: Any = None, replica_id: int = 0, blob_ref: str | None = None)[source]
Bases:
objectOne decoded envelope as the messaging layer carries it: the exact field set
videoflow.wire.serialization.decode_envelopereturns, as attributes.Built at the messenger’s receive boundary via
from_decodedand never mutated afterwards — an assembler that supersedes a redelivery swaps the whole entry rather than editing one.- blob_ref: str | None
- event_ts: float | None
- flow_id: str
- classmethod from_decoded(decoded: dict[str, Any]) EnvelopeEntry[source]
Adapts a
decode_enveloperesult to anEnvelopeEntry.- Arguments:
decoded (dict): the envelope dict from
videoflow.wire.serialization.decode_envelope.
- Returns:
The equivalent
EnvelopeEntry. Only the fields the join path actually reads are required; the rest are tolerated as absent (carrying their declared default) so a caller can hand over a partial envelope.
- Raises:
KeyError: if
trace_idorseqis missing — without them a group has no identity to assemble on.
- is_stop_signal: bool
- message: Any
- metadata: dict | None
- parent_span_id: Any
- producer_name: str
- replica_id: int
- run_id: str
- seq: int
- span_id: Any
- trace_id: str
- type: str
- class videoflow.messaging.grouping.GroupAssembler(node_name: str, parent_names: list[str], policy: JoinPolicy)[source]
Bases:
objectBase interface: feed entries with
add, expire withsweep, drain withpop_ready.- add(parent_name: str, entry: EnvelopeEntry, handle: Any) None[source]
- evictions
Groups discarded (evicted or expired) so far — read by the messenger’s drop accounting.
- has_pending_from(parent_name: str) bool[source]
Whether any buffered state still holds a message from this parent (EOS drain check).
- oldest_wait_seconds(now: float | None = None) float[source]
How long the oldest incomplete group has waited (0 when none).
- pop_ready(now: float | None = None) ReadyGroup | None[source]
- videoflow.messaging.grouping.GroupEntry = videoflow.messaging.grouping.EnvelopeEntry | videoflow.messaging.grouping.CollectEntry | None
a normal entry, a collected window, or
Nonefor a parent missing from a quorum emission.- Type:
What one parent contributes to a
ReadyGroup
- class videoflow.messaging.grouping.ReadyGroup(trace_id: str, seq: int, event_ts: float | None, entries: dict[str, EnvelopeEntry | CollectEntry | None], handles: list)[source]
Bases:
objectA fully assembled input group, ready to hand to the node.
- Attributes:
trace_id: identity the node’s output will carry forward (a parent trace id for trace groups; a minted time-window id for time groups).
seq: deterministic representative sequence number (stable across redelivery of the same group, for output dedup).
event_ts: the group’s event time (min over members), carried forward.
entries:
{parent_name: entry}where entry is anEnvelopeEntry,Nonefor a parent missing from a quorum emission, or aCollectEntryfor a collect parent’s window.handles: every unresolved ack handle backing this group, in no particular order.
- class videoflow.messaging.grouping.TimeGroupAssembler(node_name: str, parent_names: list[str], policy: JoinPolicy)[source]
Bases:
GroupAssemblerGroups by event time. A message from a synchronized parent joins the pending group whose time is nearest to its
event_ts(and withintolerance_ms) among groups not already holding that parent; otherwise it seeds a new group. Messages from collect parents are buffered per parent and attached as a list at emission time (every sample within that parent’s window of the group’s time).A group is ready when every sync parent is present — held for the largest collect window first, so trailing high-rate samples can arrive — or when the policy timeout expires and at least
quorumsync parents are present (missing parents emit asNone). A timed-out group below quorum is evicted per the missing policy.Messages without an
event_ts(pre-v3 upstream) fall back to their arrival time — correct enough for co-located low-latency flows, but real deployments should stamp at the producer.- add(parent_name: str, entry: EnvelopeEntry, handle: Any) None[source]
- has_pending_from(parent_name: str) bool[source]
Whether any buffered state still holds a message from this parent (EOS drain check).
- oldest_wait_seconds(now: float | None = None) float[source]
How long the oldest incomplete group has waited (0 when none).
- pop_ready(now: float | None = None) ReadyGroup | None[source]
- class videoflow.messaging.grouping.TraceGroupAssembler(node_name: str, parent_names: list[str], policy: JoinPolicy)[source]
Bases:
GroupAssemblerGroups by exact
trace_id. A group is ready when every parent’s half with the same trace id has arrived; a group that outlives the policy timeout is evicted per the missing policy, and the oldest group is evicted (as drop) beyondmax_pending.- add(parent_name: str, entry: EnvelopeEntry, handle: Any) None[source]
- has_pending_from(parent_name: str) bool[source]
Whether any buffered state still holds a message from this parent (EOS drain check).
- oldest_wait_seconds(now: float | None = None) float[source]
How long the oldest incomplete group has waited (0 when none).
- pop_ready(now: float | None = None) ReadyGroup | None[source]
- videoflow.messaging.grouping.make_assembler(node_name: str, parent_names: list[str], policy: JoinPolicy) GroupAssembler[source]
videoflow.messaging.jetstream_backend module
NATS JetStream as a MessagingBackend — the transport half of what
NATSMessenger used to be all of.
The split follows the contract in videoflow.backends.messaging: this adapter
owns the connection, its asyncio loop thread, the streams and durables it
provisions, the pull loops that prefetch deliveries into bounded per-subscription
queues, the leases it keeps alive, and the settlement of every delivery token it
hands out. It knows nothing about joining inputs, dead-letter policy, payload
bytes or end-of-stream drains — those stay in the messenger, which composes this
with a payload store.
What the contract buys, concretely:
A delivery token names the subscription, the message, the broker’s attempt count and the subscription generation. A redelivery of the same message makes the earlier token stale: settling it returns
SettleStalewithout a broker call, so a superseded handle can never TERM the delivery that replaced it (MSG-011).Settlement is confirmed or unknown, never assumed.
Completedusesack_syncand reportsSettleUnknownwhen the acknowledgement is not confirmed within the timeout — the caller must not release a payload obligation on that (PAY-004).Terminalrequires a durable record reference (DELIV-15).Publication outcomes are typed. A deadline that expires after the send is
PublicationUnknown; a broker refusal isRejectedwithretryablesaying whether waiting helps (a full BATCH stream) or not (no stream captures the subject); a lost receipt is never reported as rejection (MSG-013, MSG-014).observe_publicationresolves an unknown send by an idempotent re-publish inside the stream’s duplicate window and isUnresolvablebeyond it.Observations distinguish zero from unknown.
observe_subscriptionisUnknownwhen the consumer could not be read, and itsunresolvedcount is fed by the server’s ownMAX_DELIVERIESadvisories — and, throughmark_exhausted, by the messenger’s ledger when the retry budget lives there (max_deliver = -1) and a dead letter could not be recorded — so work the broker retains but will never resolve on its own is not reported as “nothing pending” (MSG-012).Stop-receive is separate from shutdown.
stop_receiving(the messenger’squiesce) halts the pull loops and hands every parked delivery back at once while the deliveries already handed out stay the receiver’s to settle;shutdownhands back whatever is still held. A SIGTERMed worker runs only the first before it dies, and that is what returns its prefetched inputs to the survivors now instead of afterack_wait(RUN-030).
Everything a NATS API behaviour relies on is cited against nats-py 2.15.0
(.venv/lib/python3.12/site-packages/nats): Msg.ack_sync waits for the
server’s reply and marks the message acked only then (nats/aio/msg.py);
nak/term/in_progress are fire-and-forget (same file); JetStreamContext.publish
raises nats.js.errors.APIError for a server-side refusal and
nats.errors.TimeoutError when no PubAck arrives (nats/js/client.py);
pull_subscribe creates the durable from config when consumer_info says it
does not exist (nats/js/client.py); a JetStream Msg.metadata carries
num_delivered and sequence.stream (nats/aio/msg.py).
- videoflow.messaging.jetstream_backend.ACK_CONFIRM_SECONDS = 2.0
How long a
Completedsettlement waits for the server’s ack reply before it is reportedSettleUnknown(Msg.ack_sync(timeout), nats-py 2.15.0).
- videoflow.messaging.jetstream_backend.FETCH_TIMEOUT_SECONDS = 1.0
How long one pull request waits for a message before the loop re-checks whether it should stop. Also the unit
receive_anywaits in.
- class videoflow.messaging.jetstream_backend.JetStreamMessagingBackend(nats_url: str, flow_id: str, run_id: str, flow_type: str, prefetch: int = 4, fetch_timeout: float = 1.0, ack_confirm_seconds: float = 2.0, keepalive: bool = True, connect_timeout: float = 30.0, byte_budget: int | None = None)[source]
Bases:
MessagingBackend- Arguments:
nats_url: the server, e.g.
nats://localhost:4222.flow_id / run_id / flow_type: the run this backend serves; every channel and subscription it touches belongs to it.
prefetch: per-subscription local prefetch depth (
DEFAULT_PREFETCH).fetch_timeout: seconds one pull request waits (
FETCH_TIMEOUT_SECONDS).ack_confirm_seconds: how long
Completedwaits for the broker’s reply.keepalive: whether to extend the lease of every unsettled delivery periodically (
ack_wait / 3), as the messenger always has.byte_budget: the most envelope bytes this receiver holds unsettled — parked or handed out — before its pull loops pause (RUN-025;
VF_PREFETCH_BYTES). A fetch already in flight when the budget fills still lands, so the resident total may exceed it by one message: the documented atomic-admission tolerance. None: unbounded.
- capabilities() MessagingCapabilities[source]
What this broker offers, with the parts that need a read-back reported as observed:
max_payloadfrom the connection, replication and storage from the first stream this backend provisioned (ensure_channel), andUnknown('unread')for either until then.
- close(owned: Sequence[ChannelId], expected_generation: str) CleanupObservation[source]
Delete exactly the owned channels’ streams by ownership (
topology.delete_run_streams).
- property connected: bool
- ensure_channel(spec: ChannelSpec, operation_id: str) VerifiedChannel[source]
Create or reconcile the channel and read back its effective configuration. An immutable mismatch (retention, replicas) raises
IncompatibleProfile; “already exists” is never accepted without inspection.
- ensure_subscription(spec: SubscriptionSpec, operation_id: str) VerifiedSubscription[source]
Bind the subscription’s durable (creating it when absent, from a config that reproduces today’s bytes), read it back, and start prefetching into its queue. A data subscription also subscribes to the broker’s
MAX_DELIVERIESadvisory for its durable, which feedsunresolved.
- property loop: AbstractEventLoop
- mark_exhausted(subscription: SubscriptionId, stream_sequence: int | None) None[source]
Count a retained message as unresolved work in
observe_subscriptionwithout a broker advisory: the messenger’s ledger found its retry budget exhausted and could not record its dead letter (DELIV-15’spending_handoff), so the broker will keep redelivering it and nothing will resolve it until the record exists. Cleared by the settlement that eventually terminates it, or by the message leaving the stream.
- observe_ack_floor(subscription: SubscriptionId) Known[int] | Unknown[source]
consumer_info().ack_floor.stream_seqof any data durable of the run, bound here or not.
- observe_channel(channel: ChannelId) Known[ChannelObservation] | Unknown[source]
stream_info().stateof the channel’s stream: first/last sequence, message count, retention.
- observe_publication(envelope: Envelope) Accepted | Rejected | PublicationUnknown | PublicationUnresolvable[source]
Resolve an earlier
PublicationUnknownby an idempotent re-publish: inside the stream’s duplicate window the broker answersduplicatewhen the first send was stored and stores it now when it was not — either way one accepted copy. Outside the window nothing can be known:Unresolvable.
- observe_subscription(subscription: SubscriptionId) Known[SubscriptionObservation] | Unknown[source]
- prefetched(subscription: SubscriptionId) int[source]
Deliveries fetched from the broker and parked locally for
subscription(0 when the backend keeps none).
- publish(envelope: Envelope, deadline: float) Accepted | Rejected | PublicationUnknown | PublicationUnresolvable[source]
Publish with a monotonic deadline; on expiry the outcome is
PublicationUnknown, never a guess.
- receive(subscription: SubscriptionId, item_credit: int, byte_credit: int, deadline: float) list[Delivery][source]
- receive_any(subscriptions: Sequence[SubscriptionId], item_credit: int, byte_credit: int, deadline: float) list[tuple[SubscriptionId, Delivery]][source]
Waits until
deadline(monotonic) for any of the subscriptions’ queues to hold a delivery and returns everything that became ready in that wait — every itemasyncio.waitreports done has already been dequeued, so all of them are returned (discarding all but one lost messages once).
- renew(token: DeliveryToken) LeaseObservation[source]
- resident_bytes() int[source]
Envelope bytes this receiver holds unsettled right now (parked or handed out).
- set_admission(subscription: SubscriptionId, admit: Callable[[Delivery], bool], on_skip: Callable[[Delivery], None] | None = None) bool[source]
Ask the backend to run
admiton every delivery ofsubscriptionbefore it is parked forreceive: a delivery it refuses is settledCompletedby the backend at once — so a message this consumer will never process does not occupy its ack window — and, once that settlement is confirmed, handed toon_skipoff the backend’s own threads.admitruns on the backend’s thread and must be cheap and must not block. Returns False when the backend keeps no such filter; the caller then decides on the receiving side.
- settle(token: DeliveryToken, outcome: Completed | Retry | Terminal, settlement_id: str) SettleConfirmed | SettleUnknown | SettleStale[source]
- shutdown(hand_back: bool = True) None[source]
Stop the pull loops, hand back every delivery this receiver still holds, flush pending publishes, close the connection and the loop thread.
The hand-back is what makes a scale-down or a rollout cheap: a delivery parked in the prefetch queue, or received and never settled, would otherwise sit leased until
ack_waitlapsed — on a replica that no longer exists. A NAK returns it to the broker at once for the survivors. A delivery already settled is left alone (MsgAlreadyAckdError).- Arguments:
hand_back: False closes without the NAKs — what a crashed process looks like to the broker (its leases lapse on their own). Tests simulating a crash pass it; a worker never does.
- skipped(subscription: SubscriptionId) int[source]
Deliveries the admission filter refused and this backend acked on the consumer’s behalf.
- start() None[source]
Connect on a fresh loop thread. Raises
BrokerUnavailablewhen the server cannot be reached.
- stop_receiving() int[source]
Stop admitting input on every bound subscription — the quiesce half of a graceful retirement (RUN-030): the pull loops fetch nothing more, and every delivery parked in a prefetch queue (fetched, never handed to a receiver) is NAKed back to the broker at once, so a survivor picks it up now rather than after
ack_waitlapses on a process that is about to die. The deliveries already handed out stay this receiver’s to settle (shutdownhands back what is still held then). Idempotent; a second call hands back nothing more.- Returns:
how many parked deliveries were handed back.
- subscribe_control(callback: Callable[[], None]) None[source]
Invoke
callback(on the loop thread) when the run’s flow-wide stop is published.
- supersede(token: DeliveryToken) bool[source]
Retire a delivery token locally — the receiver now holds a newer attempt of the same logical message — without any broker settlement. Returns whether the token was live. A later
settleof the retired token isSettleStale. The default is a no-op for adapters whose tokens are already fenced by attempt and generation.
- videoflow.messaging.jetstream_backend.channel_spec_for(flow_id: str, run_id: str, node: str, flow_type: str, profile: str, required: Sequence[SubscriptionId] = (), realtime_buffer: int = 1, batch_max_msgs: int = 10000, replicas: int = 1, max_bytes: int | None = None) ChannelSpec[source]
The channel a node’s output stream must provide, from the flow type — today’s stream settings, as a spec.
replicasis the copies a replicated broker keeps (STREAM-14);max_bytescaps the stream’s storage (a capacity fixture); both default to today’s unset values.
- videoflow.messaging.jetstream_backend.consumer_credit(nb_tasks: int, partitioned: bool, item_credit: int = 1, prefetch: int = 4) int[source]
STREAM-15: the broker-sidemax_ack_pendinga subscription needs so every replica can holditem_creditinputs in processing plusprefetchparked, without one replica’s un-acked work starving another’s — a shared (competing) durable multiplies by the replica count, a per-replica (partitioned) durable does not. Provisioning and the worker call this so the durable created and the durable bound agree (MSG-017).
- videoflow.messaging.jetstream_backend.stream_config_for_spec(spec: ChannelSpec, generation: str | None = None) nats.js.api.StreamConfig[source]
The JetStream
StreamConfiga channel spec means — byte-identical to today’s for a default spec.
- videoflow.messaging.jetstream_backend.subscription_spec_for(subscription: SubscriptionId, ack_wait_seconds: float, max_deliver: int, credit: int, byte_credit: int = 0) SubscriptionSpec[source]
A subscription spec:
creditis the broker-sidemax_ack_pendingthis subscription is bound with.
videoflow.messaging.nats_messenger module
NATS JetStream-backed implementation of videoflow.core.engine.Messenger.
One JetStream stream per node (subject vf.{flow_id}.{node.name}); a node’s
messenger publishes only its own output there. Each real parent gets its own
durable pull consumer, named after the consuming node so that replicas of the
same consuming node (nb_tasks > 1) share one durable name (competing
consumers / load balancing), while distinct children of the same parent get
distinct durable names (each gets its own full copy — broadcast fan-out).
The transport itself — connection, loop thread, streams, durables, prefetch,
leases, settlement, publication outcomes — lives in
videoflow.messaging.jetstream_backend.JetStreamMessagingBackend behind the
MessagingBackend contract; this class composes it with a payload store and
keeps what is the messenger’s to decide: trace and sequence bookkeeping, join
assembly, the end-of-stream drain, the delivery-policy ladder, dead-lettering,
and when a payload obligation is released (only after a confirmed settlement).
Every delivery is a DeliveryToken from the backend, so a superseded handle
can never terminate the attempt that replaced it, and every payload is fetched
on the receiving thread after ownership is decided, never on the broker loop.
- class videoflow.messaging.nats_messenger.NATSMessenger(node: Node, parent_names: list[str], nats_url: str, flow_id: str, flow_type: str, run_id: str, blob_store: BlobStore | None = None, replica_id: int = 0, ack_wait: int = 60, max_retries: int = 3, eos_quiescence_ms: int = 500, nb_tasks: int = 1, partition_by: str | None = None, join_policy: dict | None = None, envelope_version: int | None = None, blob_readers: int | None = None, blob_ttl_seconds: int | None = None, delivery_policy: dict | None = None, backend: MessagingBackend | None = None, payload_store: PayloadStore | None = None, blob_reader_ids: list[str] | None = None, runtime: FlowRuntime | None = None, replayable: bool = False, prefetch_bytes: int | None = None)[source]
Bases:
Messenger- Arguments:
node: the
videoflow.core.node.Nodethis messenger is bound to.parent_names ([str]): the real parents of
node, by.name.nats_url (str): e.g.
nats://localhost:4222.flow_id (str): shared across every node in the flow.
flow_type (str):
videoflow.core.constants.REALTIMEorBATCH— controls the stream retention/discard policy used fornode’s own output stream.blob_store: optional
videoflow.wire.serialization.BlobStorefor payloads over the inline size threshold (the RFC 0002 counter store).blob_readers (int): how many downstream reads each message this node publishes receives (Σ over children of
nb_tasksif partitioned else 1, computed by the compiler); enables refcounted blob reclamation (BLOB-5).Nonedisables it (blobs are TTL-only).blob_ttl_seconds (int): TTL for offloaded payloads;
Nonepicks the flow-type default (3600s realtime / 86400s batch, BLOB-7).join_policy (dict): serialized
videoflow.core.policies.JoinPolicycontrolling how multi-parent input groups are formed (by trace id or by event time) and expired; defaults per flow type when unset. The policy’smax_pendingbounds how many not-yet-complete groups are held in memory before the oldest is evicted.backend: the
MessagingBackendto compose; aJetStreamMessagingBackendonnats_urlwhen None.payload_store: an obligation-keeping
PayloadStore(RFC 0006). Used instead ofblob_storewhen given: puts acquire the reader obligations inblob_reader_idsand the publisher’s intent, releases happen by reader id after a confirmed settlement (BLOB-14).blob_reader_ids ([str]): the reader obligations every put acquires (
VF_BLOB_READER_IDS):<child>per competing child,<child>/p<i>per partitioned replica.runtime: the node’s
FlowRuntimeledger (RFC 0006 CTRL-4): terminators and received sets, the outbox, attempt counts, the terminal log, pending dead-letter handoffs, open groups. A memory-backed one when None — every record then lives and dies with this process, and the messenger knows it (runtime.durable_shared()).replayable (bool): this producer mints ids from its source offset (
MSGID-6) and checkpoints the last accepted one; a live source mints{node}:{epoch}:{n}(MSGID-5). Under the switch only.prefetch_bytes (int): the most envelope bytes the default backend holds unsettled before it stops fetching (
VF_PREFETCH_BYTES, RUN-025); None leaves it unbounded.
- ack_inputs() None[source]
Acknowledge the input group last returned by
receive_message— the node processed it successfully (and, for a processor, already published its output).
- check_for_termination() bool[source]
Returns true if a flow-wide termination signal has been received on the control channel. Used by
videoflow.core.task.ProducerTaskto stop pulling new input even before it naturally reachesStopIteration.
- checkpoint(state: bytes) None[source]
One ledger write: the node’s state and the group it was updated with (
last_input_key), so a replacement’s restored state and the inputs it must still see describe the same committed prefix (RUN-022). The group is remembered as covered: if it is redelivered to this process’s replacement, it is acknowledged without being handed to the node again.This is the immediate form, for a node whose input produces no output (a sink, a leaf): its checkpoint is the commit. A node with an output hands its checkpoint to
publish_messageinstead (RuntimeContext), and_publishcommits state and output together. A superseded owner is refused (RUN-023); a ledger that dies with the process is refused too, rather than faking durability (RUN-022).
- fail_inputs(exc: BaseException) None[source]
The node raised while processing the last input group. The action is decided by
DeliveryPolicy.action_forfrom how the error classified and how many times the broker has delivered it — not by the flow type alone. This messenger only executes the verdict.The difference that matters: a poison message is dead-lettered on its first failure rather than burning four attempts on its way to the same place, and a worker-fatal error naks without dead-lettering, because the message is fine and this worker is not. A delivery is terminated only against a durable record: the dead letter’s id when one was accepted, an entry in this node’s terminal log otherwise (DELIV-15); a dead-letter publish that failed keeps the delivery alive for a later attempt.
- join_status() dict[str, Any][source]
What the join is doing right now (RUN-006): how many groups wait for a missing member, how long the oldest has waited, and whether the policy would ever give up on them (
missing = 'wait'never does — a node in that state is waiting, which the health seam must not report as healthy processing progress). Cancellation is the control stop:quiesce/the flow-wide stop ends the wait and hands the halves back.
- last_input_info() dict[str, dict | None] | None[source]
Per-parent envelope info (
event_ts,metadata,trace_id,seq) for the input group last returned byreceive_message;Noneentries for parents missing from a quorum emission.Nonefor producers.
- last_input_key() str | None[source]
A stable identity for the input group last returned by
receive_message, derived from its trace_id + seq — used as an idempotency key by a sink. The same logical event yields the same key across redelivery/restart.
- pending_count() int[source]
Messages waiting for this node across all its parents — locally prefetched, held in incomplete join groups, or still on the broker. Parents whose broker query failed contribute only their local queue; use
pending_observationwhere “failed” must not read as “zero”.
- pending_observation() Known[int] | Unknown[source]
pending_countas an observation:Unknownas soon as any parent’s broker query failed, because a total that silently omits one parent is exactly the “zero pending” that declared a stalled node idle. FeedsProgressDeadline, which neither resets nor trips on Unknown.
- publication_stats: dict[str, int]
accepted, duplicate, unknown, dropped, rejected.
- Type:
Publication outcomes by kind
- publish_abort(error: Any) None[source]
Publishes an abnormal end-of-stream carrying why this node died.
Rides the same
_eossubject as a clean EOS, which is the point: it reuses the per-replica EOS consumers and the provisioning interest anchor unchanged, so a marker published by a dying node is still retained and still reaches every downstream replica. Its dedup id is distinct from the clean marker’s so a node that aborts is never mistaken for one that finished.
- publish_message(message: Any, metadata: dict | None = None) None[source]
Publish one data output.
metadatamay carry the node’s pending checkpoint under the reservedCHECKPOINT_METADATA_KEY(the task puts it there,RuntimeContext.checkpoint); it is taken off here, committed with the output in one ledger write, and never reaches the wire.
- publish_stop_signal() None[source]
Publishes a termination marker on this node’s own subject. Unlike
publish_message, this is never dropped regardless of retention policy — every downstream consumer must observe it exactly once.
- quiesce() None[source]
Stop admitting input (SIGTERM, a scale-down, a rollout): what is already held by the task still settles normally, and what the adapter prefetched but never handed out goes back to the broker at once, so a survivor picks it up now rather than after
ack_waitlapses on a process that is about to die (RUN-030). Idempotent. A quiesced process publishes no EOS: its replacement continues the stream (stop_reason).
- receive_message() dict[source]
Blocks until this node has received a complete input: one message from every real parent, all derived from the same upstream originating event (see the
trace_idpropagation scheme in the concrete implementation) — or until every parent has signaled termination.- Returns:
a dict
{parent_name: entry}with exactly one entry per real parent of this node. Each entry carriesmessage,metadata,event_ts, and two termination flags:is_stop_signal(that parent ended cleanly) andis_abort(it died). An aborted entry also carriesabort_originandabort_error— the originating node and its error record — so the failure can be reported and relayed downstream rather than degenerating into a hang.
- restore_checkpoint() bytes | None[source]
The last checkpointed state, and the group it covers: a redelivery of that group is acknowledged, not re-applied (
receive_message) — after its committed output, if the checkpoint carries one, is confirmed on the broker.
- resume_offset() int[source]
For a replayable producer: the last accepted source offset in the ledger, so the source resumes from the next one after a restart (
MSGID-6). 0 when nothing was accepted (or the node is live).
- set_output_event_timestamp(value: float) None[source]
Set the event time (epoch seconds) stamped on this node’s next published output (via
RuntimeContext.set_event_timestamp) — when the underlying real-world event was captured. Producers of time-sensitive data (cameras, sensors) should set this; downstream nodes inherit their input group’s event time automatically. Default: no-op.
- set_output_partition_key(value: Any) None[source]
Set the partition key attached to this node’s next published output (via
RuntimeContext.set_partition_key), so a downstream partitioned node can route by a business key. Default: no-op.
- stop_reason() str | None[source]
Which stop
check_for_terminationreports, one of theSTOP_*constants, or None while the flow runs. Default: the control stop whenever a termination was signalled — the only kind a bare messenger has.
- subscription_status() dict[str, Known[Any] | Unknown][source]
Per parent, the backend’s observation of this node’s data subscription —
available,leased,unresolved(retained but exhausted: never “all done”, MSG-012),dropped— orUnknownwhen it could not be read.
- take_drops() dict[str, int][source]
Inputs this messenger gave up on since the last call, by reason, that no caller above the
Messengerseam could have seen: a retry budget exhausted (exhausted), bytes that could not be decoded (undecodable), a join group evicted (join_evicted), a live publication the broker refused (publish_discarded).
videoflow.messaging.obligations module
The runtime’s own obligation ledger (RFC 0006 BLOB-14 step 4): what a
worker derives, at start and periodically, from durable records it already
keeps and from the broker’s own facts — no test-built ledger, no counter.
For every publication a parent of this node made (the parent’s outbox in the run ledger: the payload refs, the reader obligations the put acquired, the accepted stream sequence), the ledger decides which readers still owe a release:
a publication the channel evicted (its sequence is below the channel’s first retained sequence,
observe_channel) will never reach a reader that has not taken it — nobody owes it anything (PAY-012);otherwise a reader owes its obligation while its ack floor on that channel is below the sequence (
observe_ack_floor); a floor at or past it means the reader settled the message, and a release that never happened (a crash after a confirmed ack, PAY-006) is what reconciliation cancels;an
intent/<publication_id>is owed while the publication is unresolved; a definitely refused one owes nothing (PAY-007, PAY-013).
Anything that could not be observed — a floor, a channel — is required: an
unobservable release is not a release. The ledger is authoritative only for
the reader ids it knows and for publishers’ intents; an archive’s or a
dead-letter pin’s obligation (archive/*, dlq/*) is never cancelled by it.
- class videoflow.messaging.obligations.RuntimeObligationLedger(runtime: FlowRuntime, backend: MessagingBackend, channels: Sequence[str])[source]
Bases:
ObligationLedger- Arguments:
runtime: the node’s ledger (its store is shared with its parents’ outboxes).
backend: the transport, for channel retention and ack floors.
- channels: the publishing nodes whose obligations to judge — this node’s
parents, and itself for its own intents.
videoflow.messaging.topology module
Broker topology: naming, JetStream stream/consumer configuration, and up-front provisioning of a flow’s streams and durable consumers.
Everything that decides how a message routes — subject names, stream names, durable names, and the retention/discard policies that give REALTIME vs BATCH their semantics — lives here, so the messenger, the compiler, the manifests, and the provisioning entrypoint all agree on one source of truth.
Naming is scoped by flow_id and run_id so that re-running or
redeploying a flow gets a fresh set of streams instead of colliding with the
previous run’s durables.
Ownership is a separate question from naming, and this module answers it too.
Names are hyphen-joined and hyphens are legal inside every part, so a stream
name alone cannot say where the run ends and the node begins: vf-f-r-x-n is
node x-n of run r and node n of run r-x. Two things make
ownership exact anyway. Every stream and durable is created with owner labels
in its JetStream metadata (videoflow.io/flow-id, run-id, node,
kind, generation; RFC 0006 STREAM-14) that teardown compares
verbatim. For streams that predate the labels, the stream’s own
data subject is the authority: subjects are dot-delimited and sanitize never
lets a dot through, so vf.f.r.x-n and vf.f.r-x.n are distinct strings —
teardown reads the run out of the subject token, never out of a name prefix.
Provisioning also reads every stream and consumer back after creating it and
reports the fields the broker did not honour, because a REALTIME stream the
broker silently kept at INTEREST retention is a flow with the wrong semantics,
not a provisioned one.
- videoflow.messaging.topology.DEFAULT_ITEM_CREDIT = 1
Inputs one replica holds in processing at a time.
- videoflow.messaging.topology.DEFAULT_MAX_ACK_PENDING = 8
The broker-side
max_ack_pendingprovisioning used before RFC 0006 (STREAM-15replaced it withconsumer_credit); kept as the reviewed value the conformance negative controls reproduce.
- videoflow.messaging.topology.DEFAULT_PREFETCH = 4
Deliveries a worker parks locally per subscription beyond the one it is processing, so the next input is already in hand when the current one settles.
- videoflow.messaging.topology.DEFAULT_READ_BACK_TIMEOUT_SECONDS = 30.0
How long
read_back_streamsgives the broker, connect included, before every stream it was asked about is reportedUnknown('timeout').
- videoflow.messaging.topology.DEFAULT_REALTIME_BUFFER = 1
REALTIME keeps only the freshest N messages per node and never blocks the producer (a new publish evicts the oldest). BATCH uses INTEREST retention so acked messages are freed, bounding the backlog to unacked messages; a full stream then rejects new publishes (DiscardPolicy.NEW), which the publisher turns into blocking backpressure instead of silent loss.
- videoflow.messaging.topology.DLQ_RETENTION_SECONDS = 604800
the forensic and replay horizon a dead letter’s payload obligation must cover too (
BLOB-14step 3).- Type:
How long dead letters are retained (
STREAM-8)
- videoflow.messaging.topology.LEGACY_BIND_CREDIT = 6
The
max_ack_pendinga worker bound its durable with before RFC 0006 (its local queue depth plus two); kept for the same reason.
- class videoflow.messaging.topology.VerifiedConsumer(requested: nats.js.api.ConsumerConfig, effective: nats.js.api.ConsumerConfig | None, mismatches: tuple[str, ...] = ())[source]
Bases:
objectVerifiedStream’s counterpart for a durable consumer.- effective: nats.js.api.ConsumerConfig | None
- mismatches: tuple[str, ...] = ()
- requested: nats.js.api.ConsumerConfig
- class videoflow.messaging.topology.VerifiedStream(requested: nats.js.api.StreamConfig, effective: nats.js.api.StreamConfig | None, mismatches: tuple[str, ...] = ())[source]
Bases:
objectWhat the broker holds after
_ensure_stream: the config that was asked for, the config read back (None when the read-back itself failed), and each requested field the two differ on, rendered'field: requested X, effective Y'. Emptymismatchesmeans the broker gave the flow exactly what it asked for.- effective: nats.js.api.StreamConfig | None
- mismatches: tuple[str, ...] = ()
- requested: nats.js.api.StreamConfig
- videoflow.messaging.topology.connect_options_for(timeout: float, fail_fast: bool, error_cb: Callable[[BaseException], Awaitable[None]]) dict[str, Any][source]
nats.connectkeyword arguments for a short-lived observation connection.fail_fast(the operator’s machine) disables reconnects; note that nats-py 2.15.0 still cycles the server pool on a refused connect (nats/aio/client.py_select_next_serverloops until a server answers, andmax_reconnect_attempts = 0never discards one), so the caller bounds the connect withasyncio.wait_forand reads the refusal out oferror_cb— which is also where a server-side permissions violation on a JetStream API subject arrives (client.py_process_err), while the request itself merely times out. Withoutfail_fast(an in-cluster entrypoint whose broker may still be starting) the client’s own retry schedule applies: 60 attempts two seconds apart.
- videoflow.messaging.topology.consumer_config_for(flow_id: str, run_id: str, consumer_node_name: str, parent_node_name: str, ack_wait: int = 60, max_deliver: int = 1, max_ack_pending: int = 8, generation: str | None = None) nats.js.api.ConsumerConfig[source]
Durable pull-consumer config for one (child, parent) edge. Filters to the parent’s data subject so EOS markers (on the
_eossubject of the same stream) are handled by a separate per-replica consumer instead.max_deliveris 1 for REALTIME (no redelivery — freshest wins) andretries + 1for BATCH;max_ack_pendingbounds how many un-acked messages the broker will hand out before it stops delivering (this is the server-side half of prefetch bounding). Under RFC 0006 the durable is labelled with its consuming node as owner.
- videoflow.messaging.topology.consumer_credit(nb_tasks: int, partitioned: bool, item_credit: int = 1, prefetch: int = 4) int[source]
STREAM-15: the broker-sidemax_ack_pendinga subscription needs so every replica can holditem_creditinputs in processing plusprefetchparked, without one replica’s un-acked work starving another’s — a shared (competing) durable multiplies by the replica count, a per-replica (partitioned) durable does not. Provisioning and the worker call this so the durable created and the durable bound agree (MSG-017).
- videoflow.messaging.topology.credit_admits(effective_credit: int, partitioned: bool, item_credit: int = 1, prefetch: int = 4) int[source]
The inverse of
consumer_credit: how many replicas an effectivemax_ack_pending(read back from the broker) lets holditem_creditinputs in processing plusprefetchparked at the same time. A plan that admits more replicas than this is incompatible with the durable as it stands — the surplus replicas start but never hold an input (RUN-024): the credit must be re-derived and updated, not the replicas added. A per-replica (partitioned) durable serves one replica whatever its credit.
- async videoflow.messaging.topology.delete_run_streams(nc: nats.aio.client.Client, flow_id: str, run_id: str, node_names: Iterable[str] | None = None, generation: str | None = None) CleanupObservation[source]
Teardown: delete the streams this run owns — by exact ownership, never by name prefix — and report truthfully what happened.
Ownership, per stream (
_owned_stream): a stream carrying RFC 0006 owner labels is this run’s iff the labels equal(flow_id, run_id)verbatim (andgeneration, when given); an unlabelled stream is this run’s iff its data subject names this run token for token. Neither can mistake runr-xfor runrthe waystartswith('vf-{flow}-{run}-')did, and neither needs the graph:node_names([spec.name for spec in specs]) only narrows the deletion to those nodes’ streams, for a caller tearing down its own provisioning.What is not attributed — and is therefore left standing, and not reported, since nothing on it says whose it is: an unlabelled stream provisioned with custom subjects by something other than this module. Nothing in-tree does that.
The flow’s dead-letter stream is deliberately never one of them. Teardown runs in a
finallyon success, failure, stall and Ctrl-C alike, so deleting the DLQ here destroyed exactly the evidence an operator wants after a failed run. It ages out on its own retention instead, andvideoflow dlq purgeremoves it on purpose. (It carries flow-level labels with no run id, and its subjectvf.{flow}._dlq.>is not a data subject, so neither rule can match it.)- Returns:
CleanupObservation:removedare confirmed gone (a stream that was already gone counts),remainingare owned streams whose delete failed, andcompleteis False whenever anything remains or the inventory could not be read — a listing failure deletes nothing and is not “nothing to delete”. Re-running is safe, and is the remedy.
- videoflow.messaging.topology.dlq_stream_config(flow_id: str, replicas: int = 1) nats.js.api.StreamConfig[source]
- videoflow.messaging.topology.dlq_stream_name(flow_id: str) str[source]
The dead-letter stream, scoped to the flow rather than the run.
Everything else about a run is disposable and is deleted with it; dead letters are the opposite — they are the forensic record of what went wrong, and they are most wanted precisely after a run that failed and was torn down. Run scoping meant
delete_run_streamsdestroyed the evidence on the way out and the stream’s week-long retention never applied to anybody. The run id lives in the subject instead, so entries stay attributable and filterable.
- videoflow.messaging.topology.dlq_subject_filter(flow_id: str, run_id: str | None = None, node_name: str | None = None) str[source]
A subject wildcard selecting dead letters for inspection: the whole flow, one run of it, or one node of one run. Used by
videoflow dlq.
- videoflow.messaging.topology.dlq_subject_for(flow_id: str, run_id: str, node_name: str) str[source]
vf.{flow}._dlq.{run}.{node}— filterable by run, by node, or by both.
- videoflow.messaging.topology.durable_name_for(consumer_node_name: str, parent_node_name: str) str[source]
- videoflow.messaging.topology.eos_anchor_config(flow_id: str, run_id: str, node_name: str, generation: str | None = None) nats.js.api.ConsumerConfig[source]
Provision-time durable on a node’s EOS subject that exists purely to create interest, so an EOS marker is retained by the BATCH (INTEREST-retention) stream no matter when it is published.
The real EOS consumers are per-process (uuid-suffixed) durables created in each worker’s setup — they cannot be pre-provisioned, so without this anchor an EOS published by a fast-finishing parent before a slow-starting child registers its EOS consumer is silently discarded (no interest at publish time), and the child then waits for EOS forever: the flow never terminates. The anchor is never fetched from and never acks, so the marker stays retained for any number of late-created consumers (their default DeliverPolicy.ALL replays it); the run’s stream teardown deletes the anchor with everything else. No
inactive_threshold: it must not be reaped while the run is alive.
- videoflow.messaging.topology.eos_anchor_durable_name_for(node_name: str) str[source]
The provision-time interest anchor on a node’s EOS subject (see eos_anchor_config).
- videoflow.messaging.topology.eos_consumer_config(flow_id: str, run_id: str, consumer_node_name: str, parent_node_name: str, instance_id: str, inactive_threshold: int = 3600, generation: str | None = None) nats.js.api.ConsumerConfig[source]
Per-replica durable pull-consumer for a parent’s EOS subject.
inactive_thresholdlets the server clean it up automatically some time after the flow ends, so per-process (uuid-suffixed) EOS consumers don’t accumulate.
- videoflow.messaging.topology.eos_durable_name_for(consumer_node_name: str, parent_node_name: str, instance_id: str) str[source]
- videoflow.messaging.topology.eos_subject_for(flow_id: str, run_id: str, node_name: str) str[source]
- videoflow.messaging.topology.join_item_credit(parents: Sequence[str], join_policy: dict | None, flow_type: str) int[source]
The per-replica processing credit a node needs: one input group for a single-parent node; for a join, its policy’s working set — every half of every pending group plus the group being assembled (
JoinPolicy.working_set) — because a credit smaller than that lets an adversarial parent ordering fill the durables with halves that can never complete (RUN-005). Provisioning and the bind derive it identically.
- videoflow.messaging.topology.max_deliver_for(flow_type: str, max_retries: int = 3, delivery: dict | None = None, ledger_budget: bool = False) int[source]
Broker-side delivery cap for one node’s durables.
Derived from the node’s effective
DeliveryPolicyrather than from the flow type alone, because delivery is overridable per node: an at-least-once sink in a REALTIME flow needs a cap above 1 or its retries would be silently impossible, and a best-effort node in a BATCH flow should not be retried at all. Provisioning and the messenger both call this so the durable they create and the durable they bind agree.- Arguments:
flow_type: supplies the preset.
max_retries: deployment-level retry count (
VF_MAX_RETRIES).delivery: the node’s own override, as a dict (
NodeSpec.delivery).ledger_budget: the retry budget is enforced by the runtime ledger’s attempt counts (RFC 0006
STREAM-15, decision D11: only with a durable, shared runtime store), so an at-least-once durable is provisioned withmax_deliver = -1— the broker never strands a message; a worker-fatal redelivery never counts. Best-effort durables keep their cap of 1 either way.
- videoflow.messaging.topology.observation_failure(error: BaseException, reported: Sequence[str] = ()) tuple[str, str][source]
(reason, detail)for a failed observation of the broker, in theUnknownvocabulary:auth(credentials refused, or a permissions violation the error callback saw while the request timed out),unreachable(no server answered — a refused connect shows up as a timeout with aConnectionRefusedErrorinreported),timeout(the server is there and slow),malformed(an answer the client could not interpret).
- videoflow.messaging.topology.partitioned_durable_name_for(consumer_node_name: str, parent_node_name: str, replica_id: int) str[source]
- videoflow.messaging.topology.profile_mismatches(profile: str, effective: nats.js.api.StreamConfig) tuple[str, ...][source]
The ways a stream’s effective configuration contradicts a messaging profile — the semantic half of a read-back, beside the field-level
_mismatches. A field can match the request and still not carry the profile (the request asked for the wrong shape), and a stream nobody here provisioned can carry the profile with fields no request ever set (a byte limit instead of a message limit), so the two checks are separate. Each finding reads'<field>: <effective value> <what that means>'; empty means the stream carries the profile.reliable_workneeds retention that keeps a message until it is acknowledged (INTEREST or WORK_QUEUE) andDiscardPolicy.NEWat the limit —OLDon a full stream evicts an unacknowledged message, which is exactly the loss the profile forbids.live_latestneeds LIMITS retention,DiscardPolicy.OLD(the freshest message wins) and a bound on the backlog (max_msgs,max_bytesormax_age), or “latest” is whatever fits on the disk.durable_controlandreplay_archiveare not stream-level promises on JetStream (the planner rejects them before a stream exists), so they have no findings here.
- async videoflow.messaging.topology.provision_flow(nc: nats.aio.client.Client, specs: list[NodeSpec], flow_id: str, run_id: str, flow_type: str, max_retries: int = 3, ack_wait: int = 60, max_ack_pending: int | None = None, generation: str | None = None, replicas: int = 1, ledger_budget: bool = False) None[source]
Idempotently create every stream and durable consumer a flow needs, before any worker publishes. Required for BATCH: under INTEREST retention a message published with no registered consumer interest is discarded immediately, so the durables must exist first. Every resource is read back after creation (
_ensure_stream/_ensure_consumer), so a broker that did not honour a requested field is reported rather than trusted.- Arguments:
nc: a connected
natsclient.specs: list of
videoflow.core.compiler.NodeSpec.max_ack_pending: broker-side credit per durable. None derives it from the consuming node’s replica count and join working set (
consumer_credit, STREAM-15).generation: provisioning generation recorded in the owner labels (RFC 0006), so
delete_run_streamscan be asked to remove only what this provisioning created. None leaves that label out.replicas: stream copies to request (a replicated broker profile’s
jetstream_replicas); read back and reported like every other field.ledger_budget: provision at-least-once durables with
max_deliver = -1because the workers’ runtime ledger enforces the retry budget (max_deliver_for); the workers derive the same from the same store.
- async videoflow.messaging.topology.provision_flow_connect(nats_url: str, specs: list[NodeSpec], flow_id: str, run_id: str, flow_type: str, connect_options: dict[str, Any] | None = None, **kwargs: Any) None[source]
Connects to NATS, provisions, and drains — a self-contained entrypoint.
- Arguments:
connect_options: extra kwargs for
nats.connect. The default (retry forever) is right for the in-cluster provision Job, whose broker may still be starting; a local run passes fail-fast options instead so an unreachable broker reports itself rather than hanging.
- videoflow.messaging.topology.provision_flow_sync(nats_url: str, specs: list[NodeSpec], flow_id: str, run_id: str, flow_type: str, connect_options: dict[str, Any] | None = None, timeout: float | None = None, **kwargs: Any) None[source]
Synchronous wrapper for callers outside an event loop (the local engine, the init entrypoint).
- Arguments:
timeout: overall bound in seconds, or None (the default) to wait indefinitely.
nats.connectretries an unreachable server forever —allow_reconnect/max_reconnect_attemptsonly govern reconnects after a successful connect — so a caller that would rather report the problem than block must set this.
- Raises:
TimeoutErrorwhentimeoutelapses first.
- async videoflow.messaging.topology.quiet_error_cb(_e: BaseException) None[source]
Silences the client’s per-attempt error logging; the read-back reports the failure itself.
- videoflow.messaging.topology.read_back_streams(nats_url: str, flow_id: str, run_id: str, node_names: Sequence[str], flow_type: str, timeout: float = 30.0, replicas: int = 1, generation: str | None = None, fail_fast: bool = True) dict[str, VerifiedStream | Unknown][source]
read_back_streams_asyncover a short-lived connection of its own — for a worker checking its channels before it opens, or the provision entrypoint checking what it just created — so nothing reaches into a messenger’s live connection. A connect that fails withintimeoutreports every node as the sameUnknown(observation_failure); nothing is ever inferred.- Arguments:
node_names: the nodes whose output streams to read.
flow_type / replicas / generation: what provisioning requested, so
VerifiedStream.mismatchesis meaningful.timeout: overall bound, connect included.
fail_fast: no reconnects (the operator’s machine, a worker whose broker is already up); False lets the client retry a broker that is still starting, as the in-cluster provision Job does.
- async videoflow.messaging.topology.read_back_streams_async(js: nats.js.JetStreamContext, flow_id: str, run_id: str, node_names: Sequence[str], flow_type: str, replicas: int = 1, generation: str | None = None, reported: Sequence[str] = ()) dict[str, VerifiedStream | Unknown][source]
Read the streams of the named nodes back from a JetStream context, keyed by node. Each entry is a
VerifiedStreamwhoserequestedis what provisioning asks for under this flow type (stream_config_for) and whosemismatchesare the requested fields the broker does not honour, or anUnknownsaying why nothing can be said:missing(the stream does not exist — a definite observation, kept apart from the unobservable ones), ortimeout/auth/unreachable/api/malformed(observation_failure).stream_inforeturns the applied configuration (nats-py 2.15.0,nats/js/manager.pystream_info->api.StreamInfo.config), and a stream that does not exist isnats.js.errors.NotFoundError(err_code 10059).
- videoflow.messaging.topology.stream_config_for(flow_id: str, run_id: str, node_name: str, flow_type: str, subjects: list[str] | None = None, realtime_buffer: int = 1, batch_max_msgs: int = 10000, generation: str | None = None, replicas: int = 1) nats.js.api.StreamConfig[source]
- Arguments:
generation: provisioning generation recorded in the owner labels (RFC 0006); None leaves that label out.
replicas: copies the stream keeps (a replicated broker profile’s
jetstream_replicas); 1 leaves the field to the server default, so a single-server request is exactly what it always was.
- videoflow.messaging.topology.stream_label_selector(flow_id: str, run_id: str) str[source]
Prefix shared by every stream of one run. Diagnostic only: it is also the prefix of every stream of run
{run}-x, sodelete_run_streamsnever selects on it — ownership comes from owner metadata or from the subject tokens (subject_owner). Enumerated bybackends.identity.derived_names.
- videoflow.messaging.topology.stream_name_for(flow_id: str, run_id: str, node_name: str) str[source]
- videoflow.messaging.topology.subject_owner(subject: str) tuple[str, str, str] | None[source]
The
(flow, run, node)tokens a data or EOS subject encodes — sanitized, exactly assubject_for/eos_subject_forwrote them — or None for any other subject (control, DLQ, wildcards, foreign).This is the one place a run boundary is exact without metadata: subject tokens are dot-delimited and
sanitizenever lets a dot through, sovf.f.r.x-n(runr, nodex-n) andvf.f.r-x.n(runr-x, noden) are different strings although both hyphen-join to the stream namevf-f-r-x-n.delete_run_streamsattributes streams that predate owner labels with it.
- videoflow.messaging.topology.verify_channel_profiles(read_back: Mapping[str, VerifiedStream | Unknown], requests: Sequence[ProfileRequest], *, unknown_is_fatal: bool, where: str, config_mismatches: bool = False) None[source]
Bind explicit profile requests to what the broker actually holds. For every request whose channel was read back: a stream whose effective configuration contradicts the profile (
profile_mismatches) is a definite rejection; a stream that does not exist is a broker problem (provisioning has not run, or ran against another run id); a stream that could not be observed is fatal only whenunknown_is_fatal— an explicit request must not pass on an unread guarantee — and a logged warning otherwise. Requests for channels the caller did not read back are not this caller’s to judge.- Arguments:
config_mismatches: also count the field-level
VerifiedStream.mismatches(anum_replicasbelow the requested count, a clamped limit). The provision entrypoint knows the full request and passes True; a worker, which does not know the replica count asked for, judges the profile only.where:
provision/worker <node>, for the message.
- Raises:
IncompatibleProfile: a stream definitely does not carry its requested profile.
BrokerUnavailable: a requested channel’s stream does not exist.
UnobservableState: a requested channel could not be read back and
unknown_is_fatal.