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,55 @@
package com.launchdarkly.sdk.server;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event;
import com.launchdarkly.sdk.server.datasources.FDv1FallbackDirective;

import java.time.Duration;

/**
* Reads FDv1 fallback directives from the signals that can carry them.
*/
final class FDv1FallbackDirectives {
private static final String PROTOCOL_FALLBACK_TTL = "protocolFallbackTTL";

private FDv1FallbackDirectives() {
}

/**
* Reads a directive from the {@code X-LD-FD-Fallback} and {@code X-LD-FD-Fallback-TTL} response headers.
*
* @param fallbackHeader the value of the {@code X-LD-FD-Fallback} header, or null
* @param ttlHeader the value of the {@code X-LD-FD-Fallback-TTL} header, or null
* @return the directive, or null if the headers do not request fallback
*/
static FDv1FallbackDirective fromHeaders(String fallbackHeader, String ttlHeader) {
if (fallbackHeader == null || !fallbackHeader.equalsIgnoreCase("true")) {
return null;
}
return FDv1FallbackDirective.fromServiceTtl(ttlHeader);
}

/**
* Reads a directive from a {@code goodbye} event. The presence of {@code protocolFallbackTTL} is itself the
* directive; its value is the TTL in seconds.
*
* @param goodbye the goodbye event
* @return the directive, or null if the event has no {@code protocolFallbackTTL}
*/
static FDv1FallbackDirective fromGoodbye(FDv2Event goodbye) {
JsonElement data = goodbye.getData();
if (data == null || !data.isJsonObject()) {
return null;
}
JsonObject object = data.getAsJsonObject();
if (!object.has(PROTOCOL_FALLBACK_TTL) || object.get(PROTOCOL_FALLBACK_TTL).isJsonNull()) {
return null;
}
JsonElement ttl = object.get(PROTOCOL_FALLBACK_TTL);
if (!ttl.isJsonPrimitive() || !ttl.getAsJsonPrimitive().isNumber()) {
return FDv1FallbackDirective.defaultTtl();
}
return FDv1FallbackDirective.fromServiceTtl(Duration.ofSeconds((long) ttl.getAsDouble()));
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.io.Closeable;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
Expand All @@ -31,6 +32,7 @@ interface Condition extends Closeable {
enum ConditionType {
FALLBACK,
RECOVERY,
FDV1_FALLBACK_EXPIRED,
}

CompletableFuture<Condition> execute();
Expand Down Expand Up @@ -180,4 +182,57 @@ public ConditionType getType() {
return ConditionType.RECOVERY;
}
}

/**
* This condition is met when the TTL of an FDv1 fallback directive elapses, meaning the data source should
* return to its FDv2 synchronizers. A directive received while the condition is pending supersedes the TTL that
* was in effect.
* <p>
* This is package-private, instead of private, for ease of testing.
*/
static class FDv1FallbackExpiryCondition implements Condition {
private final CompletableFuture<Condition> resultFuture = new CompletableFuture<>();
private final ScheduledExecutorService sharedExecutor;
private ScheduledFuture<?> timerFuture;

public FDv1FallbackExpiryCondition(ScheduledExecutorService sharedExecutor, Duration delay) {
this.sharedExecutor = sharedExecutor;
schedule(delay);
}

private synchronized void schedule(Duration delay) {
if (timerFuture != null) {
timerFuture.cancel(false);
}
timerFuture = sharedExecutor.schedule(
() -> resultFuture.complete(this),
Math.max(0, delay.toNanos()),
TimeUnit.NANOSECONDS);
}

@Override
public CompletableFuture<Condition> execute() {
return resultFuture;
}

@Override
public void inform(FDv2SourceResult sourceResult) {
if (sourceResult.isFdv1Fallback()) {
schedule(sourceResult.getFdv1FallbackDirective().getTtl());
}
}

@Override
public synchronized void close() {
if (timerFuture != null) {
timerFuture.cancel(false);
timerFuture = null;
}
}

@Override
public ConditionType getType() {
return ConditionType.FDV1_FALLBACK_EXPIRED;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

enum HeaderConstants {
ENVIRONMENT_ID("x-ld-envid"),
FDV1_FALLBACK("x-ld-fd-fallback");
FDV1_FALLBACK("x-ld-fd-fallback"),
FDV1_FALLBACK_TTL("x-ld-fd-fallback-ttl");

private final String headerName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event;
import com.launchdarkly.sdk.internal.fdv2.sources.FDv2ProtocolHandler;
import com.launchdarkly.sdk.fdv2.Selector;
import com.launchdarkly.sdk.server.datasources.FDv1FallbackDirective;
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
import com.launchdarkly.sdk.fdv2.ChangeSet;
Expand Down Expand Up @@ -35,13 +36,14 @@ protected void internalShutdown() {
requestor.close();
}

private static boolean getFallback(FDv2Requestor.FDv2PayloadResponse response) {
private static FDv1FallbackDirective getFallback(FDv2Requestor.FDv2PayloadResponse response) {
if (response != null && response.getHeaders() != null) {
String headerValue = response.getHeaders().get(HeaderConstants.FDV1_FALLBACK.getHeaderName());
return headerValue != null && headerValue.equalsIgnoreCase("true");
return FDv1FallbackDirectives.fromHeaders(
response.getHeaders().get(HeaderConstants.FDV1_FALLBACK.getHeaderName()),
response.getHeaders().get(HeaderConstants.FDV1_FALLBACK_TTL.getHeaderName()));
}

return false;
return null;
}

private static String getEnvironmentId(FDv2Requestor.FDv2PayloadResponse response) {
Expand All @@ -53,7 +55,7 @@ private static String getEnvironmentId(FDv2Requestor.FDv2PayloadResponse respons

protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean oneShot) {
return requestor.Poll(selector).handle(((pollingResponse, ex) -> {
boolean fdv1Fallback = getFallback(pollingResponse);
FDv1FallbackDirective fdv1Fallback = getFallback(pollingResponse);
String environmentId = getEnvironmentId(pollingResponse);
if (ex != null) {
if (ex instanceof IOException) {
Expand Down Expand Up @@ -148,7 +150,8 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
case GOODBYE:
String reason = ((FDv2ProtocolHandler.FDv2ActionGoodbye) res).getReason();
logger.info("Goodbye was received from the LaunchDarkly connection with reason: '{}'.", reason);
return FDv2SourceResult.goodbye(reason, fdv1Fallback);
FDv1FallbackDirective goodbyeFallback = FDv1FallbackDirectives.fromGoodbye(event);
return FDv2SourceResult.goodbye(reason, goodbyeFallback != null ? goodbyeFallback : fdv1Fallback);
case NONE:
break;
case INTERNAL_ERROR: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
Expand Down Expand Up @@ -36,6 +37,11 @@ class SourceManager implements Closeable {
*/
private SynchronizerFactoryWithState currentSynchronizerFactory;

/**
* The FDv2 synchronizers that were available when FDv1 fallback began, so they can be restored on recovery.
*/
private final List<SynchronizerFactoryWithState> suspendedForFDv1Fallback = new ArrayList<>();

public SourceManager(List<SynchronizerFactoryWithState> synchronizers, List<FDv2DataSource.DataSourceFactory<Initializer>> initializers) {
this.synchronizers = synchronizers;
this.initializers = initializers;
Expand All @@ -61,15 +67,47 @@ public boolean hasFDv1Fallback() {
}

/**
* Block all synchronizers aside from the fdv1 fallback and unblock the fdv1 fallback.
* Block all synchronizers aside from the fdv1 fallback and unblock the fdv1 fallback. The FDv2 synchronizers
* that were available are remembered so that {@link #endFDv1Fallback()} can restore them.
*/
public void fdv1Fallback() {
for (SynchronizerFactoryWithState factory : synchronizers) {
if(factory.isFDv1Fallback()) {
synchronized (activeSourceLock) {
for (SynchronizerFactoryWithState factory : synchronizers) {
if (factory.isFDv1Fallback()) {
factory.unblock();
} else if (factory.getState() == SynchronizerFactoryWithState.State.Available) {
factory.block();
suspendedForFDv1Fallback.add(factory);
}
}
}
}

/**
* Restore the FDv2 synchronizers that were blocked by {@link #fdv1Fallback()}, block the fdv1 fallback, and
* start again from the first synchronizer.
*/
public void endFDv1Fallback() {
synchronized (activeSourceLock) {
for (SynchronizerFactoryWithState factory : synchronizers) {
if (factory.isFDv1Fallback()) {
factory.block();
}
}
for (SynchronizerFactoryWithState factory : suspendedForFDv1Fallback) {
factory.unblock();
} else {
factory.block();
}
suspendedForFDv1Fallback.clear();
synchronizerIndex = -1;
}
}

/**
* @return true if {@link #fdv1Fallback()} suspended FDv2 synchronizers that have not yet been restored
*/
public boolean hasSuspendedFDv2Synchronizers() {
synchronized (activeSourceLock) {
return !suspendedForFDv1Fallback.isEmpty();
}
}

Expand Down Expand Up @@ -133,6 +171,9 @@ public com.launchdarkly.sdk.server.datasources.Synchronizer getNextAvailableSync
SynchronizerFactoryWithState factory = getNextAvailableSynchronizer();
if (factory == null) {
currentSynchronizerFactory = null;
// Nothing replaces the previously active source, so stop it rather than leave it running.
safeClose(activeSource);
activeSource = null;
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.launchdarkly.eventsource.FaultEvent;
import com.launchdarkly.eventsource.HttpConnectStrategy;
import com.launchdarkly.eventsource.MessageEvent;
import com.launchdarkly.eventsource.ResponseHeaders;
import com.launchdarkly.eventsource.RetryDelayStrategy;
import com.launchdarkly.eventsource.StreamClosedByCallerException;
import com.launchdarkly.eventsource.StreamEvent;
Expand All @@ -20,6 +21,7 @@
import com.launchdarkly.sdk.fdv2.Selector;
import com.launchdarkly.sdk.internal.http.HttpHelpers;
import com.launchdarkly.sdk.internal.http.HttpProperties;
import com.launchdarkly.sdk.server.datasources.FDv1FallbackDirective;
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
import com.launchdarkly.sdk.server.datasources.SelectorSource;
import com.launchdarkly.sdk.server.datasources.Synchronizer;
Expand Down Expand Up @@ -320,7 +322,8 @@ private void handleMessage(MessageEvent event) {
case GOODBYE:
String reason = ((FDv2ProtocolHandler.FDv2ActionGoodbye) action).getReason();
logger.info("Goodbye was received from the LaunchDarkly connection with reason: '{}'.", reason);
result = FDv2SourceResult.goodbye(reason, getFallback(event));
FDv1FallbackDirective goodbyeFallback = FDv1FallbackDirectives.fromGoodbye(fdv2Event);
result = FDv2SourceResult.goodbye(reason, goodbyeFallback != null ? goodbyeFallback : getFallback(event));
// We drop this current connection and attempt to restart the stream.
restartStream(false); // Not a failure - deliberate server-initiated restart
break;
Expand Down Expand Up @@ -447,23 +450,25 @@ private FDv2Event parseFDv2Event(String eventName, Reader eventDataReader) throw
}
}

private static boolean getFallback(Exception ex) {
private static FDv1FallbackDirective getFallback(Exception ex) {
if(ex instanceof StreamHttpErrorException) {
String headerValue = ((StreamHttpErrorException) ex).getHeaders()
.value(HeaderConstants.FDV1_FALLBACK.getHeaderName());
return headerValue != null && headerValue.equalsIgnoreCase("true");
return getFallback(((StreamHttpErrorException) ex).getHeaders());
}
return false;
return null;
}

private static boolean getFallback(StreamEvent event) {
String headerName = HeaderConstants.FDV1_FALLBACK.getHeaderName();
String headerValue = null;
private static FDv1FallbackDirective getFallback(StreamEvent event) {
if(event instanceof FaultEvent) {
headerValue = ((FaultEvent) event).getHeaders().value(headerName);
return getFallback(((FaultEvent) event).getHeaders());
} else if (event instanceof MessageEvent) {
headerValue = ((MessageEvent) event).getHeaders().value(headerName);
return getFallback(((MessageEvent) event).getHeaders());
}
return headerValue != null && headerValue.equalsIgnoreCase("true");
return null;
}

private static FDv1FallbackDirective getFallback(ResponseHeaders headers) {
return FDv1FallbackDirectives.fromHeaders(
headers.value(HeaderConstants.FDV1_FALLBACK.getHeaderName()),
headers.value(HeaderConstants.FDV1_FALLBACK_TTL.getHeaderName()));
}
}
Loading
Loading