Skip to content

State Lifecycle and Limits

iliyan85 edited this page Jul 26, 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 the deduplication, multipart assembly, and secure-ingress state covered by Campaign B. 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 target_id 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 Station ID, transmitted timestamp, and signature; peer address is excluded Local monotonic clock 60 seconds 100,000 records No Expiry, capacity eviction No
Secure sessions Module-wide SecureState Exact peer socket address Local monotonic clock 300 seconds since last valid activity 100,000 sessions Invalid or replayed traffic: no; valid keepalive, ping, or NMEA activity: yes Expiry, least-recently-active capacity eviction, same-address replacement No
Data nonces Per-session set owned through SecureState Exact 12-byte nonce within one session Local monotonic clock 300 seconds 100,000 per session No Expiry, capacity eviction, owning-session discard 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, sessions, per-session nonces, and their statistics.
  • 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, session creation and last-seen time, 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.

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;
  • 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;
  • a valid matching UDPSEC keepalive, or a fully validated ping or NMEA packet, updates session last-seen time and least-recently-active order.

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 data-nonce sets evict their oldest live record;
  • secure sessions evict the least-recently-active live session.

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;
  • session state distinguishes expiry, capacity eviction, and replacement;
  • nonce state distinguishes its own expiry or capacity eviction from discard with the owning session.

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 and supplies the corresponding immutable ProcessingSnapshot to the processor. PythonDataPlaneProcessor matches frame.source_id once only when that table is active, and every accepted sentence from the frame uses the same result. An update during that frame affects the next accepted frame, not work already using the captured table. Older immutable 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/session/nonce state, and runtime routing changes;
  • independent processes do not share state;
  • no multiprocessing coordinator currently distributes routing generations or processing state;
  • runtime routing replacement does not rewrite config.yaml;
  • secure 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