Skip to content

Commit d6b623d

Browse files
Use a shared volatile
Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
1 parent 5f57a5e commit d6b623d

2 files changed

Lines changed: 47 additions & 42 deletions

File tree

providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/evaluator/InProcessEvaluator.java

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,29 @@ public class InProcessEvaluator implements IEvaluator {
4040
private final GoFeatureFlagProviderOptions options;
4141
/** Method to call when we have a configuration change. */
4242
private final Consumer<ProviderEventDetails> emitProviderConfigurationChanged;
43-
/** Local copy of the flags' configuration. */
44-
private volatile Map<String, Flag> flags;
45-
/** Evaluation context enrichment. */
46-
private volatile Map<String, Object> evaluationContextEnrichment;
47-
/** Last hash of the flags' configuration. */
48-
private volatile String etag;
49-
/** Last update of the flags' configuration. */
50-
private volatile Date lastUpdate;
43+
/** Immutable snapshot of all flag configuration state; updated atomically by the polling daemon. */
44+
private volatile EvaluatorState state;
5145
/** disposable which manage the polling of the flag configurations. */
5246
private Disposable configurationDisposable;
5347

48+
private static final class EvaluatorState {
49+
final Map<String, Flag> flags;
50+
final Map<String, Object> evaluationContextEnrichment;
51+
final String etag;
52+
final Date lastUpdate;
53+
54+
EvaluatorState(
55+
Map<String, Flag> flags,
56+
Map<String, Object> evaluationContextEnrichment,
57+
String etag,
58+
Date lastUpdate) {
59+
this.flags = flags;
60+
this.evaluationContextEnrichment = evaluationContextEnrichment;
61+
this.etag = etag;
62+
this.lastUpdate = lastUpdate;
63+
}
64+
}
65+
5466
/**
5567
* Constructor of the InProcessEvaluator.
5668
*
@@ -63,11 +75,9 @@ public InProcessEvaluator(
6375
GoFeatureFlagProviderOptions options,
6476
Consumer<ProviderEventDetails> emitProviderConfigurationChanged) {
6577
this.api = api;
66-
this.flags = Collections.emptyMap();
67-
this.etag = "";
6878
this.options = options;
69-
this.lastUpdate = new Date(0);
7079
this.emitProviderConfigurationChanged = emitProviderConfigurationChanged;
80+
this.state = new EvaluatorState(Collections.emptyMap(), null, "", new Date(0));
7181
int poolSize = options.getWasmEvaluatorPoolSize() != null
7282
? options.getWasmEvaluatorPoolSize()
7383
: Const.DEFAULT_WASM_EVALUATOR_POOL_SIZE;
@@ -76,8 +86,8 @@ public InProcessEvaluator(
7686

7787
@Override
7888
public GoFeatureFlagResponse evaluate(String key, Object defaultValue, EvaluationContext evaluationContext) {
79-
Map<String, Flag> currentFlags = this.flags;
80-
if (currentFlags.get(key) == null) {
89+
EvaluatorState current = this.state;
90+
if (current.flags.get(key) == null) {
8191
val err = new GoFeatureFlagResponse();
8292
err.setReason(Reason.ERROR.name());
8393
err.setErrorCode(ErrorCode.FLAG_NOT_FOUND.name());
@@ -88,30 +98,29 @@ public GoFeatureFlagResponse evaluate(String key, Object defaultValue, Evaluatio
8898
val wasmInput = WasmInput.builder()
8999
.flagContext(FlagContext.builder()
90100
.defaultSdkValue(defaultValue)
91-
.evaluationContextEnrichment(this.evaluationContextEnrichment)
101+
.evaluationContextEnrichment(current.evaluationContextEnrichment)
92102
.build())
93103
.evalContext(evaluationContext.asObjectMap())
94-
.flag(currentFlags.get(key))
104+
.flag(current.flags.get(key))
95105
.flagKey(key)
96106
.build();
97107
return this.evaluationPool.evaluate(wasmInput);
98108
}
99109

100110
@Override
101111
public boolean isFlagTrackable(final String flagKey) {
102-
Flag flag = this.flags.get(flagKey);
112+
Flag flag = this.state.flags.get(flagKey);
103113
return flag != null && (flag.getTrackEvents() == null || flag.getTrackEvents());
104114
}
105115

106116
@Override
107117
public void init() {
108-
val configFlags = api.retrieveFlagConfiguration(this.etag, options.getEvaluationFlagList());
109-
this.flags = configFlags.getFlags();
110-
this.etag = configFlags.getEtag();
111-
this.lastUpdate = configFlags.getLastUpdated();
112-
this.evaluationContextEnrichment = configFlags.getEvaluationContextEnrichment();
113-
// We call the WASM engine to avoid a cold start at the 1st evaluation
114-
this.evaluationPool.preWarmWasm();
118+
val configFlags = api.retrieveFlagConfiguration(this.state.etag, options.getEvaluationFlagList());
119+
this.state = new EvaluatorState(
120+
configFlags.getFlags(),
121+
configFlags.getEvaluationContextEnrichment(),
122+
configFlags.getEtag(),
123+
configFlags.getLastUpdated());
115124

116125
// start the polling of the flag configuration
117126
this.configurationDisposable = startCheckFlagConfigurationChangesDaemon();
@@ -136,8 +145,8 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
136145

137146
Observable<Long> intervalObservable = Observable.interval(pollingIntervalMs, TimeUnit.MILLISECONDS);
138147
Observable<FlagConfigResponse> apiCallObservable = intervalObservable
139-
.flatMap(tick -> Observable.fromCallable(
140-
() -> this.api.retrieveFlagConfiguration(this.etag, options.getEvaluationFlagList()))
148+
.flatMap(tick -> Observable.fromCallable(() ->
149+
this.api.retrieveFlagConfiguration(this.state.etag, options.getEvaluationFlagList()))
141150
.onErrorResumeNext(e -> {
142151
log.error("error while calling flag configuration API", e);
143152
return Observable.empty();
@@ -146,22 +155,24 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
146155

147156
return apiCallObservable.subscribe(
148157
response -> {
149-
if (response.getEtag().equals(this.etag)) {
158+
EvaluatorState current = this.state;
159+
if (response.getEtag().equals(current.etag)) {
150160
log.debug("flag configuration has not changed: {}", response);
151161
return;
152162
}
153163

154-
if (response.getLastUpdated().before(this.lastUpdate)) {
164+
if (response.getLastUpdated().before(current.lastUpdate)) {
155165
log.info("configuration received is older than the current one");
156166
return;
157167
}
158168

159169
log.info("flag configuration has changed");
160-
this.etag = response.getEtag();
161-
this.lastUpdate = response.getLastUpdated();
162-
val flagChanges = findFlagConfigurationChanges(this.flags, response.getFlags());
163-
this.flags = response.getFlags();
164-
this.evaluationContextEnrichment = response.getEvaluationContextEnrichment();
170+
val flagChanges = findFlagConfigurationChanges(current.flags, response.getFlags());
171+
this.state = new EvaluatorState(
172+
response.getFlags(),
173+
response.getEvaluationContextEnrichment(),
174+
response.getEtag(),
175+
response.getLastUpdated());
165176
val changeDetails = ProviderEventDetails.builder()
166177
.flagsChanged(flagChanges)
167178
.message("flag configuration has changed")

providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/wasm/WasmEvaluatorPool.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ public final class WasmEvaluatorPool {
2929
public WasmEvaluatorPool(int size) throws WasmFileNotFound {
3030
this.pool = new ArrayBlockingQueue<>(size);
3131
for (int i = 0; i < size; i++) {
32-
if (!pool.offer(new EvaluationWasm())) {
32+
EvaluationWasm instance = new EvaluationWasm();
33+
instance.preWarmWasm();
34+
if (!pool.offer(instance)) {
3335
log.warn(
3436
"Failed to add WASM instance {} to pool during initialisation"
3537
+ " — pool capacity may be exceeded",
@@ -62,16 +64,8 @@ public GoFeatureFlagResponse evaluate(WasmInput wasmInput) {
6264
return instance.evaluate(wasmInput);
6365
} finally {
6466
if (!pool.offer(instance)) {
65-
log.warn("Failed to return WASM instance to pool — pool may be exhausted");
67+
log.error("Failed to return WASM instance to pool — instance leaked, pool capacity may be compromised");
6668
}
6769
}
6870
}
69-
70-
/**
71-
* Pre-warms all instances in the pool to avoid cold-start latency on
72-
* the first evaluation after provider initialisation.
73-
*/
74-
public void preWarmWasm() {
75-
pool.forEach(EvaluationWasm::preWarmWasm);
76-
}
7771
}

0 commit comments

Comments
 (0)