Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package sdktest;

import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.server.interfaces.FlagChangeListener;
import com.launchdarkly.sdk.server.interfaces.FlagTracker;
import com.launchdarkly.sdk.server.interfaces.FlagValueChangeListener;

import org.slf4j.Logger;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import sdktest.Representations.FlagChangeNotification;
import sdktest.Representations.FlagValueChangeNotification;

/**
* The flag change listeners that the test harness has registered with one SDK client, keyed by
* the listener ID the harness assigned. Each listener posts a notification to its callback URI
* whenever the SDK fires it. Registering an ID that is already in use replaces the earlier
* listener.
*/
final class ListenerRegistry {
private final FlagTracker tracker;
private final Logger logger;
private final Map<String, FlagChangeListener> listeners = new HashMap<>();

ListenerRegistry(FlagTracker tracker, Logger logger) {
this.tracker = tracker;
this.logger = logger;
}

/**
* Registers a general flag change listener. Every flag change event the SDK delivers is posted
* as a notification carrying the flag key.
*/
synchronized void registerFlagChangeListener(String listenerId, URI callbackUri) {
HookCallbackService callback = new HookCallbackService(callbackUri);
FlagChangeListener listener = event -> {
FlagChangeNotification notification = new FlagChangeNotification();
notification.listenerId = listenerId;
notification.flagKey = event.getKey();
post(callback, listenerId, notification);
};
tracker.addFlagChangeListener(listener);
store(listenerId, listener);
}

/**
* Registers a flag value change listener for one flag and context. The SDK re-evaluates the flag
* on each change and fires only when the value differs; the notification carries both values.
* The Java SDK's API takes no application default value, so the harness's default is accepted
* but not used.
*/
synchronized void registerFlagValueChangeListener(String listenerId, String flagKey, LDContext context,
URI callbackUri) {
HookCallbackService callback = new HookCallbackService(callbackUri);
FlagValueChangeListener valueListener = event -> {
FlagValueChangeNotification notification = new FlagValueChangeNotification();
notification.listenerId = listenerId;
notification.flagKey = event.getKey();
notification.oldValue = event.getOldValue();
notification.newValue = event.getNewValue();
post(callback, listenerId, notification);
};
FlagChangeListener subscription = tracker.addFlagValueChangeListener(flagKey, context, valueListener);
store(listenerId, subscription);
}

/**
* Removes a listener so that it posts no further notifications.
*
* @return false if no listener has that ID
*/
synchronized boolean unregister(String listenerId) {
FlagChangeListener listener = listeners.remove(listenerId);
if (listener == null) {
return false;
}
tracker.removeFlagChangeListener(listener);
return true;
}

/**
* Removes every listener. Called when the SDK client entity closes.
*/
synchronized void closeAll() {
for (FlagChangeListener listener : new ArrayList<>(listeners.values())) {
tracker.removeFlagChangeListener(listener);
}
listeners.clear();
}

private void store(String listenerId, FlagChangeListener listener) {
FlagChangeListener previous = listeners.put(listenerId, listener);
if (previous != null) {
tracker.removeFlagChangeListener(previous);
}
}

// The SDK calls listeners from a worker thread. A callback failure must not propagate into the
// SDK's dispatch, so it is logged instead.
private void post(HookCallbackService callback, String listenerId, Object notification) {
try {
callback.post(notification);
} catch (RuntimeException e) {
logger.warn("Listener {} could not post its notification: {}", listenerId, e.toString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,41 @@ public static class CommandParams {
MigrationVariationParams migrationVariation;

MigrationOperationParams migrationOperation;

RegisterFlagChangeListenerParams registerFlagChangeListener;
RegisterFlagValueChangeListenerParams registerFlagValueChangeListener;
UnregisterListenerParams unregisterListener;
}

public static class RegisterFlagChangeListenerParams {
String listenerId;
URI callbackUri;
}

public static class RegisterFlagValueChangeListenerParams {
String listenerId;
String flagKey;
LDContext context;
LDValue defaultValue;
URI callbackUri;
}

public static class UnregisterListenerParams {
String listenerId;
}

// Posted to a listener's callback URI when a general flag change listener fires.
public static class FlagChangeNotification {
String listenerId;
String flagKey;
}

// Posted to a listener's callback URI when a flag value change listener fires.
public static class FlagValueChangeNotification {
String listenerId;
String flagKey;
LDValue oldValue;
LDValue newValue;
}

public static class EvaluateFlagParams {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
import sdktest.Representations.EvaluateFlagResponse;
import sdktest.Representations.GetBigSegmentsStoreStatusResponse;
import sdktest.Representations.IdentifyEventParams;
import sdktest.Representations.RegisterFlagChangeListenerParams;
import sdktest.Representations.RegisterFlagValueChangeListenerParams;
import sdktest.Representations.UnregisterListenerParams;
import sdktest.Representations.HookConfig;
import sdktest.Representations.SdkConfigHookParams;
import sdktest.Representations.SdkConfigParams;
Expand All @@ -76,6 +79,7 @@

public class SdkClientEntity {
private final LDClient client;
private final ListenerRegistry listeners;
final Logger logger;

public SdkClientEntity(TestService owner, CreateInstanceParams params) {
Expand All @@ -84,6 +88,7 @@ public SdkClientEntity(TestService owner, CreateInstanceParams params) {

LDConfig config = buildSdkConfig(params.configuration, params.tag);
this.client = new LDClient(params.configuration.credential, config);
this.listeners = new ListenerRegistry(client.getFlagTracker(), logger);
if (!client.isInitialized() && !params.configuration.initCanFail) {
throw new RuntimeException("client initialization failed or timed out");
}
Expand Down Expand Up @@ -129,6 +134,20 @@ public Object doCommand(CommandParams params) throws TestService.BadRequestExcep
return res;
case "migrationOperation":
return doMigrationOperation(params.migrationOperation);
case "registerFlagChangeListener":
RegisterFlagChangeListenerParams rfcl = params.registerFlagChangeListener;
listeners.registerFlagChangeListener(rfcl.listenerId, rfcl.callbackUri);
return null;
case "registerFlagValueChangeListener":
RegisterFlagValueChangeListenerParams rfvcl = params.registerFlagValueChangeListener;
listeners.registerFlagValueChangeListener(rfvcl.listenerId, rfvcl.flagKey, rfvcl.context, rfvcl.callbackUri);
return null;
case "unregisterListener":
UnregisterListenerParams ul = params.unregisterListener;
if (!listeners.unregister(ul.listenerId)) {
throw new TestService.BadRequestException("no listener with id " + ul.listenerId);
}
return null;
default:
throw new TestService.BadRequestException("unknown command: " + params.command);
}
Expand Down Expand Up @@ -372,6 +391,7 @@ private MigrationExecution getExecution(String execution) {
}

public void close() {
listeners.closeAll();
try {
client.close();
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class TestService {
"event-gzip",
"event-sampling",
"filtering",
"flag-change-listeners",
"flag-value-change-listeners",
"hook-environment-id",
"inline-context-all",
"migrations",
Expand Down
Loading