com.launchdarkly.sdk.server.LDClient Maven / Gradle / Ivy
Show all versions of launchdarkly-java-server-sdk Show documentation
package com.launchdarkly.sdk.server;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.LogValues;
import com.launchdarkly.sdk.EvaluationDetail;
import com.launchdarkly.sdk.EvaluationReason;
import com.launchdarkly.sdk.LDUser;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;
import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder;
import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider;
import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration;
import com.launchdarkly.sdk.server.interfaces.DataSource;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates;
import com.launchdarkly.sdk.server.interfaces.DataStore;
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems;
import com.launchdarkly.sdk.server.interfaces.Event;
import com.launchdarkly.sdk.server.interfaces.EventProcessor;
import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent;
import com.launchdarkly.sdk.server.interfaces.FlagChangeListener;
import com.launchdarkly.sdk.server.interfaces.FlagTracker;
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
import org.apache.commons.codec.binary.Hex;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
import static com.launchdarkly.sdk.server.Util.isAsciiHeaderValue;
/**
* A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate
* a single {@code LDClient} for the lifetime of their application.
*/
public final class LDClient implements LDClientInterface {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private final String sdkKey;
private final boolean offline;
private final Evaluator evaluator;
final EventProcessor eventProcessor;
final DataSource dataSource;
final DataStore dataStore;
private final BigSegmentStoreStatusProvider bigSegmentStoreStatusProvider;
private final BigSegmentStoreWrapper bigSegmentStoreWrapper;
private final DataSourceUpdates dataSourceUpdates;
private final DataStoreStatusProviderImpl dataStoreStatusProvider;
private final DataSourceStatusProviderImpl dataSourceStatusProvider;
private final FlagTrackerImpl flagTracker;
private final EventBroadcasterImpl flagChangeBroadcaster;
private final ScheduledExecutorService sharedExecutor;
private final EventFactory eventFactoryDefault;
private final EventFactory eventFactoryWithReasons;
private final LDLogger baseLogger;
private final LDLogger evaluationLogger;
private final Evaluator.PrerequisiteEvaluationSink prereqEvalsDefault;
private final Evaluator.PrerequisiteEvaluationSink prereqEvalsWithReasons;
/**
* Creates a new client instance that connects to LaunchDarkly with the default configuration.
*
* If you need to specify any custom SDK options, use {@link LDClient#LDClient(String, LDConfig)}
* instead.
*
* Applications should instantiate a single instance for the lifetime of the application. In
* unusual cases where an application needs to evaluate feature flags from different LaunchDarkly
* projects or environments, you may create multiple clients, but they should still be retained
* for the lifetime of the application rather than created per request or per thread.
*
* The client will begin attempting to connect to LaunchDarkly as soon as you call the constructor.
* The constructor will return when it successfully connects, or when the default timeout of 5 seconds
* expires, whichever comes first. If it has not succeeded in connecting when the timeout elapses,
* you will receive the client in an uninitialized state where feature flags will return default
* values; it will still continue trying to connect in the background. You can detect whether
* initialization has succeeded by calling {@link #isInitialized()}. If you prefer to customize
* this behavior, use {@link LDClient#LDClient(String, LDConfig)} instead.
*
* For rules regarding the throwing of unchecked exceptions for error conditions, see
* {@link LDClient#LDClient(String, LDConfig)}.
*
* @param sdkKey the SDK key for your LaunchDarkly environment
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
* for security reasons, in case of an illegal SDK key, the exception message does
* not include the key
* @throws NullPointerException if a non-nullable parameter was null
* @see LDClient#LDClient(String, LDConfig)
*/
public LDClient(String sdkKey) {
// COVERAGE: this constructor cannot be called in unit tests because it uses the default base
// URI and will attempt to make a live connection to LaunchDarkly.
this(sdkKey, LDConfig.DEFAULT);
}
private static final DataModel.FeatureFlag getFlag(DataStore store, String key) {
ItemDescriptor item = store.get(FEATURES, key);
return item == null ? null : (DataModel.FeatureFlag)item.getItem();
}
private static final DataModel.Segment getSegment(DataStore store, String key) {
ItemDescriptor item = store.get(SEGMENTS, key);
return item == null ? null : (DataModel.Segment)item.getItem();
}
/**
* Creates a new client to connect to LaunchDarkly with a custom configuration.
*
* This constructor can be used to configure advanced SDK features; see {@link LDConfig.Builder}.
*
* Applications should instantiate a single instance for the lifetime of the application. In
* unusual cases where an application needs to evaluate feature flags from different LaunchDarkly
* projects or environments, you may create multiple clients, but they should still be retained
* for the lifetime of the application rather than created per request or per thread.
*
* Unless it is configured to be offline with {@link LDConfig.Builder#offline(boolean)} or
* {@link Components#externalUpdatesOnly()}, the client will begin attempting to connect to
* LaunchDarkly as soon as you call the constructor. The constructor will return when it successfully
* connects, or when the timeout set by {@link LDConfig.Builder#startWait(java.time.Duration)} (default:
* 5 seconds) expires, whichever comes first. If it has not succeeded in connecting when the timeout
* elapses, you will receive the client in an uninitialized state where feature flags will return
* default values; it will still continue trying to connect in the background. You can detect
* whether initialization has succeeded by calling {@link #isInitialized()}.
*
* If you prefer to have the constructor return immediately, and then wait for initialization to finish
* at some other point, you can use {@link #getDataSourceStatusProvider()} as follows:
*
* LDConfig config = new LDConfig.Builder()
* .startWait(Duration.ZERO)
* .build();
* LDClient client = new LDClient(sdkKey, config);
*
* // later, when you want to wait for initialization to finish:
* boolean inited = client.getDataSourceStatusProvider().waitFor(
* DataSourceStatusProvider.State.VALID, Duration.ofSeconds(10));
* if (!inited) {
* // do whatever is appropriate if initialization has timed out
* }
*
*
* This constructor can throw unchecked exceptions if it is immediately apparent that
* the SDK cannot work with these parameters. For instance, if the SDK key contains a
* non-printable character that cannot be used in an HTTP header, it will throw an
* {@link IllegalArgumentException} since the SDK key is normally sent to LaunchDarkly
* in an HTTP header and no such value could possibly be valid. Similarly, a null
* value for a non-nullable parameter may throw a {@link NullPointerException}. The
* constructor will not throw an exception for any error condition that could only be
* detected after making a request to LaunchDarkly (such as an SDK key that is simply
* wrong despite being valid ASCII, so it is invalid but not illegal); those are logged
* and treated as an unsuccessful initialization, as described above.
*
* @param sdkKey the SDK key for your LaunchDarkly environment
* @param config a client configuration object
* @throws IllegalArgumentException if a parameter contained a grossly malformed value;
* for security reasons, in case of an illegal SDK key, the exception message does
* not include the key
* @throws NullPointerException if a non-nullable parameter was null
* @see LDClient#LDClient(String, LDConfig)
*/
public LDClient(String sdkKey, LDConfig config) {
checkNotNull(config, "config must not be null");
this.sdkKey = checkNotNull(sdkKey, "sdkKey must not be null");
if (!isAsciiHeaderValue(sdkKey) ) {
throw new IllegalArgumentException("SDK key contained an invalid character");
}
this.offline = config.offline;
this.sharedExecutor = createSharedExecutor(config);
boolean eventsDisabled = Components.isNullImplementation(config.eventProcessorFactory);
if (eventsDisabled) {
this.eventFactoryDefault = EventFactory.Disabled.INSTANCE;
this.eventFactoryWithReasons = EventFactory.Disabled.INSTANCE;
} else {
this.eventFactoryDefault = EventFactory.DEFAULT;
this.eventFactoryWithReasons = EventFactory.DEFAULT_WITH_REASONS;
}
// Do not create diagnostic accumulator if config has specified is opted out, or if we're not using the
// standard event processor
final boolean useDiagnostics = !config.diagnosticOptOut && config.eventProcessorFactory instanceof EventProcessorBuilder;
final ClientContextImpl context = new ClientContextImpl(
sdkKey,
config,
sharedExecutor,
useDiagnostics ? new DiagnosticAccumulator(new DiagnosticId(sdkKey)) : null
);
this.baseLogger = context.getBasic().getBaseLogger();
this.evaluationLogger = this.baseLogger.subLogger(Loggers.EVALUATION_LOGGER_NAME);
this.eventProcessor = config.eventProcessorFactory.createEventProcessor(context);
EventBroadcasterImpl bigSegmentStoreStatusNotifier =
EventBroadcasterImpl.forBigSegmentStoreStatus(sharedExecutor, baseLogger);
BigSegmentsConfiguration bigSegmentsConfig = config.bigSegmentsConfigBuilder.createBigSegmentsConfiguration(context);
if (bigSegmentsConfig.getStore() != null) {
bigSegmentStoreWrapper = new BigSegmentStoreWrapper(bigSegmentsConfig, bigSegmentStoreStatusNotifier, sharedExecutor,
this.baseLogger.subLogger(Loggers.BIG_SEGMENTS_LOGGER_NAME));
} else {
bigSegmentStoreWrapper = null;
}
bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderImpl(bigSegmentStoreStatusNotifier, bigSegmentStoreWrapper);
EventBroadcasterImpl dataStoreStatusNotifier =
EventBroadcasterImpl.forDataStoreStatus(sharedExecutor, baseLogger);
DataStoreUpdatesImpl dataStoreUpdates = new DataStoreUpdatesImpl(dataStoreStatusNotifier);
this.dataStore = config.dataStoreFactory.createDataStore(context, dataStoreUpdates);
this.evaluator = new Evaluator(new Evaluator.Getters() {
public DataModel.FeatureFlag getFlag(String key) {
return LDClient.getFlag(LDClient.this.dataStore, key);
}
public DataModel.Segment getSegment(String key) {
return LDClient.getSegment(LDClient.this.dataStore, key);
}
public BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key) {
BigSegmentStoreWrapper wrapper = LDClient.this.bigSegmentStoreWrapper;
return wrapper == null ? null : wrapper.getUserMembership(key);
}
}, evaluationLogger);
this.flagChangeBroadcaster = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor, baseLogger);
this.flagTracker = new FlagTrackerImpl(flagChangeBroadcaster,
(key, user) -> jsonValueVariation(key, user, LDValue.ofNull()));
this.dataStoreStatusProvider = new DataStoreStatusProviderImpl(this.dataStore, dataStoreUpdates);
EventBroadcasterImpl dataSourceStatusNotifier =
EventBroadcasterImpl.forDataSourceStatus(sharedExecutor, baseLogger);
DataSourceUpdatesImpl dataSourceUpdates = new DataSourceUpdatesImpl(
dataStore,
dataStoreStatusProvider,
flagChangeBroadcaster,
dataSourceStatusNotifier,
sharedExecutor,
context.getLogging().getLogDataSourceOutageAsErrorAfter(),
baseLogger
);
this.dataSourceUpdates = dataSourceUpdates;
this.dataSource = config.dataSourceFactory.createDataSource(context, dataSourceUpdates);
this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates);
this.prereqEvalsDefault = makePrerequisiteEventSender(false);
this.prereqEvalsWithReasons = makePrerequisiteEventSender(true);
// We pre-create those two callback objects, rather than using inline lambdas when we call the Evaluator,
// because using lambdas would cause a new closure object to be allocated every time.
Future startFuture = dataSource.start();
if (!config.startWait.isZero() && !config.startWait.isNegative()) {
if (!(dataSource instanceof ComponentsImpl.NullDataSource)) {
baseLogger.info("Waiting up to {} milliseconds for LaunchDarkly client to start...",
config.startWait.toMillis());
}
try {
startFuture.get(config.startWait.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
baseLogger.error("Timeout encountered waiting for LaunchDarkly client initialization");
} catch (Exception e) {
baseLogger.error("Exception encountered waiting for LaunchDarkly client initialization: {}",
LogValues.exceptionSummary(e));
baseLogger.debug("{}", LogValues.exceptionTrace(e));
}
if (!dataSource.isInitialized()) {
baseLogger.warn("LaunchDarkly client was not successfully initialized");
}
}
}
@Override
public boolean isInitialized() {
return dataSource.isInitialized();
}
@Override
public void track(String eventName, LDUser user) {
trackData(eventName, user, LDValue.ofNull());
}
@Override
public void trackData(String eventName, LDUser user, LDValue data) {
if (user == null || user.getKey() == null || user.getKey().isEmpty()) {
baseLogger.warn("Track called with null user or null/empty user key!");
} else {
eventProcessor.sendEvent(eventFactoryDefault.newCustomEvent(eventName, user, data, null));
}
}
@Override
public void trackMetric(String eventName, LDUser user, LDValue data, double metricValue) {
if (user == null || user.getKey() == null || user.getKey().isEmpty()) {
baseLogger.warn("Track called with null user or null/empty user key!");
} else {
eventProcessor.sendEvent(eventFactoryDefault.newCustomEvent(eventName, user, data, metricValue));
}
}
@Override
public void identify(LDUser user) {
if (user == null || user.getKey() == null || user.getKey().isEmpty()) {
baseLogger.warn("Identify called with null user or null/empty user key!");
} else {
eventProcessor.sendEvent(eventFactoryDefault.newIdentifyEvent(user));
}
}
private void sendFlagRequestEvent(Event.FeatureRequest event) {
if (event != null) {
eventProcessor.sendEvent(event);
}
}
@Override
public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) {
FeatureFlagsState.Builder builder = FeatureFlagsState.builder(options);
if (isOffline()) {
evaluationLogger.debug("allFlagsState() was called when client is in offline mode.");
}
if (!isInitialized()) {
if (dataStore.isInitialized()) {
evaluationLogger.warn("allFlagsState() was called before client initialized; using last known values from data store");
} else {
evaluationLogger.warn("allFlagsState() was called before client initialized; data store unavailable, returning no data");
return builder.valid(false).build();
}
}
if (user == null || user.getKey() == null) {
evaluationLogger.warn("allFlagsState() was called with null user or null user key! returning no data");
return builder.valid(false).build();
}
boolean clientSideOnly = FlagsStateOption.hasOption(options, FlagsStateOption.CLIENT_SIDE_ONLY);
KeyedItems flags;
try {
flags = dataStore.getAll(FEATURES);
} catch (Exception e) {
evaluationLogger.error("Exception from data store when evaluating all flags: {}", LogValues.exceptionSummary(e));
evaluationLogger.debug(e.toString(), LogValues.exceptionTrace(e));
return builder.valid(false).build();
}
for (Map.Entry entry : flags.getItems()) {
if (entry.getValue().getItem() == null) {
continue; // deleted flag placeholder
}
DataModel.FeatureFlag flag = (DataModel.FeatureFlag)entry.getValue().getItem();
if (clientSideOnly && !flag.isClientSide()) {
continue;
}
try {
EvalResult result = evaluator.evaluate(flag, user, null);
// Note: the null parameter to evaluate() is for the PrerequisiteEvaluationSink; allFlagsState should
// not generate evaluation events, so we don't want the evaluator to generate any prerequisite evaluation
// events either.
builder.addFlag(flag, result);
} catch (Exception e) {
evaluationLogger.error("Exception caught for feature flag \"{}\" when evaluating all flags: {}", entry.getKey(),
LogValues.exceptionSummary(e));
evaluationLogger.debug(e.toString(), LogValues.exceptionTrace(e));
builder.addFlag(flag, EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.exception(e)));
}
}
return builder.build();
}
@Override
public boolean boolVariation(String featureKey, LDUser user, boolean defaultValue) {
return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.BOOLEAN).booleanValue();
}
@Override
public int intVariation(String featureKey, LDUser user, int defaultValue) {
return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.NUMBER).intValue();
}
@Override
public double doubleVariation(String featureKey, LDUser user, double defaultValue) {
return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.NUMBER).doubleValue();
}
@Override
public String stringVariation(String featureKey, LDUser user, String defaultValue) {
return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.STRING).stringValue();
}
@Override
public LDValue jsonValueVariation(String featureKey, LDUser user, LDValue defaultValue) {
return evaluate(featureKey, user, LDValue.normalize(defaultValue), null);
}
@Override
public EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) {
EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue),
LDValueType.BOOLEAN, true);
return result.getAsBoolean();
}
@Override
public EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue) {
EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue),
LDValueType.NUMBER, true);
return result.getAsInteger();
}
@Override
public EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue) {
EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue),
LDValueType.NUMBER, true);
return result.getAsDouble();
}
@Override
public EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue) {
EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue),
LDValueType.STRING, true);
return result.getAsString();
}
@Override
public EvaluationDetail jsonValueVariationDetail(String featureKey, LDUser user, LDValue defaultValue) {
EvalResult result = evaluateInternal(featureKey, user, LDValue.normalize(defaultValue),
null, true);
return result.getAnyType();
}
@Override
public boolean isFlagKnown(String featureKey) {
if (!isInitialized()) {
if (dataStore.isInitialized()) {
baseLogger.warn("isFlagKnown called before client initialized for feature flag \"{}\"; using last known values from data store", featureKey);
} else {
baseLogger.warn("isFlagKnown called before client initialized for feature flag \"{}\"; data store unavailable, returning false", featureKey);
return false;
}
}
try {
if (getFlag(dataStore, featureKey) != null) {
return true;
}
} catch (Exception e) {
baseLogger.error("Encountered exception while calling isFlagKnown for feature flag \"{}\": {}", featureKey,
LogValues.exceptionSummary(e));
baseLogger.debug("{}", LogValues.exceptionTrace(e));
}
return false;
}
private LDValue evaluate(String featureKey, LDUser user, LDValue defaultValue, LDValueType requireType) {
return evaluateInternal(featureKey, user, defaultValue, requireType, false).getValue();
}
private EvalResult errorResult(EvaluationReason.ErrorKind errorKind, final LDValue defaultValue) {
return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.error(errorKind));
}
private EvalResult evaluateInternal(String featureKey, LDUser user, LDValue defaultValue,
LDValueType requireType, boolean withDetail) {
EventFactory eventFactory = withDetail ? eventFactoryWithReasons : eventFactoryDefault;
if (!isInitialized()) {
if (dataStore.isInitialized()) {
evaluationLogger.warn("Evaluation called before client initialized for feature flag \"{}\"; using last known values from data store", featureKey);
} else {
evaluationLogger.warn("Evaluation called before client initialized for feature flag \"{}\"; data store unavailable, returning default value", featureKey);
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.CLIENT_NOT_READY));
return errorResult(EvaluationReason.ErrorKind.CLIENT_NOT_READY, defaultValue);
}
}
DataModel.FeatureFlag featureFlag = null;
try {
featureFlag = getFlag(dataStore, featureKey);
if (featureFlag == null) {
evaluationLogger.info("Unknown feature flag \"{}\"; returning default value", featureKey);
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.FLAG_NOT_FOUND));
return errorResult(EvaluationReason.ErrorKind.FLAG_NOT_FOUND, defaultValue);
}
if (user == null || user.getKey() == null) {
evaluationLogger.warn("Null user or null user key when evaluating flag \"{}\"; returning default value", featureKey);
sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue,
EvaluationReason.ErrorKind.USER_NOT_SPECIFIED));
return errorResult(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, defaultValue);
}
if (user.getKey().isEmpty()) {
evaluationLogger.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly");
}
EvalResult evalResult = evaluator.evaluate(featureFlag, user,
withDetail ? prereqEvalsWithReasons : prereqEvalsDefault);
if (evalResult.isNoVariation()) {
evalResult = EvalResult.of(defaultValue, evalResult.getVariationIndex(), evalResult.getReason());
} else {
LDValue value = evalResult.getValue(); // guaranteed not to be an actual Java null, but can be LDValue.ofNull()
if (requireType != null &&
!value.isNull() &&
value.getType() != requireType) {
evaluationLogger.error("Feature flag evaluation expected result as {}, but got {}", defaultValue.getType(), value.getType());
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.WRONG_TYPE));
return errorResult(EvaluationReason.ErrorKind.WRONG_TYPE, defaultValue);
}
}
sendFlagRequestEvent(eventFactory.newFeatureRequestEvent(featureFlag, user, evalResult, defaultValue));
return evalResult;
} catch (Exception e) {
evaluationLogger.error("Encountered exception while evaluating feature flag \"{}\": {}", featureKey,
LogValues.exceptionSummary(e));
evaluationLogger.debug("{}", LogValues.exceptionTrace(e));
if (featureFlag == null) {
sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue,
EvaluationReason.ErrorKind.EXCEPTION));
} else {
sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue,
EvaluationReason.ErrorKind.EXCEPTION));
}
return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e));
}
}
@Override
public FlagTracker getFlagTracker() {
return flagTracker;
}
@Override
public BigSegmentStoreStatusProvider getBigSegmentStoreStatusProvider() {
return bigSegmentStoreStatusProvider;
}
@Override
public DataStoreStatusProvider getDataStoreStatusProvider() {
return dataStoreStatusProvider;
}
@Override
public DataSourceStatusProvider getDataSourceStatusProvider() {
return dataSourceStatusProvider;
}
@Override
public void close() throws IOException {
baseLogger.info("Closing LaunchDarkly Client");
this.dataStore.close();
this.eventProcessor.close();
this.dataSource.close();
this.dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.OFF, null);
if (this.bigSegmentStoreWrapper != null) {
this.bigSegmentStoreWrapper.close();
}
this.sharedExecutor.shutdownNow();
}
@Override
public void flush() {
this.eventProcessor.flush();
}
@Override
public boolean isOffline() {
return offline;
}
@Override
public String secureModeHash(LDUser user) {
if (user == null || user.getKey() == null) {
return null;
}
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(sdkKey.getBytes(), HMAC_ALGORITHM));
return Hex.encodeHexString(mac.doFinal(user.getKey().getBytes("UTF8")));
} catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e) {
// COVERAGE: there is no way to cause these errors in a unit test.
baseLogger.error("Could not generate secure mode hash: {}", LogValues.exceptionSummary(e));
baseLogger.debug("{}", LogValues.exceptionTrace(e));
}
return null;
}
@Override
public void alias(LDUser user, LDUser previousUser) {
this.eventProcessor.sendEvent(eventFactoryDefault.newAliasEvent(user, previousUser));
}
/**
* Returns the current version string of the client library.
* @return a version string conforming to Semantic Versioning (http://semver.org)
*/
@Override
public String version() {
return Version.SDK_VERSION;
}
// This executor is used for a variety of SDK tasks such as flag change events, checking the data store
// status after an outage, and the poll task in polling mode. These are all tasks that we do not expect
// to be executing frequently so that it is acceptable to use a single thread to execute them one at a
// time rather than a thread pool, thus reducing the number of threads spawned by the SDK. This also
// has the benefit of producing predictable delivery order for event listener notifications.
private ScheduledExecutorService createSharedExecutor(LDConfig config) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("LaunchDarkly-tasks-%d")
.setPriority(config.threadPriority)
.build();
return Executors.newSingleThreadScheduledExecutor(threadFactory);
}
private Evaluator.PrerequisiteEvaluationSink makePrerequisiteEventSender(boolean withReasons) {
final EventFactory factory = withReasons ? eventFactoryWithReasons : eventFactoryDefault;
return new Evaluator.PrerequisiteEvaluationSink() {
@Override
public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, EvalResult result) {
eventProcessor.sendEvent(
factory.newPrerequisiteFeatureRequestEvent(flag, user, result, prereqOfFlag));
}
};
}
}