Skip to content

fix: Deliver the terminal OFF data source status when the client is closed - #247

Merged
tanderson-ld merged 2 commits into
mainfrom
ta/sdk-3432/off-on-close
Nov 3, 2026
Merged

tanderson-ld merged 2 commits into
mainfrom
ta/sdk-3432/off-on-close

Conversation

@tanderson-ld

@tanderson-ld tanderson-ld commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

What

LDClient.close() tore down the shared executor with shutdownNow(), which discards any already-queued tasks. The terminal OFF data source status is broadcast asynchronously on that same executor during close(), so a registered status listener could miss the final OFF whenever the executor thread happened to be busy at the moment close() ran.

close() now shuts the executor down gracefully (shutdown()), waits up to SHUTDOWN_DRAIN_MILLIS (5s) for already-submitted tasks — including that OFF broadcast — to run, and falls back to shutdownNow() if the drain does not complete promptly so close() cannot hang on a stuck task.

Why

The drop has been latent since the shared single-thread executor was introduced in 5.0.0, but was invisible in practice: before the RETRY changes the meaningful terminal states were real data source failures, not the close-driven OFF. Now that data sources no longer fail permanently and the OpenFeature provider translates a data-source OFF into a provider status transition, a dropped close-OFF leaves a status listener without the terminal state it expects.

The 5s drain cap aligns with the teardown wait LaunchDarkly's Ruby and Python server SDKs already use for their data-system threads. A healthy drain returns in microseconds; the cap only bounds the pathological stuck-task case.

Testing

Adds statusListenerReceivesTheTerminalOffWhenCloseOccursWhileTheExecutorThreadIsBusy, which occupies the single executor thread while the OFF is broadcast on close and asserts the listener still observes it. Verified non-vacuous: it fails on the old shutdownNow() and passes with the drain. Checkstyle clean.

Ticket: SDK-3432


Note

Overview
Fixes a shutdown race where LDClient.close() called shutdownNow() on the single-thread shared executor, dropping the queued task that broadcasts the terminal OFF data source status (emitted during dataSystem.close()). Status listeners—and integrations like OpenFeature that map OFF to provider state—could miss the final transition when the executor thread was still busy.

close() now uses shutdown(), waits up to SHUTDOWN_DRAIN_MILLIS (5s) for in-flight/queued tasks to finish, then falls back to shutdownNow() (including on interrupt) so shutdown cannot hang indefinitely. A note documents that any teardown after this step may be delayed by up to that drain window.

Adds regression test statusListenerReceivesTheTerminalOffWhenCloseOccursWhileTheExecutorThreadIsBusy (SDK-3432) with a mock data source that emits OFF on close and a listener that blocks the executor thread during INTERRUPTED.

Reviewed by Cursor Bugbot for commit 109e3d6. Bugbot is set up for automated code reviews on this repo. Configure here.

…losed

LDClient.close() shut down the shared executor with shutdownNow(), which
discards any already-queued tasks. The terminal OFF data source status is
broadcast asynchronously on that same executor during close(), so a status
listener could miss the final OFF whenever the executor thread happened to be
busy when close() ran.

close() now shuts the executor down gracefully and waits up to
SHUTDOWN_DRAIN_MILLIS for already-submitted tasks (such as that OFF broadcast)
to run, then falls back to shutdownNow() if the drain does not complete
promptly so close() cannot hang on a stuck task.

Adds a regression test that occupies the single executor thread while the OFF
is broadcast on close and asserts the listener still observes it.
@tanderson-ld
tanderson-ld requested a review from a team as a code owner November 2, 2026 20:39
# Conflicts:
#	lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java
@tanderson-ld
tanderson-ld merged commit f444bee into main Nov 3, 2026
30 checks passed
@tanderson-ld
tanderson-ld deleted the ta/sdk-3432/off-on-close branch November 3, 2026 14:13
tanderson-ld pushed a commit that referenced this pull request Nov 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[7.17.5](launchdarkly-java-server-sdk-7.17.4...launchdarkly-java-server-sdk-7.17.5)
(2026-11-03)


### Bug Fixes

* Bump embedded Guava to 33.8.0-jre to address CVE-2026-102554
([#246](#246))
([0d66cfb](0d66cfb))
* Deliver the terminal OFF data source status when the client is closed
([#247](#247))
([f444bee](f444bee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> This **Release Please** PR cuts **launchdarkly-java-server-sdk
7.17.5** by bumping version metadata in `.release-please-manifest.json`,
`gradle.properties`, `Version.SDK_VERSION`, and adding the **7.17.5**
section to `CHANGELOG.md`.
> 
> The release notes capture two bug fixes already merged on main:
**embedded Guava is raised to 33.8.0-jre** (CVE-2026-102554), and
**`DataSourceStatusProvider` listeners get a terminal OFF status when
`LDClient` closes**, so shutdown no longer leaves apps waiting on a
final status update.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8d8b4d2. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@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.

2 participants