Skip to content

fix(flagd): back off before stream reconnect - #391

Open
looooown2006 wants to merge 2 commits into
open-feature:mainfrom
looooown2006:fix/flagd-stream-reconnect-backoff
Open

fix(flagd): back off before stream reconnect#391
looooown2006 wants to merge 2 commits into
open-feature:mainfrom
looooown2006:fix/flagd-stream-reconnect-backoff

Conversation

@looooown2006

Copy link
Copy Markdown

Summary

Fixes the flagd provider stream reconnect loop so both gRPC stream implementations apply an application-level delay before creating the next stream after a stream error or normal stream completion.

Changes:

  • GrpcResolver now waits retry_backoff_max_ms before recreating the RPC EventStream.
  • GrpcWatcher now waits retry_backoff_max_ms before recreating the in-process SyncFlags stream.
  • shutdown uses an event-backed wait so shutdown can interrupt the backoff instead of blocking until the timeout expires.
  • added regression coverage for RPC and in-process stream error/completion reconnect paths.

Closes #390.

Verification

uv run --frozen pytest tests/test_grpc_watcher.py tests/test_grpc_resolver.py -q
# 10 passed
uv run --frozen pytest tests/test_config.py tests/test_errors.py tests/test_file_store.py tests/test_flagd.py tests/test_grpc_watcher.py tests/test_grpc_resolver.py tests/test_in_process.py tests/test_metadata.py tests/test_targeting.py -q
# 199 passed
uv tool run ruff check providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py providers/openfeature-provider-flagd/tests/test_grpc_watcher.py providers/openfeature-provider-flagd/tests/test_grpc_resolver.py
# All checks passed
uv run --frozen poe mypy
# Success: no issues found in 26 source files

Note: I also tried the broader non-e2e pytest command, but the local run still initialized the Docker-backed e2e test-harness fixture and failed before running those cases because Docker Desktop's Linux engine was not running on this Windows machine.

@looooown2006
looooown2006 requested review from a team as code owners May 10, 2026 09:30
@github-actions
github-actions Bot requested review from aepfli and federicobond May 10, 2026 09:30

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a _shutdown_event using threading.Event in both GrpcResolver and grpc_watcher to manage reconnection delays and facilitate cleaner shutdowns. It refactors the listen method in GrpcResolver into smaller helper methods and adds comprehensive unit tests for the reconnection and backoff logic. The reviewer suggests using the initial backoff duration rather than the maximum backoff for reconnection attempts to prevent excessive delays during recovery from transient failures.

logger.debug(self.config.fatal_status_codes)

self.retry_grace_period = config.retry_grace_period
self.retry_backoff_max_seconds = config.retry_backoff_max_ms * 0.001

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.

medium

Using the maximum backoff duration (retry_backoff_max_ms) as a constant delay for every reconnect attempt can lead to significant delays in recovery from transient failures. It is generally better to use the initial backoff duration (retry_backoff_ms) or implement an exponential backoff strategy. Since gRPC already handles connection-level backoff internally, a small application-level delay is sufficient to prevent tight loops while allowing faster recovery.

Suggested change
self.retry_backoff_max_seconds = config.retry_backoff_max_ms * 0.001
self.retry_backoff_seconds = config.retry_backoff_ms * 0.001
self.retry_backoff_max_seconds = config.retry_backoff_max_ms * 0.001

)

def _wait_before_reconnect(self) -> None:
self._shutdown_event.wait(self.retry_backoff_max_seconds)

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.

medium

As mentioned in the initialization, using the maximum backoff for every retry significantly impacts availability. Consider using the initial backoff duration instead. Note that you will also need to update the corresponding regression tests in tests/test_grpc_resolver.py.

Suggested change
self._shutdown_event.wait(self.retry_backoff_max_seconds)
self._shutdown_event.wait(self.retry_backoff_seconds)

return False

def _wait_before_reconnect(self) -> None:
self._shutdown_event.wait(self.retry_backoff_max_seconds)

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.

medium

Using the maximum backoff duration (retry_backoff_max_ms) as a constant delay for every reconnect attempt can lead to significant delays in recovery from transient failures. It is generally better to use the initial backoff duration (retry_backoff_ms) or implement an exponential backoff strategy. Since gRPC already handles connection-level backoff internally, a small application-level delay is sufficient to prevent tight loops while allowing faster recovery. Note that you will also need to update the corresponding regression tests in tests/test_grpc_watcher.py.

Suggested change
self._shutdown_event.wait(self.retry_backoff_max_seconds)
self._shutdown_event.wait(self.retry_backoff_seconds)

@looooown2006

Copy link
Copy Markdown
Author

Thanks for the review. I kept this tied to retry_backoff_max_ms because #390 and the flagd provider spec both describe retryBackoffMaxMs as the explicit application-level delay before recreating the stream after an error or completion.

This is separate from gRPC's RPC-level retry behavior; the added wait only applies between one stream ending and the next stream being created. That seems to align with the issue/spec and the reconnect-loop scenario they call out.

Signed-off-by: looooown2006 <102905927+looooown2006@users.noreply.github.com>
@toddbaert

Copy link
Copy Markdown
Member

@coderabbitai /review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

@toddbaert: I’ll review the changes in #391.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@toddbaert
toddbaert force-pushed the fix/flagd-stream-reconnect-backoff branch from 7282f02 to 4f5c496 Compare July 30, 2026 19:27
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@toddbaert, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f75725c-553c-411a-9f5a-6ef237fac970

📥 Commits

Reviewing files that changed from the base of the PR and between 62e251d and 23ace6e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py
  • providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py
  • providers/openfeature-provider-flagd/tests/test_grpc_resolver.py
  • providers/openfeature-provider-flagd/tests/test_grpc_watcher.py

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

Comment @coderabbitai help to get the list of available commands.

def _wait_before_reconnect(self) -> None:
self._shutdown_event.wait(self.retry_backoff_max_seconds)

def _handle_rpc_error(self, e: grpc.RpcError) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the method extractions.

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
@toddbaert
toddbaert force-pushed the fix/flagd-stream-reconnect-backoff branch from 3869a48 to 23ace6e Compare July 30, 2026 20:03
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.20%. Comparing base (62e251d) to head (23ace6e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #391      +/-   ##
==========================================
- Coverage   95.55%   94.20%   -1.35%     
==========================================
  Files          24       31       +7     
  Lines        1034     1070      +36     
==========================================
+ Hits          988     1008      +20     
- Misses         46       62      +16     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: add explicit application-level backoff on stream reconnection

4 participants