Skip to content

State Lifecycle and Limits

iliyan85 edited this page Aug 23, 2026 · 7 revisions

State, Lifecycle, and Limits

Process-local TTL state is live while age < ttl and expires when age >= ttl.

This is the common boundary for deduplication, multipart assembly, and current secure-ingress state. The canonical behavioural contract remains normative for exact observable edge cases; this page explains how the owners fit together in the running Python service.

Ownership at a Glance

State Authoritative owner Identity Clock TTL Capacity Duplicate refresh Removal reasons Durable
Deduplication entries One process-local Deduplicator, owned by PythonDataPlaneProcessor (scope, logical key); the scope is global or a process-local numeric EgressTargetId Local monotonic clock 30 seconds by default Optional max_entries; current service wiring uses None No Expiry, capacity eviction, reset No
Multipart assembly groups One process-local AIVDMAssembler, owned by PythonDataPlaneProcessor (source_identity, sequential_id, channel, declared_total); the processor passes frame.assembler_key as source_identity Local monotonic clock 1.0 second by default Optional max_pending_groups; current service wiring uses None Exact duplicate: no; unique progress: yes Expiry, conflict, completion, capacity eviction, reset No
Multipart fragment declarations AIVDMAssembler admission policy Declared total on the incoming sentence None before admission Not applicable Optional max_fragments_per_group; current service wiring uses None Not applicable Rejected as LIMIT_EXCEEDED before group state is touched No
Multipart TAG s, c, and g context PythonDataPlaneProcessor Exact assembler AssemblyKey No independent clock Follows the owning assembler generation No independent capacity; current assembler group capacity is unbounded Metadata on an exact duplicate may refine context without refreshing assembly TTL Assembler-reported conflict, expiry, or capacity removal; completion; reset keys if reset is invoked No
Handshake replay records Module-wide SecureState Exactly build_handshake_replay_key(client_auth_digest, client_signature); peer address is excluded Local monotonic clock 60 seconds 100,000 records No Expiry, capacity eviction No
Pending secure sessions Module-wide SecureState Exact complete peer socket address Local monotonic clock 30 seconds from creation 100,000 pending sessions No; pending traffic does not refresh lifetime Expiry, oldest-live capacity eviction, same-address pending replacement, promotion No
Active secure sessions Module-wide SecureState Exact complete peer socket address Local monotonic clock 300 seconds since last valid activity 100,000 active sessions Invalid or replayed traffic: no; fully validated encrypted ping or NMEA activity: yes Expiry, least-recently-active capacity eviction, same-address replacement at promotion No
Data nonces Private per-owner set through SecureState Exact 12-byte nonce within one pending or active session Local monotonic clock 300 seconds from admission 100,000 per pending or active session No Expiry, per-owner capacity eviction, owning-session discard; pending set transfers on promotion No
Routing snapshot and generation RoutingState Current immutable RoutingTable or None, plus an integer generation No clock None One current snapshot Not applicable Whole-snapshot replacement or disable No

max_fragments_per_group, max_pending_groups, and deduplication max_entries are Python constructor capabilities. They are not aismixer YAML settings. The current service's PythonDataPlaneProcessor constructs its assembler and deduplicator with their None capacity defaults, so those retained-state dimensions are not capacity-bounded in normal runtime wiring. Secure-state capacities are hard production defaults, but the secure state is still process-local and non-durable.

One Authoritative Owner

Each state domain has one owner:

  • The long-lived PythonDataPlaneProcessor owns its process-local Deduplicator, AIVDMAssembler, SourceState, multipart TAG contexts, processing configuration and helpers, and processor counters.
  • Within that processor, Deduplicator is authoritative for retained logical-message observations.
  • Within that processor, AIVDMAssembler is authoritative for fragment groups and their unique-progress time.
  • PythonDataPlaneProcessor separately owns multipart output metadata associated with those groups.
  • SecureState owns replay records, independent pending and active session stores, each session's private nonces, promotion, and their statistics.
  • Forwarder owns the immutable dense zero-based numeric egress registry, including IDs for unnamed legacy destinations.
  • RoutingState owns the active routing snapshot and generation number.

Production constructs exactly one PythonDataPlaneProcessor inside each main() invocation and gives it to one serial processor-stage consumer. There is no import-time global production processor. Mutable components injected into that instance become lifecycle-owned by it; callers must not share or reset them independently.

Auxiliary queues and ordering indexes support expiry or deterministic eviction; they do not define a second state model. In particular, the assembler does not own output TAG s, c, or g. It reports discarded AssemblyKey values, and PythonDataPlaneProcessor consumes those keys to keep its metadata context aligned.

Campaign C did not add a second assembler lifecycle. Production ParsedSentence values enter through feed_parsed_outcome(), while the legacy string feed_outcome() API remains valid; both converge on the same state transitions. The Python assembler still materializes and stores sentence strings.

Processor Reset Boundary

PythonDataPlaneProcessor.reset() is a synchronous lifecycle operation. The processor owner must serialize it with process(); the processor adds no locking or asynchronous start, stop, close, or worker lifecycle.

A successful reset clears retained state in this defined order:

  1. assembler pending groups;
  2. deduplication entries;
  3. SourceState entries;
  4. multipart TAG s contexts;
  5. multipart TAG c contexts; and
  6. multipart TAG g contexts.

It returns an immutable report containing the discarded counts. Reset retains the processor and owned-component identities, processing configuration, injected helpers, TTLs and capacity settings, and cumulative processor and component metrics. It does not drain runtime queues, alter routing or forwarder state, clear other runtime owners' counters, or replace the processor.

Reset is ordered, fail-fast, and non-transactional. If a step raises, later owners are not reset and earlier completed changes are not rolled back. The current control protocol and aismixerctl do not expose reset. Routing replacement and normal shutdown do not invoke it. Exact report fields and failure accounting remain in the canonical behavioural contract.

Clock Ownership

Deduplication and multipart assembly use injected local monotonic clocks, with time.monotonic as the default. Their runtime instances use those defaults. Wall-clock adjustments therefore do not extend or revive these TTL entries.

UDPSEC separates two clock domains:

  • Wall time is used for the externally meaningful handshake freshness check, pong timestamps, and diagnostics. The handshake window accepts a difference of exactly 30 seconds and rejects a larger difference.
  • Monotonic time owns replay TTL, pending creation and TTL, active creation and last-seen time, active TTL, nonce TTL, and local capacity ordering.

Ingress network policy is applied before secure-state clock observation or mutation. Every allowed UDPSEC packet uses one monotonic observation for its local lifecycle decisions.

Routing snapshots and processor-owned TAG contexts have no independent clock. TAG context follows assembler lifecycle notifications.

Pending-To-Active Promotion

ServerHello installs address-bound pending state with authenticated station identity, separate C2S and S2C AES-GCM owners, creation time, and a private nonce set. It leaves any active session at that address intact. Pending failure, timeout, expiry, same-address pending replacement, or pending capacity eviction also leaves that active state intact.

Promotion requires a DATA packet decryptable by the pending C2S owner and a strict sequence-zero confirmation ping for the authenticated station. Its nonce is admitted before promotion. Promotion transfers both directional owners and the nonce set, removes the pending entry, and replaces any same-address active entry as one transition. The response is a sequence-zero pong encrypted by the promoted S2C owner. Ordinary active ping sequences are built-in integers greater than zero.

Duplicate Non-Refresh

Rejecting or observing an already-live identity does not normally make it live for longer:

  • a deduplication hit retains its original insertion time;
  • an exact multipart fragment duplicate does not refresh group progress time;
  • a repeated handshake replay key does not refresh its expiry;
  • pending-session traffic does not refresh its fixed creation-based lifetime;
  • a repeated data nonce does not refresh its expiry.

The intended refresh cases are different:

  • a new unique multipart ordinal updates the group progress time;
  • promotion starts active-session activity, and a fully validated encrypted active ping or NMEA packet updates active last-seen time and least-recently-active order.

UDPSEC liveness is encrypted ping/pong only; unencrypted control traffic does not refresh secure state.

An exact multipart duplicate may still carry a lower valid TAG c, a new TAG g observation, or eligible TAG s metadata. That metadata can refine processor-owned context without refreshing the assembler lifetime.

Expiry Before Live Eviction

When bounded state needs capacity, expired state is removed before a live victim is selected:

  • deduplication evicts the oldest currently live insertion;
  • multipart assembly evicts the least-recently-progressed group, using AssemblyKey as a deterministic timestamp tie-break;
  • handshake replay and each pending or active data-nonce set evict their oldest live record;
  • pending sessions evict the oldest live pending entry;
  • active sessions evict the least-recently-active live entry during promotion.

These policies apply only when the corresponding capacity is enabled. A live duplicate is rejected without causing a capacity eviction.

Cleanup is operation-driven, not a guarantee of immediate physical removal at the instant wall time passes. The logical expiry boundary is still exact: an entry at age == ttl is not live.

One Removal, One Reason

Lifecycle accounting keeps removal reasons distinct:

  • deduplication distinguishes expiry, capacity eviction, and reset;
  • assembly distinguishes expiry, conflict, completion, capacity eviction, and reset;
  • pending state distinguishes expiry, capacity eviction, same-address replacement, and promotion;
  • active state distinguishes expiry, capacity eviction, and replacement during promotion;
  • nonce state distinguishes its own expiry or capacity eviction from discard with the owning pending or active session. Promotion transfers the pending nonce owner into active state rather than discarding it.

Processor-owned TAG context has no separate statistics. It is removed because its assembler generation was discarded, or consumed when that generation completes. PythonDataPlaneProcessor clears every assembler-reported discarded key before it observes metadata on the current arrival, preventing stale context from being attached to a fresh generation.

Immutable Statistics

Deduplicator.stats(), AIVDMAssembler.stats(), and SecureState.stats() return frozen point-in-time snapshots.

The shared guarantees are:

  • reading statistics does not observe a clock;
  • reading statistics performs no cleanup;
  • cumulative counters remain cumulative across ordinary removals;
  • current and peak sizes are reported where applicable;
  • an earlier snapshot does not change when live state changes later.

Assembler reset additionally counts the number of groups it discarded. Deduplicator reset clears retained keys and increments the reset-call counter while preserving its other cumulative counters and peak. These detailed component-state statistics are reference and regression-test surfaces; the runtime statistics methods do not expose them as a complete lifecycle-state endpoint. Campaign F added separate queue, processor, egress-operation, and traffic metrics. See Runtime Observability.

Routing Snapshot Lifecycle

RoutingState starts at generation 0, whether its initial table is enabled or None. Reading a snapshot or status does not change that generation.

Every successful replace or disable operation installs a whole new immutable snapshot and increments the generation by exactly one. This includes replacing with equivalent content or disabling an already-disabled table. Callers may supply expected_generation; a stale value rejects the operation without changing the current snapshot.

After direct frames and valid legacy IngressEvent values cross the compatibility adapter, each fan-in reader waits for bounded processing capacity. A frame waiting for that permit has no routing snapshot. Once the permit is available, admission reads the current RoutingState, calls match_target_ids(frame.source_id) once when its table is active, constructs an immutable ProcessingSnapshot, and binds it with the exact frame into one ProcessingWorkItem without an intervening await. When routing is disabled, the processing snapshot selects global deduplication and carries all numeric IDs in the Forwarder registry. ProcessingSnapshot contains no RoutingTable and is not the compiled routing program.

Every accepted sentence from the bound frame uses the same target-only processing view. A routing update can affect a frame still waiting for capacity, but not an admitted work item. Older immutable routing snapshots remain safe for existing readers, while RoutingState owns only the current snapshot and keeps no durable history.

Routing state has no TTL, capacity eviction, or lifecycle-statistics object. Runtime changes are neither written back to static configuration nor shared with another process.

Process-Local Scope

All state in the table is in memory:

  • one long-lived PythonDataPlaneProcessor owns its processing state for the lifetime of the current runtime process; that state is not shared with another process;
  • service restart loses deduplication history, pending assemblies, multipart metadata, secure replay, pending, active, and nonce state, and runtime routing changes;
  • independent processes do not share state;
  • numeric egress target IDs are process-local registry positions, not stable external identifiers or operator configuration values;
  • no multiprocessing coordinator currently distributes routing generations or processing state;
  • runtime routing replacement does not rewrite config.yaml;
  • queue, processor, egress-operation, and traffic counters are in-memory lifetime values for current runtime component instances and restart from zero with a new service process;
  • secure pending and active cleanup is driven by allowed traffic rather than a background cleanup timer.

This model makes current Python behavior deterministic and testable. It does not provide persistence, restoration, or distributed coordination.

See also Deduplication, Multipart NMEA Assembly, TAG Handling, UDPSEC Security and State, Runtime Routing Control, and Runtime Observability.

Clone this wiki locally