Skip to content

Commit 4402e66

Browse files
committed
feat: improve wait logic to a more elegant solution open-feature#1160
Signed-off-by: christian.lutnik <[email protected]>
1 parent 969448a commit 4402e66

File tree

6 files changed

+201
-58
lines changed

6 files changed

+201
-58
lines changed

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
44
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
5+
import dev.openfeature.contrib.providers.flagd.resolver.common.Wait;
56
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
67
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
78
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
@@ -35,6 +36,7 @@ public class FlagdProvider extends EventProvider {
3536
private volatile Structure syncMetadata = new ImmutableStructure();
3637
private volatile EvaluationContext enrichedContext = new ImmutableContext();
3738
private final List<Hook> hooks = new ArrayList<>();
39+
private final Wait connectionWait = new Wait();
3840

3941
protected final void finalize() {
4042
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
@@ -55,7 +57,7 @@ public FlagdProvider() {
5557
public FlagdProvider(final FlagdOptions options) {
5658
switch (options.getResolverType().asString()) {
5759
case Config.RESOLVER_IN_PROCESS:
58-
this.flagResolver = new InProcessResolver(options, this::isConnected, this::onConnectionEvent);
60+
this.flagResolver = new InProcessResolver(options, connectionWait, this::onConnectionEvent);
5961
break;
6062
case Config.RESOLVER_RPC:
6163
this.flagResolver = new GrpcResolver(
@@ -82,6 +84,7 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws
8284

8385
this.flagResolver.init();
8486
this.initialized = this.connected = true;
87+
connectionWait.onFinished();
8588
}
8689

8790
@Override
@@ -151,13 +154,12 @@ EvaluationContext getEnrichedContext() {
151154
return enrichedContext;
152155
}
153156

154-
private boolean isConnected() {
155-
return this.connected;
156-
}
157-
158157
private void onConnectionEvent(ConnectionEvent connectionEvent) {
159158
final boolean wasConnected = connected;
160159
final boolean isConnected = connected = connectionEvent.isConnected();
160+
if (isConnected) {
161+
connectionWait.onFinished();
162+
}
161163

162164
syncMetadata = connectionEvent.getSyncMetadata();
163165
enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java

-39
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import dev.openfeature.sdk.exceptions.GeneralError;
4+
5+
/**
6+
* A helper class to wait for events.
7+
*/
8+
public class Wait {
9+
private volatile boolean isFinished;
10+
11+
/**
12+
* Create a new Wait object.
13+
*/
14+
public Wait() {}
15+
16+
private Wait(boolean isFinished) {
17+
this.isFinished = isFinished;
18+
}
19+
20+
/**
21+
* Blocks the calling thread until either {@link Wait#onFinished()} is called or the deadline is exceeded, whatever
22+
* happens first.
23+
* If the deadline is exceeded, a GeneralError will be thrown.
24+
*
25+
* @param deadline the maximum time in ms to wait
26+
* @throws GeneralError when the deadline is exceeded before {@link Wait#onFinished()} is called on this object
27+
*/
28+
public void waitUntilFinished(long deadline) {
29+
long start = System.currentTimeMillis();
30+
long end = start + deadline;
31+
while (!isFinished) {
32+
long now = System.currentTimeMillis();
33+
// if wait(0) is called, the thread would wait forever, so we abort when this would happen
34+
if (now >= end) {
35+
throw new GeneralError(String.format(
36+
"Deadline exceeded. Condition did not complete within the %d ms deadline", deadline));
37+
}
38+
long remaining = end - now;
39+
synchronized (this) {
40+
if (isFinished) { // might have changed in the meantime
41+
return;
42+
}
43+
try {
44+
this.wait(remaining);
45+
} catch (InterruptedException e) {
46+
// try again. Leave the continue to make PMD happy
47+
continue;
48+
}
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Wake up all threads that have called {@link Wait#waitUntilFinished(long)}.
55+
*/
56+
public void onFinished() {
57+
synchronized (this) {
58+
isFinished = true;
59+
this.notifyAll();
60+
}
61+
}
62+
63+
/**
64+
* Create a new Wait object that is already finished. Calls to {@link Wait#waitUntilFinished(long)} will return
65+
* immediately.
66+
*
67+
* @return an already finished Wait object
68+
*/
69+
public static Wait finished() {
70+
return new Wait(true);
71+
}
72+
}

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java

+6-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
77
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
88
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionState;
9-
import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
9+
import dev.openfeature.contrib.providers.flagd.resolver.common.Wait;
1010
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
1111
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
1212
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage;
@@ -28,7 +28,6 @@
2828
import dev.openfeature.sdk.exceptions.TypeMismatchError;
2929
import java.util.Map;
3030
import java.util.function.Consumer;
31-
import java.util.function.Supplier;
3231
import lombok.extern.slf4j.Slf4j;
3332

3433
/**
@@ -42,7 +41,7 @@ public class InProcessResolver implements Resolver {
4241
private final Consumer<ConnectionEvent> onConnectionEvent;
4342
private final Operator operator;
4443
private final long deadline;
45-
private final Supplier<Boolean> connectedSupplier;
44+
private final Wait connectionWait;
4645
private final String scope;
4746

4847
/**
@@ -51,20 +50,17 @@ public class InProcessResolver implements Resolver {
5150
* Flags are evaluated locally.
5251
*
5352
* @param options flagd options
54-
* @param connectedSupplier lambda providing current connection status from
55-
* caller
53+
* @param connectionWait A {@link Wait} object, which waits until a connection is established
5654
* @param onConnectionEvent lambda which handles changes in the
5755
* connection/stream
5856
*/
5957
public InProcessResolver(
60-
FlagdOptions options,
61-
final Supplier<Boolean> connectedSupplier,
62-
Consumer<ConnectionEvent> onConnectionEvent) {
58+
FlagdOptions options, final Wait connectionWait, Consumer<ConnectionEvent> onConnectionEvent) {
6359
this.flagStore = new FlagStore(getConnector(options, onConnectionEvent));
6460
this.deadline = options.getDeadline();
6561
this.onConnectionEvent = onConnectionEvent;
6662
this.operator = new Operator();
67-
this.connectedSupplier = connectedSupplier;
63+
this.connectionWait = connectionWait;
6864
this.scope = options.getSelector();
6965
}
7066

@@ -97,7 +93,7 @@ public void init() throws Exception {
9793
stateWatcher.start();
9894

9995
// block till ready
100-
Util.busyWaitAndCheck(this.deadline, this.connectedSupplier);
96+
connectionWait.waitUntilFinished(deadline);
10197
}
10298

10399
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import dev.openfeature.sdk.exceptions.GeneralError;
4+
import java.util.concurrent.atomic.AtomicBoolean;
5+
import java.util.concurrent.atomic.AtomicLong;
6+
import org.junit.jupiter.api.Assertions;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.Timeout;
9+
10+
class WaitTest {
11+
private static final long PERMISSIBLE_EPSILON = 20;
12+
13+
@Timeout(2)
14+
@Test
15+
void waitUntilFinished_failsWhenDeadlineElapses() {
16+
final Wait wait = new Wait();
17+
Assertions.assertThrows(GeneralError.class, () -> wait.waitUntilFinished(10));
18+
}
19+
20+
@Timeout(2)
21+
@Test
22+
void waitUntilFinished_WaitsApproxForDeadline() {
23+
final Wait wait = new Wait();
24+
final AtomicLong start = new AtomicLong();
25+
final AtomicLong end = new AtomicLong();
26+
final long deadline = 45;
27+
Assertions.assertThrows(GeneralError.class, () -> {
28+
start.set(System.currentTimeMillis());
29+
try {
30+
wait.waitUntilFinished(deadline);
31+
} catch (Exception e) {
32+
end.set(System.currentTimeMillis());
33+
throw e;
34+
}
35+
});
36+
final long elapsed = end.get() - start.get();
37+
// should wait at least for the deadline
38+
Assertions.assertTrue(elapsed >= deadline);
39+
// should not wait much longer than the deadline
40+
Assertions.assertTrue(elapsed < deadline + PERMISSIBLE_EPSILON);
41+
}
42+
43+
@Timeout(2)
44+
@Test
45+
void interruptingWaitingThread_isIgnored() throws InterruptedException {
46+
final AtomicBoolean isWaiting = new AtomicBoolean();
47+
final Wait wait = new Wait();
48+
final long deadline = 500;
49+
Thread t0 = new Thread(() -> {
50+
long start = System.currentTimeMillis();
51+
isWaiting.set(true);
52+
wait.waitUntilFinished(deadline);
53+
long end = System.currentTimeMillis();
54+
long duration = end - start;
55+
// even though thread was interrupted, it still waited for the deadline
56+
Assertions.assertTrue(duration >= deadline);
57+
Assertions.assertTrue(duration < deadline + PERMISSIBLE_EPSILON);
58+
});
59+
t0.start();
60+
61+
while (!isWaiting.get()) {
62+
Thread.yield();
63+
}
64+
65+
Thread.sleep(10); // t0 should have started waiting in the meantime
66+
67+
for (int i = 0; i < 50; i++) {
68+
t0.interrupt();
69+
Thread.sleep(10);
70+
}
71+
72+
t0.join();
73+
}
74+
75+
@Timeout(2)
76+
@Test
77+
void callingOnFinished_wakesUpWaitingThread() throws InterruptedException {
78+
final AtomicBoolean isWaiting = new AtomicBoolean();
79+
final Wait wait = new Wait();
80+
Thread t0 = new Thread(() -> {
81+
long start = System.currentTimeMillis();
82+
isWaiting.set(true);
83+
wait.waitUntilFinished(10000);
84+
long end = System.currentTimeMillis();
85+
long duration = end - start;
86+
Assertions.assertTrue(duration < PERMISSIBLE_EPSILON);
87+
});
88+
t0.start();
89+
90+
while (!isWaiting.get()) {
91+
Thread.yield();
92+
}
93+
94+
Thread.sleep(10); // t0 should have started waiting in the meantime
95+
96+
wait.onFinished();
97+
98+
t0.join();
99+
}
100+
101+
@Timeout(2)
102+
@Test
103+
void waitingOnFinished_returnsInstantly() {
104+
Wait finished = Wait.finished();
105+
long start = System.currentTimeMillis();
106+
finished.waitUntilFinished(10000);
107+
long end = System.currentTimeMillis();
108+
// do not use PERMISSIBLE_EPSILON here, this should happen faster than that
109+
Assertions.assertTrue(start + 2 > end);
110+
}
111+
}

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import dev.openfeature.contrib.providers.flagd.Config;
2222
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
2323
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
24+
import dev.openfeature.contrib.providers.flagd.resolver.common.Wait;
2425
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
2526
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.MockConnector;
2627
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState;
@@ -517,16 +518,16 @@ void flagSetMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception {
517518
private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage)
518519
throws NoSuchFieldException, IllegalAccessException {
519520

520-
final InProcessResolver resolver = new InProcessResolver(options, () -> true, connectionEvent -> {});
521+
final InProcessResolver resolver = new InProcessResolver(options, Wait.finished(), connectionEvent -> {});
521522
return injectFlagStore(resolver, storage);
522523
}
523524

524525
private InProcessResolver getInProcessResolverWith(
525526
final MockStorage storage, final Consumer<ConnectionEvent> onConnectionEvent)
526527
throws NoSuchFieldException, IllegalAccessException {
527528

528-
final InProcessResolver resolver =
529-
new InProcessResolver(FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent);
529+
final InProcessResolver resolver = new InProcessResolver(
530+
FlagdOptions.builder().deadline(1000).build(), Wait.finished(), onConnectionEvent);
530531
return injectFlagStore(resolver, storage);
531532
}
532533

@@ -535,7 +536,7 @@ private InProcessResolver getInProcessResolverWith(
535536
throws NoSuchFieldException, IllegalAccessException {
536537

537538
final InProcessResolver resolver = new InProcessResolver(
538-
FlagdOptions.builder().selector(selector).deadline(1000).build(), () -> true, onConnectionEvent);
539+
FlagdOptions.builder().selector(selector).deadline(1000).build(), Wait.finished(), onConnectionEvent);
539540
return injectFlagStore(resolver, storage);
540541
}
541542

0 commit comments

Comments
 (0)