feat: SSE support for OFREP endpoints - #2012
Conversation
Signed-off-by: Jamie Sinn <james.sinn@dynatrace.com>
Signed-off-by: Jamie Sinn <james.sinn@dynatrace.com>
✅ Deploy Preview for polite-licorice-3db33c canceled.
|
📝 WalkthroughWalkthroughOFREP now supports optional SSE event-stream advertisements and ChangesOFREP SSE support
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant OFREPClient
participant OFREPHandler
participant SSEService
participant StoreTracker
OFREPClient->>OFREPHandler: Request bulk evaluation
OFREPHandler->>StoreTracker: Read selector version
StoreTracker-->>OFREPHandler: Return ETag and last-modified time
OFREPHandler-->>OFREPClient: Return eventStreams metadata
OFREPClient->>SSEService: Subscribe to channel
StoreTracker->>SSEService: Publish refetchEvaluation after a flag change
SSEService-->>OFREPClient: Send refetch event
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment |
Signed-off-by: Jamie Sinn <james.sinn@dynatrace.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
flagd/pkg/runtime/from_config.go (1)
30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider spelling out the inactivity-delay field name and its unit.
OfrepSSEInactivityDelabbreviates "Delay" and omits the unit. The consuming field isSSEInactivityDelaySec, which records the unit.Configis exported, so renaming later is a breaking change for embedders.♻️ Proposed rename
- OfrepSSEEnabled bool - OfrepSSEInactivityDel int - OfrepSSEPublicURL string + OfrepSSEEnabled bool + OfrepSSEInactivityDelaySec int + OfrepSSEPublicURL stringUpdate the assignment in
flagd/cmd/start.goat line 214 to match.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flagd/pkg/runtime/from_config.go` around lines 30 - 32, Rename the exported Config field OfrepSSEInactivityDel to OfrepSSEInactivityDelaySec to spell out the name and document seconds, and update the corresponding assignment in the start command to use the new field while preserving its existing value.flagd/pkg/service/flag-evaluation/ofrep/sse/service_test.go (1)
34-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the fixed sleeps with polling to avoid a flaky test.
The test relies on two 100 ms sleeps for synchronization. The second sleep waits for the subscription to register server-side.
Newsetses.ReplayAll = false, so an event published before registration is lost permanently. On a loaded CI runner the test then fails at the 3 second timeout.Poll
svc.active.snapshot()for the channel instead. The test is in the same package, so it can read that state directly.♻️ Proposed refactor
- // allow the tracker's initial (empty) snapshot to be consumed and skipped - time.Sleep(100 * time.Millisecond) - stream, err := eventsource.Subscribe(ts.URL+"?channels=fs1", "") require.NoError(t, err) defer stream.Close() - // allow the subscription to register server-side before publishing - time.Sleep(100 * time.Millisecond) + // wait for the subscription to register server-side; ReplayAll is false, so an + // event published before registration is lost + require.Eventually(t, func() bool { + for _, ch := range svc.active.snapshot() { + if ch == "fs1" { + return true + } + } + return false + }, 5*time.Second, 10*time.Millisecond, "subscription did not register")If
activeChannels.snapshot()returns a different shape, adapt the predicate accordingly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flagd/pkg/service/flag-evaluation/ofrep/sse/service_test.go` around lines 34 - 44, Replace the fixed synchronization sleeps in the test with polling: retain the initial delay only as needed to consume the empty snapshot, then poll svc.active.snapshot() until channel fs1 is registered before calling s.Update. Use the existing polling/assertion utilities and adapt the predicate to the snapshot’s shape, ensuring publication occurs only after subscription registration.flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go (1)
86-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the
lastModifiedcarry-forward.
TestTracker_VersiondiscardslastModifiedat every call site. No test asserts the carry-forward logic intracker.golines 134-142, which preserveslastModifiedwhen a fingerprint is unchanged and refreshes it otherwise. That value reaches clients asflagConfigLastModifiedin the bulk response, so a regression would be visible externally.💚 Proposed test
func TestTracker_Update_LastModifiedCarriedForward(t *testing.T) { tr := &Tracker{versions: map[string]version{}} flags := []model.Flag{testFlag("fs1", "a", "on")} tr.update(flags) _, firstLM, ok := tr.Version(mustSelector(t, "flagSetId=fs1")) require.True(t, ok) require.NotZero(t, firstLM) // an unchanged config must keep the original lastModified time.Sleep(1100 * time.Millisecond) // lastModified has second granularity tr.update(flags) _, sameLM, ok := tr.Version(mustSelector(t, "flagSetId=fs1")) require.True(t, ok) assert.Equal(t, firstLM, sameLM, "unchanged config must keep lastModified") // a changed config must refresh lastModified tr.update([]model.Flag{testFlag("fs1", "a", "off")}) _, newLM, ok := tr.Version(mustSelector(t, "flagSetId=fs1")) require.True(t, ok) assert.Greater(t, newLM, firstLM, "changed config must refresh lastModified") }The test needs the
timeimport.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go` around lines 86 - 111, Extend TestTracker_Version or add a focused tracker update test to assert lastModified is initially set, remains unchanged after updating with the same flags, and increases after a fingerprint-changing update. Add the required time import and account for the value’s second-level granularity when separating unchanged and changed updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@flagd/pkg/service/flag-evaluation/ofrep/handler.go`:
- Around line 232-237: Update requestETag and the 304 decision to use only the
If-None-Match header as the client cache validator; keep flagConfigEtag separate
as change-trigger metadata and prevent it from influencing the comparison. Add a
regression test covering flagConfigEtag=etag-v2 with If-None-Match: etag-v1,
ensuring the response does not incorrectly return 304.
In `@flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go`:
- Around line 59-62: Update NewOfrepService to validate that flagStore is
non-nil whenever cfg.SSEEnabled is true, returning a construction error before
calling sse.New. Preserve the existing SSE initialization for valid stores and
the current behavior when SSE is disabled.
In `@flagd/pkg/service/flag-evaluation/ofrep/sse/service.go`:
- Around line 13-15: Update the SSE service configuration and NewOfrepService
construction so the heartbeat interval is derived from the configured OFREP
inactivity delay rather than always using defaultHeartbeatInterval. Pass the
advertised delay into the SSE service, calculate a heartbeat that remains safely
below it, and preserve consistent behavior for the default configuration.
In `@flagd/pkg/service/flag-evaluation/ofrep/sse/tracker.go`:
- Around line 69-85: Update Tracker.Run to distinguish a normal context-driven
watcher closure from an unexpected store or selector error, using the
result/error signaling exposed by store.Watch. Preserve silent shutdown for
context cancellation, but log the unexpected watcher error before returning so
failures are observable; keep the existing initialization snapshot and publish
behavior unchanged.
---
Nitpick comments:
In `@flagd/pkg/runtime/from_config.go`:
- Around line 30-32: Rename the exported Config field OfrepSSEInactivityDel to
OfrepSSEInactivityDelaySec to spell out the name and document seconds, and
update the corresponding assignment in the start command to use the new field
while preserving its existing value.
In `@flagd/pkg/service/flag-evaluation/ofrep/sse/service_test.go`:
- Around line 34-44: Replace the fixed synchronization sleeps in the test with
polling: retain the initial delay only as needed to consume the empty snapshot,
then poll svc.active.snapshot() until channel fs1 is registered before calling
s.Update. Use the existing polling/assertion utilities and adapt the predicate
to the snapshot’s shape, ensuring publication occurs only after subscription
registration.
In `@flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go`:
- Around line 86-111: Extend TestTracker_Version or add a focused tracker update
test to assert lastModified is initially set, remains unchanged after updating
with the same flags, and increases after a fingerprint-changing update. Add the
required time import and account for the value’s second-level granularity when
separating unchanged and changed updates.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e146c194-5268-4b69-900f-956c7e1fccb1
⛔ Files ignored due to path filters (1)
flagd/go.sumis excluded by!**/*.sum
📒 Files selected for processing (15)
core/pkg/service/ofrep/models.gocore/pkg/store/query.goflagd/cmd/start.goflagd/go.modflagd/pkg/runtime/from_config.goflagd/pkg/service/flag-evaluation/ofrep/handler.goflagd/pkg/service/flag-evaluation/ofrep/ofrep_service.goflagd/pkg/service/flag-evaluation/ofrep/ofrep_service_test.goflagd/pkg/service/flag-evaluation/ofrep/sse/event.goflagd/pkg/service/flag-evaluation/ofrep/sse/handler.goflagd/pkg/service/flag-evaluation/ofrep/sse/service.goflagd/pkg/service/flag-evaluation/ofrep/sse/service_test.goflagd/pkg/service/flag-evaluation/ofrep/sse/tracker.goflagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.goflagd/pkg/service/flag-evaluation/ofrep/sse_bulk_test.go
Signed-off-by: Jamie Sinn <james.sinn@dynatrace.com>
Signed-off-by: Jamie Sinn <james.sinn@dynatrace.com>
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go (1)
110-118: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winTest all selector versions after unexpected watcher closure.
This test only verifies
allKey.Tracker.Versionalso servesflagSetIdandsourceselectors. If a later change leaves either entry intact, selector-specific bulk requests can receive stale304 Not Modifiedresponses. Seed those keys and assert that every lookup misses afterRunreturns.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go` around lines 110 - 118, Expand TestTracker_Run_UnexpectedCloseInvalidatesVersions to seed version entries for the flagSetId and source selectors in addition to allKey, then call Tracker.Version with each selector and assert every lookup reports ok as false after Run returns. Preserve the existing unexpected-close setup and stale-version invalidation assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@flagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.go`:
- Around line 110-118: Expand TestTracker_Run_UnexpectedCloseInvalidatesVersions
to seed version entries for the flagSetId and source selectors in addition to
allKey, then call Tracker.Version with each selector and assert every lookup
reports ok as false after Run returns. Preserve the existing unexpected-close
setup and stale-version invalidation assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bd7d3001-3481-495d-b673-b36c3e0b6f43
📒 Files selected for processing (5)
flagd/pkg/service/flag-evaluation/ofrep/handler.goflagd/pkg/service/flag-evaluation/ofrep/ofrep_service.goflagd/pkg/service/flag-evaluation/ofrep/sse/tracker.goflagd/pkg/service/flag-evaluation/ofrep/sse/tracker_test.goflagd/pkg/service/flag-evaluation/ofrep/sse_bulk_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- flagd/pkg/service/flag-evaluation/ofrep/ofrep_service.go
- flagd/pkg/service/flag-evaluation/ofrep/handler.go
- flagd/pkg/service/flag-evaluation/ofrep/sse/tracker.go
| // be notified (via a `refetchEvaluation` event) when the flag configuration changes. | ||
| type EventStream struct { | ||
| Type string `json:"type"` | ||
| Endpoint *EventStreamEndpoint `json:"endpoint"` |
There was a problem hiding this comment.
What about the url option? I saw in it in the adr and if I understood the discussion correctly, it should even be default? Or has that changed
There was a problem hiding this comment.
I actually wrote it with the URL method the first time I wrote this out, but then I realized we have a small issue of that we don't know the required path/host path that clients use to reach the OFREP endpoint as people regularly put flagd behind a proxy, SSL termination, etc.
The endpoint method allows us to use the reliance on the OFREP client to fall back to the same host as the OFREP request is being made to for the SSE endpoint. This means we don't need to configure an endpoint/host on the flagd instance.
Though - I did add a configuration property to allow configuring the hostname if you know it, or want to use a different host/route path.



This PR
The implementation of the wire protocol/message format is the bare SSE object that was defined in ADR-0008, not a custom protocol.
The SSE server is using the LaunchDarkly EventSource server which we've proven in DevCycle/Dynatrace that it's quite stable and has the ideal functionality that we want (channels, separation, and highly performant).
Related Issues
open-feature/protocol#63
Notes
The URL path for this
/ofrep/v1/sseis very open to discussion - I have no strong opinions on this, but this just felt the most logical.How to test
This was tested manually; I can add a script or something to the test folder if that's easier.