Skip to content

State Lifecycle and Limits

iliyan85 edited this page Jul 31, 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 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, multipart TAG contexts, and processing configuration.
  • 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.

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.

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 statistics are reference and regression-test surfaces; AISMixer does not currently expose them as a complete metrics endpoint.

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, the processor stage captures one routing snapshot for each accepted IngressFrame. When its table is active, orchestration calls match_target_ids(frame.source_id) once and constructs an immutable ProcessingSnapshot containing the routing generation, per-target deduplication mode, and ordered numeric egress IDs. 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 frame uses the same target-only processing view. An update during that frame affects the next accepted frame, not work already using the captured routing-state snapshot. Older immutable routing snapshots remain safe for existing readers, but RoutingState owns only the current snapshot and does not maintain 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;
  • 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, and Runtime Routing Control.

Clone this wiki locally