fix(sync/grpc): track readiness per Sync instance#1985
Conversation
The gRPC sync guarded its one-shot readiness with a package-level sync.Once. Because that Once is shared across every grpc.Sync in the process, only the first instance whose stream connects ran the closure and set its own ready flag; all other instances stayed not-ready forever, and IsReady() kept returning false for them. flagd builds a distinct grpc.Sync per configured grpc source and the runtime only reports ready once every sync is ready, so any flagd with two or more grpc sources never passed /readyz even after all streams were healthy. Set g.ready directly in handleFlagSync (the assignment is idempotent), which makes readiness per instance, matching the other sync implementations. Add a regression test that starts two independent grpc.Sync instances and asserts both become ready. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
✅ Deploy Preview for polite-licorice-3db33c canceled.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe ChangesReadiness Flag Fix and Test
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Test as Test_MultipleSyncsBecomeReady
participant Sync1 as Sync Instance 1
participant Sync2 as Sync Instance 2
participant Server as bufconn gRPC Server
Test->>Sync1: Sync(ctx1, dataSync)
Test->>Sync2: Sync(ctx2, dataSync)
Sync1->>Server: stream request (handleFlagSync)
Server-->>Sync1: SyncFlags payload
Sync1->>Sync1: set ready = true
Sync2->>Server: stream request (handleFlagSync)
Server-->>Sync2: SyncFlags payload
Sync2->>Sync2: set ready = true
Test->>Sync1: IsReady()
Test->>Sync2: IsReady()
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
|



Because that
Onceis shared across everygrpc.Syncin the process, only thefirst instance whose stream connects runs the closure and sets its own
readyflag. Every other
grpc.Syncinstance stays not-ready forever, so itsIsReady()keeps returning false.flagd builds a distinct
grpc.Syncper configured grpc source(
SyncBuilder.newGRPCincore/pkg/sync/builder/syncbuilder.go), and theruntime only reports ready once every sync is ready (
Runtime.isReadyinflagd/pkg/runtime/runtime.goreturns false if anySyncs[].IsReady()isfalse). The net effect is that any flagd configured with two or more
grpc://sync sources never passes
/readyz, even after all of its gRPC streams arehealthy, so the pod stays NotReady.
The fix
Set
g.ready = truedirectly inhandleFlagSyncand drop the package-levelonce(and its now-unusedsyncimport alias). The assignment is idempotent,so this is a no-op for the single-source case and makes readiness per instance
for the multi-source case. This also matches the other sync implementations:
the blob, file, and http syncs already set a plain
ready booldirectly, andkubernetes tracks it per instance with an
atomic.Bool. The package-levelOncewas the outlier.The write still happens-before the update pushed onto the
dataSyncchannel,so no new concurrent read path is introduced and the change stays race-clean.
Tests
Adds
Test_MultipleSyncsBecomeReady, which starts two independentgrpc.Syncinstances (each backed by its own bufconn server), drives each one, and asserts
both report
IsReady() == true. The test synchronizes by receiving a payloadfrom the sync channel before reading
IsReady(), so it establishes ahappens-before edge and is race-free by construction rather than by timing.
Before this change the test fails on the second instance
("sync 1 should be ready after its stream connects"); after it, both pass.
Readiness was previously untested in this file, so this also closes that
coverage gap.
Verified with:
go build ./...go test -race -covermode=atomic -cover -short ./pkg/sync/grpc/(fromcore/)golangci-lint run ./pkg/sync/grpc/...