diff --git a/providers/go-feature-flag/README.md b/providers/go-feature-flag/README.md
index e2eaa6bb1..ddadb5ab8 100644
--- a/providers/go-feature-flag/README.md
+++ b/providers/go-feature-flag/README.md
@@ -82,6 +82,7 @@ You can configure the provider with several options to customize its behavior. T
| **`exporterMetadata`** | `false` | exporterMetadata is the metadata we send to the GO Feature Flag relay proxy when we report the evaluation data usage. |
| **`evaluationFlagList`** | `false` | If you are using in process evaluation, by default we will load in memory all the flags available in the relay proxy. If you want to limit the number of flags loaded in memory, you can use this parameter. By setting this parameter, you will only load the flags available in the list.
If null or empty, all the flags available in the relay proxy will be loaded.
|
| **`flagChangePollingIntervalMs`** | `false` | interval time we poll the proxy to check if the configuration has changed. It is used for the in process evaluation to check if we should refresh our internal cache. default: `120000` |
+| **`wasmEvaluatorPoolSize`** | `false` | _(IN_PROCESS only)_ Number of WASM instances kept in the evaluation pool. Each instance owns independent memory, allowing fully concurrent flag evaluations without serialisation. Must be `>= 1`. _(default: number of available CPU cores)_ |
### Evaluate a feature flag
The OpenFeature client is used to retrieve values for the current `EvaluationContext`. For example, retrieving a boolean value for the flag **"my-flag"**:
diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java
index cfa45664a..9cd620553 100644
--- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java
+++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java
@@ -91,6 +91,14 @@ public class GoFeatureFlagProviderOptions {
*/
private Long flagChangePollingIntervalMs;
+ /**
+ * (optional) Number of WASM instances kept in the evaluation pool for in-process evaluation.
+ * Each instance owns independent WASM linear memory, allowing fully concurrent evaluations
+ * without serialisation. Must be >= 1 when set explicitly.
+ * Default: number of available CPU cores.
+ */
+ private Integer wasmEvaluatorPoolSize;
+
/**
* Validate the options provided to the provider.
*
@@ -107,6 +115,10 @@ public void validate() throws InvalidOptions {
throw new InvalidEndpoint("malformed endpoint: " + getEndpoint());
}
+ if (getWasmEvaluatorPoolSize() != null && getWasmEvaluatorPoolSize() < 1) {
+ throw new InvalidOptions("wasmEvaluatorPoolSize must be at least 1");
+ }
+
if (getExporterMetadata() != null) {
val acceptableExporterMetadataTypes = List.of("String", "Boolean", "Integer", "Double");
for (Map.Entry entry : getExporterMetadata().entrySet()) {
diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/evaluator/InProcessEvaluator.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/evaluator/InProcessEvaluator.java
index d8c752db7..4a63cb3ad 100644
--- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/evaluator/InProcessEvaluator.java
+++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/evaluator/InProcessEvaluator.java
@@ -6,7 +6,7 @@
import dev.openfeature.contrib.providers.gofeatureflag.bean.FlagConfigResponse;
import dev.openfeature.contrib.providers.gofeatureflag.bean.GoFeatureFlagResponse;
import dev.openfeature.contrib.providers.gofeatureflag.util.Const;
-import dev.openfeature.contrib.providers.gofeatureflag.wasm.EvaluationWasm;
+import dev.openfeature.contrib.providers.gofeatureflag.wasm.WasmEvaluatorPool;
import dev.openfeature.contrib.providers.gofeatureflag.wasm.bean.FlagContext;
import dev.openfeature.contrib.providers.gofeatureflag.wasm.bean.WasmInput;
import dev.openfeature.sdk.ErrorCode;
@@ -16,7 +16,6 @@
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
-import io.reactivex.rxjava3.subjects.PublishSubject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -35,23 +34,35 @@
public class InProcessEvaluator implements IEvaluator {
/** API to contact GO Feature Flag. */
private final GoFeatureFlagApi api;
- /** WASM evaluation engine. */
- private final EvaluationWasm evaluationEngine;
+ /** Pool of WASM evaluation engine instances for thread-safe concurrent evaluation. */
+ private final WasmEvaluatorPool evaluationPool;
/** Options to configure the provider. */
private final GoFeatureFlagProviderOptions options;
/** Method to call when we have a configuration change. */
private final Consumer emitProviderConfigurationChanged;
- /** Local copy of the flags' configuration. */
- private Map flags;
- /** Evaluation context enrichment. */
- private Map evaluationContextEnrichment;
- /** Last hash of the flags' configuration. */
- private String etag;
- /** Last update of the flags' configuration. */
- private Date lastUpdate;
+ /** Immutable snapshot of all flag configuration state; updated atomically by the polling daemon. */
+ private volatile EvaluatorState state;
/** disposable which manage the polling of the flag configurations. */
private Disposable configurationDisposable;
+ private static final class EvaluatorState {
+ final Map flags;
+ final Map evaluationContextEnrichment;
+ final String etag;
+ final Date lastUpdate;
+
+ EvaluatorState(
+ Map flags,
+ Map evaluationContextEnrichment,
+ String etag,
+ Date lastUpdate) {
+ this.flags = flags;
+ this.evaluationContextEnrichment = evaluationContextEnrichment;
+ this.etag = etag;
+ this.lastUpdate = lastUpdate;
+ }
+ }
+
/**
* Constructor of the InProcessEvaluator.
*
@@ -64,17 +75,19 @@ public InProcessEvaluator(
GoFeatureFlagProviderOptions options,
Consumer emitProviderConfigurationChanged) {
this.api = api;
- this.flags = Collections.emptyMap();
- this.etag = "";
this.options = options;
- this.lastUpdate = new Date(0);
this.emitProviderConfigurationChanged = emitProviderConfigurationChanged;
- this.evaluationEngine = new EvaluationWasm();
+ this.state = new EvaluatorState(Collections.emptyMap(), null, "", new Date(0));
+ int poolSize = options.getWasmEvaluatorPoolSize() != null
+ ? options.getWasmEvaluatorPoolSize()
+ : Const.DEFAULT_WASM_EVALUATOR_POOL_SIZE;
+ this.evaluationPool = new WasmEvaluatorPool(poolSize);
}
@Override
public GoFeatureFlagResponse evaluate(String key, Object defaultValue, EvaluationContext evaluationContext) {
- if (this.flags.get(key) == null) {
+ EvaluatorState current = this.state;
+ if (current.flags.get(key) == null) {
val err = new GoFeatureFlagResponse();
err.setReason(Reason.ERROR.name());
err.setErrorCode(ErrorCode.FLAG_NOT_FOUND.name());
@@ -85,30 +98,29 @@ public GoFeatureFlagResponse evaluate(String key, Object defaultValue, Evaluatio
val wasmInput = WasmInput.builder()
.flagContext(FlagContext.builder()
.defaultSdkValue(defaultValue)
- .evaluationContextEnrichment(this.evaluationContextEnrichment)
+ .evaluationContextEnrichment(current.evaluationContextEnrichment)
.build())
.evalContext(evaluationContext.asObjectMap())
- .flag(this.flags.get(key))
+ .flag(current.flags.get(key))
.flagKey(key)
.build();
- return this.evaluationEngine.evaluate(wasmInput);
+ return this.evaluationPool.evaluate(wasmInput);
}
@Override
public boolean isFlagTrackable(final String flagKey) {
- Flag flag = this.flags.get(flagKey);
+ Flag flag = this.state.flags.get(flagKey);
return flag != null && (flag.getTrackEvents() == null || flag.getTrackEvents());
}
@Override
public void init() {
- val configFlags = api.retrieveFlagConfiguration(this.etag, options.getEvaluationFlagList());
- this.flags = configFlags.getFlags();
- this.etag = configFlags.getEtag();
- this.lastUpdate = configFlags.getLastUpdated();
- this.evaluationContextEnrichment = configFlags.getEvaluationContextEnrichment();
- // We call the WASM engine to avoid a cold start at the 1st evaluation
- this.evaluationEngine.preWarmWasm();
+ val configFlags = api.retrieveFlagConfiguration(this.state.etag, options.getEvaluationFlagList());
+ this.state = new EvaluatorState(
+ configFlags.getFlags(),
+ configFlags.getEvaluationContextEnrichment(),
+ configFlags.getEtag(),
+ configFlags.getLastUpdated());
// start the polling of the flag configuration
this.configurationDisposable = startCheckFlagConfigurationChangesDaemon();
@@ -131,13 +143,11 @@ private Disposable startCheckFlagConfigurationChangesDaemon() {
? options.getFlagChangePollingIntervalMs()
: Const.DEFAULT_POLLING_CONFIG_FLAG_CHANGE_INTERVAL_MS;
- PublishSubject