Skip to content

feat: Retry event delivery and flush on close - #129

Merged
Zaimwa9 merged 6 commits into
mainfrom
openhands/sdk-spec-v0.9.0
Nov 13, 2026
Merged

Zaimwa9 merged 6 commits into
mainfrom
openhands/sdk-spec-v0.9.0

Conversation

@flagsmithsmith

Copy link
Copy Markdown
Contributor

Contributes to Flagsmith/sdk-spec#23

renovate Bot and others added 2 commits November 12, 2026 09:37
Its entry in Flagsmith/sdk-spec's compatibility matrix at v0.9.0: `core events local-evaluation`.
@flagsmithsmith flagsmithsmith Bot added the sdk-spec Bumps of the Flagsmith SDK behavioural spec label Nov 12, 2026
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 5c5b6ea1-b767-4ffe-9c98-ae9dc51fdfd4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@codspeed

codspeed Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing openhands/sdk-spec-v0.9.0 (ab90211) with main (e968b8c)

Open in CodSpeed

Implement SDK spec v0.9.0:

- Send a batch of events up to 3 times, one batch at a time, backing off
  from 1 second with full jitter, with a 3 second request timeout. Keep a
  batch whose last attempt failed transiently for the next flush, unless
  that attempt got a 500; drop a batch the events API rejects; stop event
  tracking when it refuses the environment key (401/403). A full buffer
  drops its oldest event.
- Add `Client#flush_events`, which returns once the tracked events are
  delivered, dropped or kept, and `Client#close(timeout_seconds:)`, which
  stops background work and sends the buffered events within the timeout.
- Record exposures under local evaluation: carry the environment
  document's `metadata.experiment` and multivariate option keys through to
  identity flags' `experiment` and `variant`.
- Wire the new spec steps in sdk-spec/steps.rb.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

Implemented SDK spec v0.9.0 in 4b281a4. CI had failed because the SDK was missing 4 steps: the local evaluation + events client, flushing events, closing the client, and tracking a numbered range of events. Behind those steps were 2 missing SDK features.

Event delivery (Flagsmith::EventProcessor)

  • Replaced Faraday's single retry with explicit delivery. A batch is sent up to 3 times, one batch at a time, with a 3 s request timeout. Retries back off from 1 s, doubling, with full jitter.
  • 5xx, 408, 429 and connection errors are retried. If the last attempt fails transiently, the batch is kept for the next flush, ahead of newer events. The exception is a 500 on the last attempt: that batch is dropped, so it is not resent forever. Other 4xx responses drop the batch. 401/403 stops event tracking.
  • When the buffer is full, the oldest event is dropped. A flush started by a full buffer no longer sends again events that a concurrent flush already attempted.
  • stop(timeout_seconds) stops the interval thread without killing a batch in flight. It sends the buffered events, waits at most the timeout, then cancels any pending retry.

Client

  • New Client#flush_events: returns once the tracked events are delivered, dropped or kept, retries included.
  • New Client#close(timeout_seconds: 5): stops polling and realtime, then the event processor.

Exposures under local evaluation

  • The environment document's feature states carry metadata.experiment, and multivariate options carry key. The SDK used to drop both.
  • Both are now mapped into the evaluation context. Identity flags from the document get experiment and variant, so get_experiment_flag records exposures, including control.
  • Environment flags carry no experiment, matching the API.
  • This follows feat: Record exposures under local evaluation flagsmith-python-client#276.

Tests and steps

  • Unit tests cover retries, keep/drop rules, 401/403, the full buffer, the close timeout, flush_events/close, and local-evaluation exposures (new experiment_environment.json fixture).
  • sdk-spec/steps.rb maps the new steps to these SDK calls.
  • Locally, rspec (450 examples) and rubocop pass. My local spec run wasn't reliable (the Flagsmith container kept timing out), so CI is the check.

Unrelated to this change, possibly a bug: under local evaluation, identity overrides get priority Float::INFINITY. With "lowest wins", a segment override with priority 0 beats them. I left this alone.

@flagsmithsmith
flagsmithsmith Bot marked this pull request as ready for review November 12, 2026 10:26
@flagsmithsmith
flagsmithsmith Bot requested a review from a team as a code owner November 12, 2026 10:26
@flagsmithsmith
flagsmithsmith Bot requested review from a team and emyller and removed request for a team November 12, 2026 10:26

@Zaimwa9 Zaimwa9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. Spec passes and the threading is sound, but once a kept batch fills the buffer, every track restarts a retry cycle. Please also make close terminal.

Comment thread lib/flagsmith/sdk/event_processor.rb Resolved
Comment thread lib/flagsmith/sdk/event_processor.rb Resolved
…minal

- While the buffer holds a batch whose attempts all failed, a full buffer
  waits for the next flush instead of starting another round of attempts
  on every tracked event.
- Once the processor is stopping, tracking and flushing do nothing, and it
  does not start again.
- A flush now only skips events a batch taken after it was requested
  already sent, so a flush resends a kept batch even when no event was
  tracked since.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

Requesting changes. Spec passes and the threading is sound, but once a kept batch fills the buffer, every track restarts a retry cycle. Please also make close terminal.

Both are fixed in 6aa2ec4; details are in the inline replies.

  • While a kept batch is held, a full buffer waits for the next flush.
  • After close, tracking and flushing do nothing.

That change also fixes a related bug: a flush with no events tracked since a kept batch failed used to send nothing. It now resends the kept batch.

Locally, rspec (456 examples, 6 consecutive runs) and rubocop pass.

A flush requested while a full-buffer flush was sending a batch counted
no batch taken since, so it sent the kept batch again once that batch's
attempts failed. Count settled batches instead: one in flight at the
request settles after it, and covers the events tracked up to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@flagsmithsmith

Copy link
Copy Markdown
Contributor Author

A follow-up to 6aa2ec4, in daead64. In CI, the spec run on 6aa2ec4 failed "A full buffer drops its oldest event": event_1 was delivered.

The cause was in 6aa2ec4's new rule for when a flush skips events another flush already sent. It counted batches taken. If the full-buffer flush had already taken the batch when the application called flush, that flush saw no batch taken since its request. So it resent the kept batch, and the resend got through.

It now counts batches settled. A batch in flight when a flush is requested settles after the request, so the flush skips the events it covered. The new spec "does not send again a batch in flight when the flush was requested" fails on 6aa2ec4 and passes now. All checks are green on daead64.

@flagsmithsmith
flagsmithsmith Bot requested a review from Zaimwa9 November 12, 2026 17:23
@Zaimwa9

Zaimwa9 commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@themis-blindfold review

Comment thread lib/flagsmith/sdk/event_processor.rb Resolved
Comment thread lib/flagsmith/sdk/event_processor.rb Resolved
Comment thread lib/flagsmith/sdk/event_processor.rb Resolved
@themis-blindfold

Copy link
Copy Markdown

⚖️ Themis review: 🟠 Fix before merge

The retry policy does not match the requested delivery contract, and shutdown can still start a send after its deadline. Retained exposure events can also be duplicated in the buffer. The SDK specification and Ruby CI checks passed, but they do not cover these cases. Ruby was unavailable for local execution.

Area Score
🎯 Correctness 2/5
🧪 Test coverage 3/5
📐 Code quality 3/5
🚀 Product impact 3/5

🟠 Majors

📝 Walkthrough
  • Event delivery — adds retries, bounded buffering, explicit flush, and shutdown.
  • Local evaluation — propagates experiment metadata and variant keys into identity flags.
  • SDK specification — updates the shared workflow and adds event and shutdown steps.
🧪 How to verify
  1. Return 500 and 501 from the events endpoint and check that each batch is dropped after one request.
  2. Block an in-flight send, enqueue another event, close with a short timeout, and check that no queued send starts after close returns.
  3. Exhaust retries for an exposure, track the same exposure again, and inspect the next batch.
  4. Run the event processor and experiment specs across supported Ruby versions.
  5. Automate: add regression tests for these response, shutdown, and retained-exposure cases.

Product take: Reliable event delivery and shutdown materially improve experiment data quality, provided failures and deadlines follow the shared SDK contract.

🧭 Assumptions & unverified claims

No unverified assumptions or claims.

May the last batch leave on time · reviewed at daead64

- A send queued behind the in-flight batch, such as the final flush when
  close times out, no longer takes a batch once the processor is closed.
- Keeping a failed batch rebuilds the buffered exposure keys, dropping
  exposures equal to one already buffered, so a kept exposure tracked
  again is sent once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@flagsmithsmith

flagsmithsmith Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

I have gone through the Themis review of daead64 and replied to each finding inline:

  • 500 and other server errors are retried. No change: spec v0.9.0 requires 500 to be retried, with the batch dropped only when the last attempt gets a 500. Its scenarios are quoted in the reply. Whether to drop 501/505+ at once, which the spec does not cover, is left to you.
  • A queued final send could start after close returns. Fixed in ab90211: no batch is taken once closed.
  • A kept batch lost its exposure deduplication. Fixed in ab90211: exposure keys are rebuilt when a batch is kept.

The new specs for the two fixes fail on daead64 and pass now. Locally, rubocop and rspec pass (460 examples). The only failures were in datapooling_manager_spec, which failed the same way locally before any event-processor change and passes in CI.

@Zaimwa9
Zaimwa9 merged commit 5abcc8e into main Nov 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk-spec Bumps of the Flagsmith SDK behavioural spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant