Skip to content

fix(analytics): guard polling start so concurrent callers don't leak intervals (SDK-81)#278

Open
tylerjroach wants to merge 2 commits into
masterfrom
fix/sdk-85-thread-safe-polling-start
Open

fix(analytics): guard polling start so concurrent callers don't leak intervals (SDK-81)#278
tylerjroach wants to merge 2 commits into
masterfrom
fix/sdk-85-thread-safe-polling-start

Conversation

@tylerjroach

@tylerjroach tylerjroach commented Jun 29, 2026

Copy link
Copy Markdown

Summary

startPollingForDefinitions had a check-then-act race: await this._initialFetchPromise happens before the !this.pollingInterval check, giving the event loop an interleaving point. Two concurrent callers both await the fetch, both observe pollingInterval == null, and both call setInterval — the first interval handle is overwritten and orphaned (still scheduled to poll, no reference to clear it).

Cache the in-flight start as _startPromise and short-circuit subsequent calls to share that promise. stopPollingForDefinitions and shutdown clear the cache so a later start can re-establish polling cleanly.

Context

Linear: SDK-81. Same cross-SDK push as the Java and Ruby fixes — three SDKs were affected (Node, Java, Ruby). Python uses a not self._polling_task check that's safe under the GIL; Go uses CompareAndSwap.

Test plan

  • Full local_flags.js suite (45 examples) passes
  • New "is idempotent under concurrent startPollingForDefinitions calls and does not leak intervals" test fires 8 concurrent startPollingForDefinitions() calls and asserts setInterval was called exactly once. Before this fix the count would be 8.

🤖 Generated with Claude Code

…rvals

startPollingForDefinitions had a check-then-act race: the initial
_fetchFlagDefinitions() was awaited *before* the `!this.pollingInterval`
check, giving the event loop an interleaving point. Two concurrent
callers both await the fetch, both observe pollingInterval == null,
and both call setInterval — the first interval handle is overwritten
and leaks (still scheduled to poll, no way to clear it).

Cache the in-flight start as `_startPromise` and short-circuit
subsequent calls to share the same promise. stopPollingForDefinitions
and shutdown clear the cache so a later start can re-establish polling.

Linear: SDK-85

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach tylerjroach requested review from a team and jakewski June 29, 2026 18:11
@linear-code

linear-code Bot commented Jun 29, 2026

Copy link
Copy Markdown

SDK-85

SDK-81

@tylerjroach tylerjroach changed the title fix(flags): guard polling start so concurrent callers don't leak intervals (SDK-85) fix(analytics): guard polling start so concurrent callers don't leak intervals (SDK-85) Jun 29, 2026
@tylerjroach tylerjroach changed the title fix(analytics): guard polling start so concurrent callers don't leak intervals (SDK-85) fix(analytics): guard polling start so concurrent callers don't leak intervals (SDK-81) Jun 29, 2026
@tylerjroach

Copy link
Copy Markdown
Author

@greptileai

@tylerjroach

Copy link
Copy Markdown
Author

@greptileai review

The initial startPollingForDefinitions guard had two dead-lock states
where the cached _startPromise permanently blocked further starts:

1. When enable_polling=false, stop hits the else branch (warn log)
   without clearing _startPromise. Subsequent start becomes a no-op.
   Now unconditionally clear, matching shutdown().

2. When the initial fetch throws, the outer try/catch logs and returns,
   leaving _startPromise resolved-with-undefined. Callers cannot retry
   via start() — they'd have to stop first, which logs a spurious warning.
   Now clear _startPromise in the catch so the next start attempts a
   fresh fetch.

Also reworked the concurrent-start test to assert on
_fetchFlagDefinitions call count rather than setInterval count. The
!pollingInterval check-then-set is already atomic within a microtask
in JS's single-threaded model, so setInterval-count would be 1 with
or without the guard. What the guard actually prevents is N redundant
fetches under N concurrent starts.
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.

1 participant