Files
opendht/doc/proxy-sync.md
T
Adrien Beraud 57a3b658ae proxy: reconcile revisioned listener snapshots
Serve atomic cached snapshots after listener readiness instead of independent GETs. Validate unchanged tokens without downloads, apply contiguous expirations locally, reject malformed revisions and stale generations, bound subscription requests and recovery backoff, and keep wrong-session pushes isolated. Preserve legacy payload handling, document transport and protocol behavior, and include implementation-dependent regressions.
2026-10-07 22:25:19 -04:00

5.8 KiB

Push Subscription Synchronization

Revision-aware clients require an updated proxy. Old clients remain supported: requests without sync retain the original subscription response format.

Validation and Snapshots

A SUBSCRIBE /key/<hash> request contains the existing push registration fields and a sync object. An empty object requests a full snapshot. A synchronized client instead supplies its last reconciled token:

{"sync":{"epoch":"<listener-epoch>","revision":7}}

The epoch is unique to the server-side listener. New and restored listeners get fresh epochs. Each addition, edit, or expiration reported by the DHT listener increments its revision. Epoch/revision state is not restored from disk.

When the token matches, the response contains only a completion record. Otherwise, the response contains newline-delimited value objects followed by this record:

{"sync":{"epoch":"<listener-epoch>","revision":8,"snapshot":true,"complete":true}}

The listener retains its current values, including edits and expirations, under the same mutex as its revision. The server copies both atomically; it does not run a GET for a synchronization request. New listeners wait for initial readiness before responding. snapshot:false means validation found the client's cache unchanged. Successful responses always have complete:true; initial search failure returns HTTP 503. The deprecated retry field is not used. Every subscription request renews the lease, independently of whether values are sent.

Clients buffer snapshot values and reconcile only after a valid completion record and successful HTTP framing. Reconciliation delivers new values and higher-sequence edits, suppresses duplicates and older versions, and expires cached IDs absent from the snapshot. Failed or invalidated responses never remove cached values. Snapshot deletion is authoritative and does not depend on wall-clock timestamps.

Push Ordering and Recovery

Value pushes include string fields epoch and revision. A push no newer than the reconciled revision is ignored. Revisions must be unsigned decimal integers without signs, whitespace, trailing characters, or overflow. A contiguous expiration revision removes the listed IDs locally without a download. The expired:"true" field identifies batched expirations using ids; the original single-value exp field remains supported. Other same-epoch changes and revision gaps require a cached snapshot. Unknown epochs first validate against the proxy, so delayed pushes from old epochs do not force a value download. Revisions cover edits as well as additions and deletions.

Clients allow one sync request per listener and coalesce overlapping triggers. Request generations reject callbacks from superseded requests. Completion state changes run under the listener's owning lock after checking the generation. Same-epoch push observations can invalidate an older snapshot. Confirmed retired epochs are remembered in a bounded, push-token-scoped set; other unknown epochs are validated without changing cached values. Returning to an earlier push token remains valid. Token rotation during a pending request is applied after that request completes. Pushes for another client session return IgnoredWrongSession without changing subscriptions, cached values, or connectivity state. Their application-level purpose is to wake the mobile app; session creation and desired subscriptions are independent actions. A new session requests its own full initial snapshot and does not inherit synchronization state from the rejected push.

Connectivity changes and existing lease refreshes validate the last reconciled token; unchanged data is not downloaded again. Successful lease renewal alone does not clear pending recovery. Failed recovery retries use the existing lease timer with exponential delays from one to sixty seconds; successful recovery restores the normal lease schedule. Stale and malformed responses also use this backoff, rather than retrying immediately. Each subscription request has a 30-second steady-clock deadline covering DNS, connection, and response body receipt. Expired requests release the in-flight slot and enter recovery. No healthy-state polling timer is added.

Without another event or polling, a silently dropped final push cannot be detected immediately. The next connectivity validation or existing lease refresh detects a revision mismatch. Guarantees concern reconciliation with the proxy's observed DHT state, not linearizable global DHT storage.

Listener Readiness

The existing four-argument listen API remains available. The new overload adds a one-shot DoneCallbackSimple as its fifth argument:

runner.listen(key, valuesCallback, {}, {}, readyCallback);

readyCallback(true) follows delivery of initially observed values and completed search/listener synchronization for each enabled address family. Isolated families with no peers are ready after delivering local values. Initial search or registration failure reports false; cancellation does not promise a callback. This is an initial completion notification, not a global consistency guarantee. It does not start a GET or add polling. Attaching to a synchronized cache delivers cached values before notifying readiness.

Streaming proxy clients request /key/<hash>/listen?sync=1. Updated proxies send {"sync":{"complete":true}} after initial values, or complete:false on initial failure. Requests without this opt-in retain the original stream format.

HTTP Transport Note

HTTP requests now report status code zero for transport errors and cancellation, even if a 200 status was received before the failure. Cancellation also sets aborted. EOF succeeds only when the HTTP parser confirms complete framing; truncated content-length and chunked responses fail. Callers must not treat an earlier status callback as proof that a response body completed successfully.