TTL-based background sync
Every getter (getString, getBoolean, getInt, getLong, getDouble, getJson, getFlag, findFlag, getSecret, getSecretFilePath) runs a freshness check before reading the snapshot:
- If the client is closed, return - nothing to refresh.
- If a billing back-off window is in force, return without syncing (see Billing).
- If a quarantine is in force, return without syncing (see Quarantine).
- If
now - snapshot.syncedAt < ttl, the snapshot is fresh - return. - Otherwise, launch a coalesced background sync. The getter returns the current value without waiting.
- Getters are always non-blocking, and never throw because of a network problem.
- The first call after TTL expiry returns a slightly stale value while the refresh runs.
- A later call, once the refresh has landed, returns the fresh value.
Coalescing and ordering
One sync applies at a time, and background refreshes coalesce.- Coalescing. A burst of stale reads, or a burst of stream events, produces one sync plus at most one follow-up run for whatever arrived while the first was in flight. Without this, every read past the TTL started its own request - the snapshot’s timestamp only advances once a sync finishes, so each read saw the same stale stamp.
- Ordering. Applying the snapshot is serialised, so two responses can never land out of order and leave the older one in the cache.
sync() participates in the same serialisation, so it waits behind an in-flight refresh rather than racing it.
ETag / 304 support
Every sync sends the last receivedETag in an If-None-Match header. When nothing has changed the server returns 304 Not Modified with an empty body: the SDK skips deserialisation, keeps the existing snapshot, and resets its freshness stamp.
That last part is what makes 304 cheap. A snapshot whose stamp did not advance would be permanently overdue, so every read past the TTL would issue another request - for a service whose data never changes, which is precisely the case the conditional request exists to make cheap.
Stream path bypasses the TTL
WhenconnectStream() is active and the server pushes an event, a sync runs immediately - the TTL is ignored. The snapshot is updated within milliseconds of a change on the platform, regardless of ttl.
For production services with the stream active, a large ttl (120 seconds or more) is fine. The stream handles freshness; the TTL is the safety net for when the stream is in fallback.
Quarantine (429 with a quarantine body)
When a sync receives HTTP 429 with a quarantine body, the SDK:- Clamps the
expires_atdeadline to at most 24 hours from now, and collapses a deadline in the past to now. - Records the clamped deadline.
- Calls
observer.onQuarantined(reason, expiresAt)with the clamped value. - Throws
NexusQuarantinedExceptioncarrying the clamped value.
Billing (402)
When a sync receives HTTP 402, the SDK:- Backs the background refresh off for five minutes.
- Calls
observer.onBillingOverdue(). - Throws
NexusBillingException.
Manual sync
NexusClosedException on a closed client.
Snapshot atomicity
The snapshot is held in anAtomicReference. Reads are lock-free. A new snapshot is constructed from the full sync response and swapped in atomically, so there is no window in which a getter could read a mix of old and new values.
syncedAt() returns the instant the snapshot was last confirmed current - by a 200 OK or a 304.
Bounded responses
Response bodies are bounded while reading, so an over-large response is never buffered in full and never reaches the parser as a truncated document:Content-Length is checked first when the server declares one. Exceeding a cap raises NexusResponseTooLargeException.
File secrets
Secrets of typefile are materialised to disk at sync time; getSecretFilePath(key) returns the path.
- The file is
0600from the moment it exists - the permissions are a creation attribute, not achmodafterwards, because a chmod-after leaves a window at the process umask. - Writes are staged under an unguessable
CREATE_NEWname and atomically renamed over the target, so a reader holding the path sees either the old file or the new one, and the rename replaces any pre-existing file rather than writing through it. - The containing directory is per client, unguessably named,
0700, and created on first use - a client with no file-type secrets leaves nothing on disk. - The file name is a SHA-256 hash of both the key and the value, because the path reaches application logs, environment variables and process listings.
- A secret that disappears from the snapshot has its file removed on that sync.
close()removes the files and the directory; a JVM shutdown hook does the same if the application exits without callingclose().
