
com.launchdarkly.sdk.server.migrations.Migration Maven / Gradle / Ivy
Show all versions of launchdarkly-java-server-sdk Show documentation
package com.launchdarkly.sdk.server.migrations;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.server.MigrationOp;
import com.launchdarkly.sdk.server.MigrationOpTracker;
import com.launchdarkly.sdk.server.MigrationOrigin;
import com.launchdarkly.sdk.server.MigrationStage;
import com.launchdarkly.sdk.server.MigrationVariation;
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
/**
* Class for performing a technology migration.
*
* This class is not intended to be instanced directly, but instead should be constructed
* using the {@link MigrationBuilder}.
*
* The thread safety model for a migration depends on the usage of thread-safe elements. Specifically the tracker,
* the client, and the thread pool should be thread-safe. Other elements of the migration instance itself are immutable
* for their thread-safety.
*
* @param The result type for reads.
* @param The result type for writes.
* @param The input parameter type for reads.
* @param The input type for writes.
*/
public final class Migration {
private final Reader readOld;
private final Reader readNew;
private final Writer writeOld;
private final Writer writeNew;
private final ReadConsistencyChecker checker;
private final MigrationExecution execution;
private final boolean latencyTracking;
private final boolean errorTracking;
private final LDClientInterface client;
private final LDLogger logger;
private final ExecutorService pool = Executors.newCachedThreadPool();
Migration(
LDClientInterface client,
Reader readOld,
Reader readNew,
Writer writeOld,
Writer writeNew,
ReadConsistencyChecker checker,
MigrationExecution execution,
boolean latencyTracking,
boolean errorTracking) {
this.client = client;
this.readOld = readOld;
this.readNew = readNew;
this.writeOld = writeOld;
this.writeNew = writeNew;
this.checker = checker;
this.execution = execution;
this.latencyTracking = latencyTracking;
this.errorTracking = errorTracking;
this.logger = client.getLogger();
}
public interface Method {
MigrationMethodResult execute(UInput payload);
}
/**
* This interface defines a read method.
*
* @param the payload type of the read
* @param the result type of the read
*/
public interface Reader extends Method {
}
/**
* This interfaces defines a write method.
*
* @param the payload type of the write
* @param the return type of the write
*/
public interface Writer extends Method {
}
/**
* This interface defines a method for checking the consistency of two reads.
*
* @param the result type of the read
*/
public interface ReadConsistencyChecker {
boolean check(TReadResult a, TReadResult b);
}
/**
* This class represents the result of a migration operation.
*
* In the case of a read operation the result will be this type. Write operations may need to return multiple results
* and therefore use the {@link MigrationWriteResult} type.
*
* @param the result type of the operation
*/
public static final class MigrationResult {
private final boolean success;
private final MigrationOrigin origin;
private final TResult result;
private final Exception exception;
public MigrationResult(
boolean success,
@NotNull MigrationOrigin origin,
@Nullable TResult result,
@Nullable Exception exception) {
this.success = success;
this.origin = origin;
this.result = result;
this.exception = exception;
}
/**
* Check if the operation was a success.
*
* @return true if the operation was a success
*/
public boolean isSuccess() {
return success;
}
/**
* Get the origin associated with the result.
*
* @return The origin of the result.
*/
public MigrationOrigin getOrigin() {
return origin;
}
/**
* The result. This may be an empty optional if an error occurred.
*
* @return The result, or an empty optional if no result was generated.
*/
public Optional getResult() {
return Optional.ofNullable(result);
}
/**
* Get the exception associated with the result or an empty optional if there
* was no exception.
*
* A result may not be successful, but may also not have an exception associated with it.
*
* @return the exception, or an empty optional if no result was produced
*/
public Optional getException() {
return Optional.ofNullable(exception);
}
}
/**
* The result of a migration write.
*
* A migration write result will always include an authoritative result, and it may contain a non-authoritative result.
*
* Not all migration stages will execute both writes, and in the case of a write error from the authoritative source
* then the non-authoritative write will not be executed.
*
* @param The result type of the write.
*/
public static final class MigrationWriteResult {
private final MigrationResult authoritative;
private final MigrationResult nonAuthoritative;
public MigrationWriteResult(@NotNull Migration.MigrationResult authoritative) {
this.authoritative = authoritative;
this.nonAuthoritative = null;
}
public MigrationWriteResult(
@NotNull Migration.MigrationResult authoritative,
@Nullable Migration.MigrationResult nonAuthoritative) {
this.authoritative = authoritative;
this.nonAuthoritative = nonAuthoritative;
}
/**
* Get the authoritative result of the write.
*
* @return the authoritative result
*/
public MigrationResult getAuthoritative() {
return authoritative;
}
/**
* Get the non-authoritative result.
*
* @return the result, or an empty optional if no result was generated
*/
public Optional> getNonAuthoritative() {
return Optional.ofNullable(nonAuthoritative);
}
}
private static final class MultiReadResult {
private final MigrationResult oldResult;
private final MigrationResult newResult;
MultiReadResult(MigrationResult oldResult, MigrationResult newResult) {
this.oldResult = oldResult;
this.newResult = newResult;
}
MigrationResult getOld() {
return oldResult;
}
MigrationResult getNew() {
return newResult;
}
}
@NotNull
private MigrationResult doSingleOp(
@Nullable TInput payload,
@NotNull MigrationOpTracker tracker,
@NotNull MigrationOrigin origin,
@NotNull Method method
) {
tracker.invoked(origin);
MigrationMethodResult res = trackLatency(payload, tracker, origin, method);
if (res.isSuccess()) {
return new MigrationResult<>(true, origin, res.getResult().orElse(null), null);
}
if (errorTracking) {
tracker.error(origin);
}
return new MigrationResult<>(false, origin, null, res.getException().orElse(null));
}
@NotNull
private MultiReadResult doMultiRead(
@Nullable TReadInput payload,
@NotNull MigrationOpTracker tracker) {
MultiReadResult result;
switch (execution.getMode()) {
case SERIAL:
result = doSerialRead(payload, tracker);
break;
case PARALLEL:
result = doParallelRead(payload, tracker);
break;
default: {
// This would likely be an implementation error from extending the execution modes and not updating this code.
logger.error("Unrecognized execution mode while executing migration.");
result = doSerialRead(payload, tracker);
}
}
if (checker != null &&
result.oldResult.success &&
result.newResult.success
) {
// Temporary variables for the lambda invocation.
MigrationResult finalNewResult = result.newResult;
MigrationResult finalOldResult = result.oldResult;
// Note the individual results could be null. For instance reading
// a DB entry that does not exist.
tracker.consistency(() -> checker.check(finalOldResult.result,
finalNewResult.result));
}
return result;
}
@NotNull
private MultiReadResult doSerialRead(
@Nullable TReadInput payload,
@NotNull MigrationOpTracker tracker) {
MigrationSerialOrder order = execution.getOrder().orElse(MigrationSerialOrder.FIXED);
int result = 0;
if (order == MigrationSerialOrder.RANDOM) {
// This random number is not used for cryptographic purposes.
result = ThreadLocalRandom.current().nextInt(2);
}
MigrationResult oldResult;
MigrationResult newResult;
if (result == 0) {
oldResult = doSingleOp(payload, tracker, MigrationOrigin.OLD, readOld);
newResult = doSingleOp(payload, tracker, MigrationOrigin.NEW, readNew);
} else {
newResult = doSingleOp(payload, tracker, MigrationOrigin.NEW, readNew);
oldResult = doSingleOp(payload, tracker, MigrationOrigin.OLD, readOld);
}
return new MultiReadResult<>(oldResult, newResult);
}
@NotNull
private MultiReadResult doParallelRead(
@Nullable TReadInput payload,
@NotNull MigrationOpTracker tracker) {
List>> tasks = new ArrayList<>();
tasks.add(() -> doSingleOp(payload, tracker, MigrationOrigin.OLD, readOld));
tasks.add(() -> doSingleOp(payload, tracker, MigrationOrigin.NEW, readNew));
try {
List>> futures = pool.invokeAll(tasks);
// We do not initialize bad results here in order to reduce the amount of garbage that needs collected.
// For happy path the result would never be used.
MigrationResult oldResult = null;
MigrationResult newResult = null;
for (Future> future : futures) {
try {
MigrationResult result = future.get();
switch (result.origin) {
case OLD:
oldResult = result;
break;
case NEW:
newResult = result;
break;
}
} catch (Exception e) {
// We do not know which result, just that one of them failed.
// After this stage we can null check and add failed results.
logger.error("An error occurred executing parallel reads: {}", e);
}
}
// If either of these is null, then we know that we failed to get the task.
// This represents a threading failure.
if (oldResult == null) {
oldResult = new MigrationResult<>(false, MigrationOrigin.OLD, null, null);
}
if (newResult == null) {
newResult = new MigrationResult<>(false, MigrationOrigin.NEW, null, null);
}
return new MultiReadResult<>(oldResult, newResult);
} catch (Exception e) {
logger.error("An error occurred executing parallel reads: {}", e);
}
// Something threading related happened, and we could not get any results.
return new MultiReadResult<>(
new MigrationResult<>(false, MigrationOrigin.OLD, null, null),
new MigrationResult<>(false, MigrationOrigin.NEW, null, null));
}
@NotNull
private MigrationMethodResult trackLatency(
@Nullable UInput payload,
@NotNull MigrationOpTracker tracker,
@NotNull MigrationOrigin origin,
@NotNull Method method
) {
MigrationMethodResult res;
if (latencyTracking) {
long start = System.currentTimeMillis();
res = safeCall(payload, method);
long stop = System.currentTimeMillis();
tracker.latency(origin, Duration.of(stop - start, ChronoUnit.MILLIS));
} else {
res = safeCall(payload, method);
}
return res;
}
@NotNull
private static MigrationMethodResult safeCall(
@Nullable UInput payload,
@NotNull Method method
) {
MigrationMethodResult res;
try {
res = method.execute(payload);
} catch (Exception e) {
res = MigrationMethodResult.Failure(e);
}
return res;
}
@NotNull
private MigrationResult handleReadStage(
@Nullable TReadInput payload,
@NotNull MigrationVariation migrationVariation,
@NotNull MigrationOpTracker tracker) {
switch (migrationVariation.getStage()) {
case OFF: // Intentionally falls through.
case DUAL_WRITE: {
return doSingleOp(payload, tracker, MigrationOrigin.OLD, readOld);
}
case SHADOW: {
return doMultiRead(payload, tracker).getOld();
}
case LIVE: {
return doMultiRead(payload, tracker).getNew();
}
case RAMP_DOWN: // Intentionally falls through.
case COMPLETE: {
return doSingleOp(payload, tracker, MigrationOrigin.NEW, readNew);
}
default: {
// If this error occurs it would be because an additional migration stage
// was added, but this code was not updated to support it.
throw new RuntimeException("Unsupported migration stage.");
}
}
}
/**
* Execute a migration based read with a payload.
*
* To execute a read without a payload use {@link #read(String, LDContext, MigrationStage)}.
*
* @param key the flag key of migration flag
* @param context the context for the migration
* @param defaultStage the default migration stage
* @param payload an optional payload that will be passed to the new/old read implementations
* @return the result of the read
*/
@NotNull
public MigrationResult read(
@NotNull String key,
@NotNull LDContext context,
@NotNull MigrationStage defaultStage,
@Nullable TReadInput payload) {
MigrationVariation migrationVariation = client.migrationVariation(key, context, defaultStage);
MigrationOpTracker tracker = migrationVariation.getTracker();
tracker.op(MigrationOp.READ);
MigrationResult res = handleReadStage(payload, migrationVariation, tracker);
client.trackMigration(tracker);
return res;
}
/**
* Execute a migration based read.
*
* To execute a read with a payload use {@link #read(String, LDContext, MigrationStage, Object)}.
*
* @param key the flag key of migration flag
* @param context the context for the migration
* @param defaultStage the default migration stage
* @return the result of the read
*/
@NotNull
public MigrationResult read(
@NotNull String key,
@NotNull LDContext context,
@NotNull MigrationStage defaultStage) {
return read(key, context, defaultStage, null);
}
@NotNull
private MigrationWriteResult handleWriteStage(
@Nullable TWriteInput payload,
@NotNull MigrationVariation migrationVariation,
@NotNull MigrationOpTracker tracker) {
switch (migrationVariation.getStage()) {
case OFF: {
MigrationResult res = doSingleOp(payload, tracker, MigrationOrigin.OLD, writeOld);
return new MigrationWriteResult<>(res);
}
case DUAL_WRITE: // Intentionally falls through.
case SHADOW: {
MigrationResult oldResult = doSingleOp(payload, tracker, MigrationOrigin.OLD, writeOld);
if (!oldResult.success) {
return new MigrationWriteResult<>(oldResult);
}
MigrationResult newResult = doSingleOp(payload, tracker, MigrationOrigin.NEW, writeNew);
return new MigrationWriteResult<>(oldResult, newResult);
}
case LIVE: // Intentionally falls through.
case RAMP_DOWN: {
MigrationResult newResult = doSingleOp(payload, tracker, MigrationOrigin.NEW, writeNew);
if (!newResult.success) {
return new MigrationWriteResult<>(newResult);
}
MigrationResult oldResult = doSingleOp(payload, tracker, MigrationOrigin.OLD, writeOld);
return new MigrationWriteResult<>(newResult, oldResult);
}
case COMPLETE: {
MigrationResult res = doSingleOp(payload, tracker, MigrationOrigin.NEW, writeNew);
return new MigrationWriteResult<>(res);
}
default: {
// If this error occurs it would be because an additional migration stage
// was added, but this code was not updated to support it.
throw new RuntimeException("Unsupported migration stage.");
}
}
}
/**
* Execute a migration based write with a payload.
*
* To execute a write without a payload use {@link #write(String, LDContext, MigrationStage)}.
*
* @param key the flag key of migration flag
* @param context the context for the migration
* @param defaultStage the default migration stage
* @param payload an optional payload that will be passed to the new/old write implementations
* @return the result of the write
*/
@NotNull
public MigrationWriteResult write(
@NotNull String key,
@NotNull LDContext context,
@NotNull MigrationStage defaultStage,
@Nullable TWriteInput payload) {
MigrationVariation migrationVariation = client.migrationVariation(key, context, defaultStage);
MigrationOpTracker tracker = migrationVariation.getTracker();
tracker.op(MigrationOp.WRITE);
MigrationWriteResult res = handleWriteStage(payload, migrationVariation, tracker);
client.trackMigration(tracker);
return res;
}
/**
* Execute a migration based write.
*
* To execute a read with a payload use {@link #write(String, LDContext, MigrationStage, Object)}.
*
* @param key the flag key of migration flag
* @param context the context for the migration
* @param defaultStage the default migration stage
* @return the result of the write
*/
@NotNull
public MigrationWriteResult write(
@NotNull String key,
@NotNull LDContext context,
@NotNull MigrationStage defaultStage) {
return write(key, context, defaultStage, null);
}
}