-
Notifications
You must be signed in to change notification settings - Fork 0
State Lifecycle and Limits
Process-local TTL state is live while
age < ttland expires whenage >= 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.
| 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.
Each state domain has one owner:
- The long-lived
PythonDataPlaneProcessorowns its process-localDeduplicator,AIVDMAssembler, multipart TAG contexts, and processing configuration. - Within that processor,
Deduplicatoris authoritative for retained logical-message observations. - Within that processor,
AIVDMAssembleris authoritative for fragment groups and their unique-progress time. -
PythonDataPlaneProcessorseparately owns multipart output metadata associated with those groups. -
SecureStateowns replay records, independent pending and active session stores, each session's private nonces, promotion, and their statistics. -
Forwarderowns the immutable dense zero-based numeric egress registry, including IDs for unnamed legacy destinations. -
RoutingStateowns 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.
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.
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.
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.
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
AssemblyKeyas 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.
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.
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.
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.
All state in the table is in memory:
- one long-lived
PythonDataPlaneProcessorowns 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.
- Quick Start
- Installation and Operations
- OpenWrt Deployment
- Configuration and Examples
- Inputs and Outputs
- Architecture Overview
- Data Representation, Processor, and Runtime Boundary
- Processing and Routing Model
- State, Lifecycle, and Limits
- Multipart NMEA Assembly
- TAG Handling
- Deduplication
- Routing Engine
- Behavioural Contract
- Native-Ready Reference Foundation