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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ run-contract-tests:
| VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh
@echo "Running SDK contract test v3..."
@curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \
-s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.1.0-alpha.6/downloader/run.sh \
| VERSION=v3.1.0-alpha.6 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh
-s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.2.0-alpha.10/downloader/run.sh \
| VERSION=v3.2.0-alpha.10 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh

contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static class SdkConfigClientSideParams {
LDContext initialContext;
boolean evaluationReasons;
boolean useReport;
boolean usePost;
boolean includeEnvironmentAttributes;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,
ConfigHelper.configureIsolatedInMemoryPersistence(builder);

if (params.dataSystem != null) {
configureDataSystem(builder, params.dataSystem);
configureDataSystem(builder, params.dataSystem, params.clientSide.usePost);
} else {
if (params.polling != null && params.polling.baseUri != null) {
endpoints.polling(params.polling.baseUri);
Expand Down Expand Up @@ -391,15 +391,16 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,
return builder.build();
}

private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem) {
private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem,
boolean usePost) {
if (Boolean.TRUE.equals(dataSystem.useDefaultDataSystem)) {
builder.dataSystem(Components.dataSystem());
builder.dataSystem(Components.dataSystem().usePost(usePost));
return;
}

SdkConfigConnectionModeConfig connModeConfig = dataSystem.connectionModeConfig;

DataSystemBuilder dsBuilder = Components.dataSystem();
DataSystemBuilder dsBuilder = Components.dataSystem().usePost(usePost);

// at the time of writing this, we did not have contract tests that could test platform state changes,
// disabling automatic mode simplifies the behavior being tested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public class TestService extends NanoHTTPD {
"evaluation-hooks",
"track-hooks",
"client-per-context-summaries",
"client-event-source-http-errors"
"client-event-source-http-errors",
"client-use-post"
};
private static final String MIME_JSON = "application/json";
static final Gson gson = new GsonBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ final class DataSourceBuildInputsInternal extends DataSourceBuildInputs {
ServiceEndpoints serviceEndpoints,
HttpConfiguration http,
boolean evaluationReasons,
boolean usePost,
SelectorSource selectorSource,
ScheduledExecutorService sharedExecutor,
@NonNull File cacheDir,
LDLogger baseLogger,
@Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData perEnvironmentData
) {
super(evaluationContext, serviceEndpoints, http, evaluationReasons,
super(evaluationContext, serviceEndpoints, http, evaluationReasons, usePost,
selectorSource, sharedExecutor, cacheDir, baseLogger);
this.perEnvironmentData = perEnvironmentData;
}
Expand All @@ -60,6 +61,7 @@ static DataSourceBuildInputsInternal get(DataSourceBuildInputs inputs) {
inputs.getServiceEndpoints(),
inputs.getHttp(),
inputs.isEvaluationReasons(),
inputs.isUsePost(),
inputs.getSelectorSource(),
inputs.getSharedExecutor(),
inputs.getCacheDir(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH,
requestor,
initialReconnectDelayMillis,
inputs.isEvaluationReasons(), inputs.getHttp().isUseReport(),
inputs.isEvaluationReasons(), inputs.isUsePost(),
httpProps, inputs.getSharedExecutor(),
inputs.getBaseLogger(), null);
}
Expand All @@ -159,10 +159,12 @@ public FDv1PollingSynchronizerBuilderImpl pollIntervalMillis(int pollIntervalMil

@Override
public Synchronizer build(DataSourceBuildInputs inputs) {
// The FDv1 endpoints do not accept POST. REPORT is their method that carries the
// context in the request body, so usePost selects REPORT here.
FeatureFetcher fetcher = new HttpFeatureFlagFetcher(
inputs.getServiceEndpoints().getPollingBaseUri(),
inputs.isEvaluationReasons(),
inputs.getHttp().isUseReport(),
inputs.isUsePost(),
LDUtil.makeHttpProperties(inputs.getHttp()),
inputs.getCacheDir(),
inputs.getBaseLogger()
Expand Down Expand Up @@ -360,9 +362,8 @@ private static FDv2Requestor makePollingRequestor(DataSourceBuildInputs inputs,
"polling", inputs.getBaseLogger());
return new DefaultFDv2Requestor(
inputs.getEvaluationContext(), pollingBase,
StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH,
StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH,
httpProps, inputs.getHttp().isUseReport(),
StandardEndpoints.FDV2_POLLING_REQUEST_BASE_PATH,
httpProps, inputs.isUsePost(),
inputs.isEvaluationReasons(), null, inputs.getBaseLogger());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
/**
* Default OkHttp-based implementation of {@link FDv2Requestor}.
* <p>
* Builds GET or REPORT requests to the FDv2 polling endpoint. If the current selector is
* Builds GET or POST requests to the FDv2 polling endpoint. If the current selector is
* non-empty, its state is sent as the {@code basis} query parameter. ETag tracking is used
* to detect 304 Not Modified responses; when the server returns 304 the application layer
* treats it as {@code ChangeSetType.None} (no flags changed), so no OkHttp disk cache is
Expand All @@ -40,7 +40,6 @@
* The OkHttpClient is closed by {@link #close()}.
*/
final class DefaultFDv2Requestor implements FDv2Requestor {
private static final String METHOD_REPORT = "REPORT";
private static final String BASIS_PARAM = "basis";
private static final String FILTER_PARAM = "filter";
private static final String WITH_REASONS_PARAM = "withReasons";
Expand All @@ -50,10 +49,10 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
private final OkHttpClient httpClient;
private final URI pollingUri;
private final okhttp3.Headers headers;
private final boolean useReport;
private final boolean usePost;
private final boolean evaluationReasons;
@Nullable
private final RequestBody reportBody;
private final RequestBody postBody;
@Nullable
private final String payloadFilter;
private final LDLogger logger;
Expand All @@ -67,10 +66,10 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
/**
* @param evaluationContext the context to evaluate flags for
* @param baseUri polling base URI from service endpoints
* @param getRequestPath path for GET requests (context appended as a path segment)
* @param reportRequestPath path for REPORT requests (context sent in the request body)
* @param requestPath polling request path; GET appends the base64-encoded context as a
* path segment, POST uses the path as is
* @param httpProperties SDK HTTP configuration (timeouts, proxy, TLS, user-agent, etc.)
* @param useReport if true, send context in the request body via REPORT; otherwise
* @param usePost if true, send the context in the request body via POST; otherwise
* append the base64-encoded context to the GET path
* @param evaluationReasons if true, append {@code withReasons=true} to the query string
* @param payloadFilter optional payload filter key; sent as {@code filter} query param
Expand All @@ -79,29 +78,27 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
DefaultFDv2Requestor(
@NonNull LDContext evaluationContext,
@NonNull URI baseUri,
@NonNull String getRequestPath,
@NonNull String reportRequestPath,
@NonNull String requestPath,
@NonNull HttpProperties httpProperties,
boolean useReport,
boolean usePost,
boolean evaluationReasons,
@Nullable String payloadFilter,
@NonNull LDLogger logger) {
this.useReport = useReport;
this.usePost = usePost;
this.evaluationReasons = evaluationReasons;
this.payloadFilter = payloadFilter;
this.logger = logger;
this.headers = httpProperties.toHeadersBuilder().build();

// Precompute the base polling URI for the chosen request method. For GET, the
// base64-encoded context is a fixed path segment (context never changes after
// construction). For REPORT, the context goes in the request body so no path
// construction). For POST, the context goes in the request body so no path
// segment is needed.
URI basePollingUri = HttpHelpers.concatenateUriPath(baseUri,
useReport ? reportRequestPath : getRequestPath);
this.pollingUri = useReport
URI basePollingUri = HttpHelpers.concatenateUriPath(baseUri, requestPath);
this.pollingUri = usePost
? basePollingUri
: HttpHelpers.concatenateUriPath(basePollingUri, LDUtil.urlSafeBase64(evaluationContext));
this.reportBody = useReport
this.postBody = usePost
? RequestBody.create(JsonSerialization.serialize(evaluationContext), JSON)
: null;

Expand Down Expand Up @@ -139,8 +136,8 @@ public Future<FDv2PayloadResponse> poll(@NonNull Selector selector) {
.url(requestUri.toURL())
.headers(headers);

if (useReport) {
reqBuilder.method(METHOD_REPORT, reportBody);
if (usePost) {
reqBuilder.post(postBody);
} else {
synchronized (etagLock) {
if (!requestUri.equals(lastRequestUri)) {
Expand Down Expand Up @@ -207,7 +204,7 @@ private void handleResponse(
return;
}

if (!useReport) {
if (!usePost) {
synchronized (etagLock) {
cachedEtag = response.header(ETAG_HEADER);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ class FDv2DataSourceBuilder implements ComponentConfigurer<DataSource>, Closeabl
private final Map<ConnectionMode, ModeDefinition> modeTable;
private final ConnectionMode startingMode;
private final ModeResolutionTable resolutionTable;
private final boolean usePost;

private ConnectionMode activeMode;
private boolean includeInitializers = true; // start with initializers
private ScheduledExecutorService sharedExecutor;
// The useReport warning is logged on the first build only. Builds run on one thread.
private boolean useReportWarningLogged = false;

FDv2DataSourceBuilder() {
this(DataSystemComponents.makeDefaultModeTable(), ConnectionMode.STREAMING, ModeResolutionTable.MOBILE);
Expand All @@ -54,10 +57,24 @@ class FDv2DataSourceBuilder implements ComponentConfigurer<DataSource>, Closeabl
@NonNull Map<ConnectionMode, ModeDefinition> modeTable,
@NonNull ConnectionMode startingMode,
@NonNull ModeResolutionTable resolutionTable
) {
this(modeTable, startingMode, resolutionTable, false);
}

/**
* @param usePost true if FDv2 polling and streaming requests send the context in the request
* body with POST instead of encoding it into the GET path
*/
FDv2DataSourceBuilder(
@NonNull Map<ConnectionMode, ModeDefinition> modeTable,
@NonNull ConnectionMode startingMode,
@NonNull ModeResolutionTable resolutionTable,
boolean usePost
) {
this.modeTable = modeTable;
this.startingMode = startingMode;
this.resolutionTable = resolutionTable;
this.usePost = usePost;
}

/**
Expand All @@ -75,6 +92,15 @@ ConnectionMode getStartingMode() {
return startingMode;
}

/**
* Returns whether FDv2 flag requests send the context in the request body with POST.
*
* @return true if flag requests use POST
*/
boolean isUsePost() {
return usePost;
}

/**
* Configures the mode to build for and whether to include initializers.
* Called by {@link ConnectivityManager} before each {@link #build} call.
Expand Down Expand Up @@ -108,6 +134,15 @@ public DataSource build(ClientContext clientContext) {

ConnectionMode mode = activeMode != null ? activeMode : startingMode;

// The FDv2 endpoints take POST, not REPORT. The data system has its own usePost option,
// so the FDv1 useReport option does not apply here.
if (clientContext.getHttp().isUseReport() && !useReportWarningLogged) {
useReportWarningLogged = true;
clientContext.getBaseLogger().warn(
"useReport has no effect with the FDv2 data system and is ignored. " +
"Use DataSystemBuilder.usePost to send the evaluation context in the request body.");
}

ModeDefinition modeDef = modeTable.get(mode);
if (modeDef == null) {
throw new IllegalStateException(
Expand Down Expand Up @@ -159,6 +194,7 @@ private DataSourceBuildInputsInternal makeInputs(ClientContext clientContext) {
clientContext.getServiceEndpoints(),
clientContext.getHttp(),
clientContext.isEvaluationReasons(),
usePost,
selectorSource,
sharedExecutor,
impl.getPlatformState().getCacheDir(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@
* <p>
*/
final class FDv2StreamingSynchronizer implements Synchronizer {
private static final String METHOD_REPORT = "REPORT";
private static final String METHOD_POST = "POST";
private static final String PING = "ping";
private static final long READ_TIMEOUT_MS = 300_000; // 5 minutes
private static final long MAX_RECONNECT_TIME_MS = 300_000; // 5 minutes

private final HttpProperties httpProperties;
private final URI streamBaseUri;
private final String streamRequestPath;
private final boolean useReport;
private final boolean usePost;
private final LDContext evaluationContext;
private final SelectorSource selectorSource;
@Nullable
Expand Down Expand Up @@ -92,7 +92,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
* @param requestor optional requestor for handling ping events via poll; may be null
* @param initialReconnectDelayMillis delay before reconnecting after an error, in milliseconds
* @param evaluationReasons true to request evaluation reasons in the stream
* @param useReport true to use HTTP REPORT for the request body
* @param usePost true to send the context in the request body with POST
* @param httpProperties HTTP configuration for the stream request
* @param executor executor used to run the streaming loop on a background
* thread; should use background-priority threads
Expand All @@ -107,7 +107,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
@Nullable FDv2Requestor requestor,
int initialReconnectDelayMillis,
boolean evaluationReasons,
boolean useReport,
boolean usePost,
@NonNull HttpProperties httpProperties,
@NonNull Executor executor,
@NonNull LDLogger logger,
Expand All @@ -120,7 +120,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
this.requestor = requestor;
this.initialReconnectDelayMillis = initialReconnectDelayMillis;
this.evaluationReasons = evaluationReasons;
this.useReport = useReport;
this.usePost = usePost;
this.httpProperties = httpProperties;
this.executor = executor;
this.logger = logger;
Expand Down Expand Up @@ -188,9 +188,9 @@ private void startStream() {
return reqBuilder.build();
});

if (useReport) {
if (usePost) {
connectStrategy = connectStrategy.methodAndBody(
METHOD_REPORT,
METHOD_POST,
RequestBody.create(JsonSerialization.serialize(evaluationContext), JSON));
}

Expand Down Expand Up @@ -244,7 +244,7 @@ private void startStream() {

private URI getStreamUri() {
URI uri = HttpHelpers.concatenateUriPath(streamBaseUri, streamRequestPath);
if (!useReport) {
if (!usePost) {
uri = HttpHelpers.concatenateUriPath(uri, LDUtil.urlSafeBase64(evaluationContext));
}
if (evaluationReasons) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ public LDConfig build() {
ModeResolutionTable resolutionTable = ModeResolutionTable.createMobile(
startingMode, backgroundMode);
effectiveDataSource = new FDv2DataSourceBuilder(
modeTable, startingMode, resolutionTable);
modeTable, startingMode, resolutionTable, this.dataSystemBuilder.isUsePost());
effectiveAutoModeSwitching = this.dataSystemBuilder.getAutomaticModeSwitchingConfig();
} else {
effectiveDataSource = this.dataSource == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ private StandardEndpoints() {}
static final String ANALYTICS_EVENTS_REQUEST_PATH = "/mobile/events/bulk";
static final String DIAGNOSTIC_EVENTS_REQUEST_PATH = "/mobile/events/diagnostic";

static final String FDV2_POLLING_REQUEST_GET_BASE_PATH = "/sdk/poll/eval";
static final String FDV2_POLLING_REQUEST_REPORT_BASE_PATH = "/sdk/poll/eval";
// GET appends the base64url context as a path segment; POST sends it in the body.
static final String FDV2_POLLING_REQUEST_BASE_PATH = "/sdk/poll/eval";
static final String FDV2_STREAMING_REQUEST_BASE_PATH = "/sdk/stream/eval";

/**
Expand Down
Loading
Loading