Repository navigation
feat: Retry event delivery and flush on close - #74
flagsmithsmith[bot] wants to merge 4 commits into
Conversation
Its entry in Flagsmith/sdk-spec's compatibility matrix at v0.9.0: `core events local-evaluation`.
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
Implements the SDK spec v0.9.0 for events: - Event batches are sent up to 3 times, giving up on each attempt after 3 seconds and backing off from 1 second, doubling, with full jitter. Batches the events API rejects are dropped, as are those whose last attempt fails with a 500; others are kept for the next flush, ahead of events tracked since. A 401 or 403 stops event tracking. - A full buffer (1000 events) drops its oldest event. - `Flagsmith.Client.flush_events/1` sends the tracked events and returns once they have been delivered, dropped or kept. - `Flagsmith.Client.close/2` sends the buffered events within a timeout, and stops the client's events processor and poller. - Local evaluation now reads experiment metadata and variant keys from the environment document, so experiment flags evaluated locally carry their experiment and variant, and send exposures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Implemented SDK spec v0.9.0 in b6b19ed. The spec run was failing because the SDK had no steps (and no SDK behaviour) for event delivery retries, flushing, closing the client, or a client with both local evaluation and events. Event delivery (
New public API (
Local evaluation of experiment flags (needed by the new
Steps ( Tests: unit tests cover retries and the 3 s attempt timeout, kept/dropped batches (400, 500 last attempt, 503 last attempt), 401/403, full-buffer dropping, close (sends the buffer, and returns within its timeout), and locally evaluated experiment exposures. Locally I ran |
Zaimwa9
left a comment
There was a problem hiding this comment.
Requesting changes. Spec passes, but a flush already running keeps sending after close/2 times out.
| try do | ||
| GenServer.call(pid, {:close, deadline}, timeout) | ||
| catch | ||
| # The processor is still sending a batch: it stops once done, past the deadline. | ||
| :exit, {:timeout, _} -> | ||
| Logger.warning("Gave up sending events after #{timeout} milliseconds while closing") | ||
| {:error, :timeout} |
There was a problem hiding this comment.
If close/2 times out while a flush is retrying, that flush keeps sending for up to ~12 s. Kill the processor on timeout, or run sends in a cancellable task.
There was a problem hiding this comment.
Fixed in f81d3f2. I went with killing the processor on timeout.
- When the
{:close, deadline}call times out,close/2kills the processor and waits for its:DOWNbefore returning{:error, :timeout}. It has to be:killbecause the processor traps exits, so a:shutdownwould just sit in its mailbox behind the running flush. The kill also takes down the request task thatTesla.Middleware.Timeoutlinks to it, so no further attempt or backoff runs. - The processor child is now
restart: :temporary(it was:transient), so the supervisor doesn't bring a killed processor back.track/2already starts a processor when none is registered, so tracking after a close or a crash still works. The only difference is that a crashed processor's flush timer now restarts with the next tracked event. The crashed processor's buffer was lost either way, so nothing that could be sent is left waiting. - Known side effect:
DynamicSupervisorlogs a:killedchild termination report when this happens, next to our own "Gave up sending events after … while closing" warning. It only happens when a close times out.
New test: "closing the client stops a flush still sending when its timeout expires". It starts a flush that the mocked events API keeps failing slowly, then calls close(config, 50) mid-retry. It asserts {:error, :timeout}, that the processor is dead and unregistered, and that no further attempt is made in the next 500 ms. It passed 10 out of 10 runs.
I left out the cancellable-task alternative. It would keep the processor alive, but flush callers would then need deferred replies, and events tracked during a send would need queueing behind the batch in flight. That's much more code for the same result here, since a closed processor isn't reused.
`close/2` stopped waiting when its timeout expired, but a flush already running kept retrying for up to ~12 seconds. The processor is now killed on timeout, and is started as a temporary child so that its supervisor doesn't restart it; tracking an event starts a new one, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addressed in f81d3f2 (details in the inline reply). When Checks run locally:
One more thing: |
Contributes to Flagsmith/sdk-spec#23