Repository navigation
feat: Apply RETRY to FDv2 synchronizer orchestration #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
+3,090
−2,119
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
5504b2e
feat: Park FDv2 synchronizers after unexpected failures
beekld e731444
refactor: Rename retryReset to resetThresholdMet
beekld 017eba3
feat: Reset park counts and add unpark conditions
beekld 987fd44
feat: Keep the current synchronizer when no other synchronizer is ava…
beekld 99e9e0f
test: Cover unpark conditions and FDv1 fallback parking
beekld 42f0a23
feat: End and extend the FDv1 fallback after its TTL
beekld 4accf45
refactor: Replace synchronizer conditions with deadlines checked on e…
beekld 1c8dd63
refactor: Remove the unused executor from FDv2DataSource
beekld 80e3b09
test: Stop the adapter test from holding a common-pool thread
beekld 465970a
fix: Report INTERRUPTED while waiting for a synchronizer to unpark
beekld bc32ee3
fix: Report the reset threshold from the FDv1 fallback adapter
beekld d81a646
fix: Report a terminal error when the stream thread ends unexpectedly
beekld d62ab41
test: Remove condition tests that assert nothing
beekld a700bff
fix: Wait for the data source to stop before reporting OFF on close
beekld 37cda43
fix: Keep synchronizing when a synchronizer throws from close
beekld cb44a7a
fix: Restart the streaming reset measurement on failures that keep th…
beekld 00f4bd6
fix: Report an error when a synchronizer shuts down by itself
beekld 929ae4a
docs: Use the spec's terms for full payloads and bases in comments
beekld 6e7c4c8
fix: Stop the stream when it connects again after close
beekld 99840d0
refactor: State the recovery condition directly
beekld 94f7478
fix: Say the configured synchronizers instead of FDv2 in fallback logs
beekld File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/AsyncLatch.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package com.launchdarkly.sdk.server; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.WeakHashMap; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| /** | ||
| * A latch that is released once, and that callers wait on through futures. | ||
| * <p> | ||
| * This exists so that a loop can wait for "a result or the latch, whichever comes first" on every iteration without | ||
| * a memory leak. The obvious way to write that wait is {@code CompletableFuture.anyOf(result, latchFuture)}. But | ||
| * {@code anyOf} attaches a completion node to each future it is given, and a pending {@link CompletableFuture} keeps | ||
| * those nodes until it completes. If the latch used one shared future, every iteration that ended with a result would | ||
| * leave one more node on that future. The latch can stay pending for the life of the process, so the nodes would | ||
| * accumulate without limit. | ||
| * <p> | ||
| * To prevent this, {@link #newFuture()} returns a new future for each wait. The latch holds these futures weakly, so a | ||
| * future that nobody waits on any more is garbage collected, together with the nodes that {@code anyOf} attached to | ||
| * it. When the latch is released, it completes each future that is still waiting. After the release, | ||
| * {@link #newFuture()} returns a future that is already complete. | ||
| */ | ||
| final class AsyncLatch { | ||
| private final Object lock = new Object(); | ||
|
|
||
| // The futures that wait for the release, or null after the release. | ||
| private Set<CompletableFuture<Void>> pending = Collections.newSetFromMap(new WeakHashMap<>()); | ||
|
|
||
| /** | ||
| * A new future that completes when the latch is released. | ||
| * | ||
| * @return the future, which is already complete if the latch has been released | ||
| */ | ||
| CompletableFuture<Void> newFuture() { | ||
| CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| synchronized (lock) { | ||
| if (pending != null) { | ||
| pending.add(future); | ||
| return future; | ||
| } | ||
| } | ||
| future.complete(null); | ||
| return future; | ||
| } | ||
|
|
||
| /** | ||
| * Releases the latch, which completes every future from {@link #newFuture()}. Releasing it again has no effect. | ||
| */ | ||
| void release() { | ||
| List<CompletableFuture<Void>> waiting; | ||
| synchronized (lock) { | ||
| if (pending == null) { | ||
| return; | ||
| } | ||
| // The list holds the futures strongly, so none is garbage collected before it completes. | ||
| waiting = new ArrayList<>(pending); | ||
| pending = null; | ||
| } | ||
| for (CompletableFuture<Void> future : waiting) { | ||
| future.complete(null); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you can conclude success from consecutive valid status.
StreamProcessor calls updateStatus(State.VALID, null) at the end of handleMessage for every message event, including the default: branch that logs "Unexpected event found in stream" (StreamProcessor.java:327-331). Two unknown event types in a row (the forward-compatibility case: any new server event name) produce two VALIDs with zero data delivered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this code kind of assumes that the FDv1 fallback is a polling synchronizer. Making this work for any kind of synchronizer would require adding a way to signal the reset from any kind of FDv1 DataSource, and I don't think that's worth the effort, given FDv1 isn't the future anyway.