Skip to content

fix(analytics): keep polling alive when fetch throws a Throwable (SDK-86)#93

Open
tylerjroach wants to merge 2 commits into
masterfrom
fix/polling-throwable-defense
Open

fix(analytics): keep polling alive when fetch throws a Throwable (SDK-86)#93
tylerjroach wants to merge 2 commits into
masterfrom
fix/polling-throwable-defense

Conversation

@tylerjroach

Copy link
Copy Markdown
Contributor

Summary

ScheduledExecutorService#scheduleAtFixedRate permanently cancels future executions if the runnable throws an uncaught Throwable. fetchDefinitions() only catches Exception, so an Error (OutOfMemoryError, StackOverflowError, LinkageError, etc.) escaping would silently kill flag-definitions polling for the JVM's lifetime — and the thread factory has no UncaughtExceptionHandler to even surface it.

Wrap the scheduled runnable in a defensive try { fetchDefinitions(); } catch (Throwable t) { logger.log(SEVERE, ...) }. fetchDefinitions() keeps its Exception handling unchanged; the wrapper exists purely to swallow + log JVM-level escapes so the schedule keeps firing.

Context

Linear: SDK-86. Practical scope is narrow (JVM-level errors only) but the defense is one-line.

Test plan

  • Full test suite passes (173 tests, was 172, +1 new)
  • New testPollingSurvivesThrowableFromFetch arms an Error to escape the second httpGet call, then asserts the polling task continues to fire (callCount >= 3 after 2.5s with a 1s interval). Before the fix the scheduler would have cancelled the task on the Error and callCount would have stopped at 2.

ScheduledExecutorService#scheduleAtFixedRate permanently cancels
future executions if the runnable throws an uncaught Throwable.
fetchDefinitions only catches Exception, so an Error (OOM,
StackOverflowError, LinkageError, etc.) escaping would silently
kill flag-definitions polling for the JVM's lifetime.

Wrap the scheduled runnable in a defensive try/catch(Throwable).
fetchDefinitions keeps its Exception handling unchanged; the
wrapper exists purely to swallow + log JVM-level escapes so the
schedule keeps firing.

Linear: SDK-86

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

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown

SDK-86

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — the change is a one-line catch-clause broadening with a targeted new test that directly validates the fix.

The change is minimal and well-scoped: broadening the catch inside fetchDefinitions() prevents the scheduler from ever seeing an uncaught Throwable. The new test exercises the exact failure mode (Error escaping httpGet) and confirms polling continues. No data-path or correctness regressions introduced.

No files require special attention.

Important Files Changed

Filename Overview
src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java Broadens catch clause in fetchDefinitions() from Exception to Throwable, preventing ScheduledExecutorService from cancelling the polling task on JVM-level errors.
src/test/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProviderTest.java Adds testPollingSurvivesThrowableFromFetch: arms an Error on the second httpGet call, then asserts callCount >= 3 after 2.5s to verify the polling task continues despite the thrown Error.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SE as ScheduledExecutorService
    participant FD as fetchDefinitions()
    participant API as Mixpanel API

    loop Every pollingIntervalSeconds
        SE->>FD: run()
        FD->>API: httpGet(url)
        alt Success
            API-->>FD: JSON response
            FD->>FD: parseDefinitions()
            FD->>FD: flagDefinitions.set(...)
        else Exception (IOException, JSONException, etc.)
            FD->>FD: catch Exception log WARNING
        else Error (OOM, StackOverflow, etc.) NEW
            FD->>FD: catch Throwable log WARNING
        end
        FD-->>SE: return (no uncaught Throwable)
        Note over SE: Task stays scheduled
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SE as ScheduledExecutorService
    participant FD as fetchDefinitions()
    participant API as Mixpanel API

    loop Every pollingIntervalSeconds
        SE->>FD: run()
        FD->>API: httpGet(url)
        alt Success
            API-->>FD: JSON response
            FD->>FD: parseDefinitions()
            FD->>FD: flagDefinitions.set(...)
        else Exception (IOException, JSONException, etc.)
            FD->>FD: catch Exception log WARNING
        else Error (OOM, StackOverflow, etc.) NEW
            FD->>FD: catch Throwable log WARNING
        end
        FD-->>SE: return (no uncaught Throwable)
        Note over SE: Task stays scheduled
    end
Loading

Reviews (2): Last reviewed commit: "refactor: simplify Throwable catch to fe..." | Re-trigger Greptile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using catch (Throwable t) instead of catch (Exception e) here will achieve same?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot can you check if this comment is valid?

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.

Yes, that's correct and simpler. Changed catch (Exception e) to catch (Throwable t) directly in fetchDefinitions() and removed the wrapper lambda — same protection, less indirection. Commit: refactor: simplify Throwable catch to fetchDefinitions directly.

Co-authored-by: ketanmixpanel <188901560+ketanmixpanel@users.noreply.github.com>
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.

3 participants