Skip to content

Multi-user mode GrowthBookClient never enables SSE reconnection (retryOnFailure hardwired to false) #226

Description

@yang198876

Summary

The SSE reconnection support added for #52 (GBFeaturesRepository.initialize(Boolean retryOnFailure)) is never reachable from the multi-user mode GrowthBookClient: it always calls the no-arg repository.initialize(), which delegates to initialize(false), and Options exposes no way to opt in. As a result, any app using GrowthBookClient with FeatureRefreshStrategy.SERVER_SENT_EVENTS permanently stops receiving feature updates after the first abnormal SSE stream failure — silently.

Affected versions

Verified against the published sources of 0.10.6 and 0.10.10 (latest release), and against current main.

Details (0.10.6 / 0.10.10)

The chain:

  1. GrowthBookClient.initialize()repository.initialize() (no-arg)
  2. GBFeaturesRepository.initialize()initialize(false)retryOnFailure hardwired to false
  3. In the SSE event source listener created by createEventSourceListenerAndStartListening(retryOnFailure):
@Override
public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
    super.onFailure(eventSource, t, response);
    if (retryOnFailure) {
        createEventSourceListenerAndStartListening(true);
        ...
    }
}

With retryOnFailure == false, an abnormal stream failure (connection reset, proxy/pod restart, mid-stream IOException) does nothing:

  • no reconnect attempt;
  • no polling fallback — schedulePolling() early-returns for SERVER_SENT_EVENTS;
  • no observability — this path never invokes onRefreshFailed(...), so registered FeatureRefreshCallback.onError(...) callbacks never fire.

The client then serves the last in-memory features forever (with isCacheDisabled(true) there is not even a cache to refresh from), and the application has no signal that anything is wrong.

Note that a graceful server close does reconnect on these versions (onClosedhandler.onClose → unconditional createEventSourceListenerAndStartListening), which makes the failure mode intermittent and easy to miss in testing: clean proxy restarts recover, abrupt failures don't.

Still present on main (after #212)

#212 nicely reworked the retry machinery (scheduleSseReconnect with exponential backoff via FeatureFetchRetryPolicy, Options.retryPolicy), but the gate is still there and multi-user mode still never passes true:

private synchronized void scheduleSseReconnect(Boolean retryOnFailure) {
    if (!Boolean.TRUE.equals(retryOnFailure)
            || this.shuttingDown.get()
            || !this.sseReconnectScheduled.compareAndSet(false, true)) {
        return;
    }
    ...
}

GrowthBookClient.initializeFeaturesRepository(...) still calls the no-arg repositorySnapshot.initialize(), so Options.retryPolicy is configurable but the SSE reconnect it parameterizes is unreachable from multi-user mode.

There is also a behavioral tightening on main: the graceful-close path now routes through scheduleSseReconnect(retryOnFailure) as well, so once this ships, multi-user mode + SSE will no longer reconnect even on a graceful close (which 0.10.x did unconditionally). That widens this issue rather than fixing it.

Reproduction sketch

  1. Build a GrowthBookClient (multi-user mode) with refreshStrategy = SERVER_SENT_EVENTS pointing at a GrowthBook Proxy; call initialize().
  2. Kill the proxy abruptly (or drop the TCP connection), then bring it back.
  3. Change a feature flag.
  4. The client never reconnects, FeatureRefreshCallback stays silent, and evaluations keep returning the pre-failure values indefinitely.

Suggested fix

Either of:

  • Have GrowthBookClient call repository.initialize(true) — with Feature: Force Refresh API and Exponential Retry #212's bounded backoff this seems like a safe default for a long-lived server-side client; or
  • Expose the flag on Options (e.g. sseReconnectOnFailure) and thread it through to initialize(...).

Happy to send a PR for either direction if you have a preference.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions