Skip to content

Commit 8469d3c

Browse files
authored
chore: Pipe headers through data sources. (#111)
This pipes the fdv1 fallback header, and the environment ID, through the data sources. It doesn't utilize either piece of information. This is draft while I sort publishing the okhttp-evensource. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces header propagation and response handling improvements across FDv2 data sources. > > - Adds `HeaderConstants` and plumbs `x-ld-fd-fallback` and `x-ld-envid` from HTTP/stream events into `FDv2SourceResult` (new `fdv1Fallback` flag) and `ChangeSet` environment ID > - Refactors `FDv2Requestor.FDv2PayloadResponse` to include `isSuccess`, `statusCode`, and factory methods (`success`, `failure`, `none`); 304 now returns a non-null "none" response > - Updates `DefaultFDv2Requestor`, `PollingBase`, `PollingInitializerImpl`, and `StreamingSynchronizerImpl` to use structured responses and extract headers for error handling and changeset conversion > - Extends unit tests to cover header propagation, new response semantics, ETag/304 handling, and error paths; replaces exception-based HTTP error assertions with response-based checks > - Bumps dependency `okhttp-eventsource` to `4.2.0` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a4533f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4bb7b35 commit 8469d3c

File tree

12 files changed

+858
-123
lines changed

12 files changed

+858
-123
lines changed

lib/sdk/server/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ ext.versions = [
7474
"launchdarklyJavaSdkInternal": "1.6.1",
7575
"launchdarklyLogging": "1.1.0",
7676
"okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
77-
"okhttpEventsource": "4.1.0",
77+
"okhttpEventsource": "4.2.0",
7878
"reactorCore":"3.3.22.RELEASE",
7979
"slf4j": "1.7.36",
8080
"snakeyaml": "2.4",

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import com.launchdarkly.logging.LDLogger;
44
import com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event;
55
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
6-
import com.launchdarkly.sdk.internal.http.HttpErrors;
76
import com.launchdarkly.sdk.internal.http.HttpHelpers;
87
import com.launchdarkly.sdk.internal.http.HttpProperties;
9-
import com.launchdarkly.sdk.json.SerializationException;
108

119
import okhttp3.Call;
1210
import okhttp3.Callback;
@@ -116,14 +114,12 @@ public void onResponse(@Nonnull Call call, @Nonnull Response response) {
116114
// Handle 304 Not Modified - no new data
117115
if (response.code() == 304) {
118116
logger.debug("FDv2 polling request returned 304: not modified");
119-
future.complete(null);
117+
future.complete(FDv2PayloadResponse.none(response.code()));
120118
return;
121119
}
122120

123121
if (!response.isSuccessful()) {
124-
future.completeExceptionally(
125-
new HttpErrors.HttpErrorException(response.code())
126-
);
122+
future.complete(FDv2PayloadResponse.failure(response.code(), response.headers()));
127123
return;
128124
}
129125

@@ -145,7 +141,7 @@ public void onResponse(@Nonnull Call call, @Nonnull Response response) {
145141
List<FDv2Event> events = FDv2Event.parseEventsArray(responseBody);
146142

147143
// Create and return the response
148-
FDv2PayloadResponse pollingResponse = new FDv2PayloadResponse(events, response.headers());
144+
FDv2PayloadResponse pollingResponse = FDv2PayloadResponse.success(events, response.headers(), response.code());
149145
future.complete(pollingResponse);
150146

151147
} catch (Exception e) {

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2Requestor.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,31 @@ interface FDv2Requestor {
1818
* to get from one payload version to another.
1919
* This isn't intended for use for implementations which may require multiple executions to get an entire payload.
2020
*/
21-
public static class FDv2PayloadResponse {
21+
public static class FDv2PayloadResponse {
2222
private final List<FDv2Event> events;
2323
private final Headers headers;
2424

25-
public FDv2PayloadResponse(List<FDv2Event> events, Headers headers) {
25+
private final boolean successful;
26+
27+
private final int statusCode;
28+
29+
private FDv2PayloadResponse(List<FDv2Event> events, Headers headers, boolean success, int statusCode) {
2630
this.events = events;
2731
this.headers = headers;
32+
this.successful = success;
33+
this.statusCode = statusCode;
34+
}
35+
36+
public static FDv2PayloadResponse failure(int statusCode, Headers headers) {
37+
return new FDv2PayloadResponse(null, headers, false, statusCode);
38+
}
39+
40+
public static FDv2PayloadResponse success(List<FDv2Event> events, Headers headers, int statusCode) {
41+
return new FDv2PayloadResponse(events, headers, true, statusCode);
42+
}
43+
44+
public static FDv2PayloadResponse none(int statusCode) {
45+
return new FDv2PayloadResponse(null, null, true, statusCode);
2846
}
2947

3048
public List<FDv2Event> getEvents() {
@@ -34,6 +52,14 @@ public List<FDv2Event> getEvents() {
3452
public Headers getHeaders() {
3553
return headers;
3654
}
55+
56+
public boolean isSuccess() {
57+
return successful;
58+
}
59+
60+
public int getStatusCode() {
61+
return statusCode;
62+
}
3763
}
3864
CompletableFuture<FDv2PayloadResponse> Poll(Selector selector);
3965

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
enum HeaderConstants {
4+
ENVIRONMENT_ID("x-ld-envid"),
5+
FDV1_FALLBACK("x-ld-fd-fallback");
6+
7+
private final String headerName;
8+
9+
HeaderConstants(String headerName) {
10+
this.headerName = headerName;
11+
}
12+
13+
public String getHeaderName() {
14+
return headerName;
15+
}
16+
}

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingBase.java

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event;
55
import com.launchdarkly.sdk.internal.fdv2.sources.FDv2ProtocolHandler;
66
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
7-
import com.launchdarkly.sdk.internal.http.HttpErrors;
87
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
98
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
109
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes;
@@ -29,23 +28,28 @@ protected void internalShutdown() {
2928
requestor.close();
3029
}
3130

31+
private static boolean getFallback(FDv2Requestor.FDv2PayloadResponse response) {
32+
if (response != null && response.getHeaders() != null) {
33+
String headerValue = response.getHeaders().get(HeaderConstants.FDV1_FALLBACK.getHeaderName());
34+
return headerValue != null && headerValue.equalsIgnoreCase("true");
35+
}
36+
37+
return false;
38+
}
39+
40+
private static String getEnvironmentId(FDv2Requestor.FDv2PayloadResponse response) {
41+
if (response != null && response.getHeaders() != null) {
42+
return response.getHeaders().get(HeaderConstants.ENVIRONMENT_ID.getHeaderName());
43+
}
44+
return null;
45+
}
46+
3247
protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean oneShot) {
3348
return requestor.Poll(selector).handle(((pollingResponse, ex) -> {
49+
boolean fdv1Fallback = getFallback(pollingResponse);
50+
String environmentId = getEnvironmentId(pollingResponse);
3451
if (ex != null) {
35-
if (ex instanceof HttpErrors.HttpErrorException) {
36-
HttpErrors.HttpErrorException e = (HttpErrors.HttpErrorException) ex;
37-
DataSourceStatusProvider.ErrorInfo errorInfo = DataSourceStatusProvider.ErrorInfo.fromHttpError(e.getStatus());
38-
// Errors without an HTTP status are recoverable. If there is a status, then we check if the error
39-
// is recoverable.
40-
boolean recoverable = e.getStatus() <= 0 || isHttpErrorRecoverable(e.getStatus());
41-
logger.error("Polling request failed with HTTP error: {}", e.getStatus());
42-
// For a one-shot request all errors are terminal.
43-
if (oneShot) {
44-
return FDv2SourceResult.terminalError(errorInfo);
45-
} else {
46-
return recoverable ? FDv2SourceResult.interrupted(errorInfo) : FDv2SourceResult.terminalError(errorInfo);
47-
}
48-
} else if (ex instanceof IOException) {
52+
if (ex instanceof IOException) {
4953
IOException e = (IOException) ex;
5054
logger.error("Polling request failed with network error: {}", e.toString());
5155
DataSourceStatusProvider.ErrorInfo info = new DataSourceStatusProvider.ErrorInfo(
@@ -54,7 +58,7 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
5458
e.toString(),
5559
new Date().toInstant()
5660
);
57-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
61+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
5862
} else if (ex instanceof SerializationException) {
5963
SerializationException e = (SerializationException) ex;
6064
logger.error("Polling request received malformed data: {}", e.toString());
@@ -64,7 +68,7 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
6468
e.toString(),
6569
new Date().toInstant()
6670
);
67-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
71+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
6872
}
6973
String msg = ex.toString();
7074
logger.error("Polling request failed with an unknown error: {}", msg);
@@ -74,17 +78,30 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
7478
msg,
7579
new Date().toInstant()
7680
);
77-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
81+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
7882
}
79-
// A null polling response indicates that we received a 304, which means nothing has changed.
80-
if (pollingResponse == null) {
83+
// If we get a 304, then that means nothing has changed.
84+
if (pollingResponse.getStatusCode() == 304) {
8185
return FDv2SourceResult.changeSet(
8286
new DataStoreTypes.ChangeSet<>(DataStoreTypes.ChangeSetType.None,
8387
Selector.EMPTY,
8488
null,
85-
// TODO: Implement environment ID support.
86-
null
87-
));
89+
null // Header derived values will have been handled on initial response.
90+
),
91+
// Headers would have been processed from the initial response.
92+
false);
93+
}
94+
if(!pollingResponse.isSuccess()) {
95+
int statusCode = pollingResponse.getStatusCode();
96+
boolean recoverable = statusCode <= 0 || isHttpErrorRecoverable(statusCode);
97+
DataSourceStatusProvider.ErrorInfo errorInfo = DataSourceStatusProvider.ErrorInfo.fromHttpError(statusCode);
98+
logger.error("Polling request failed with HTTP error: {}", statusCode);
99+
// For a one-shot request all errors are terminal.
100+
if (oneShot) {
101+
return FDv2SourceResult.terminalError(errorInfo, fdv1Fallback);
102+
} else {
103+
return recoverable ? FDv2SourceResult.interrupted(errorInfo, fdv1Fallback) : FDv2SourceResult.terminalError(errorInfo, fdv1Fallback);
104+
}
88105
}
89106
FDv2ProtocolHandler handler = new FDv2ProtocolHandler();
90107
for (FDv2Event event : pollingResponse.getEvents()) {
@@ -96,10 +113,9 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
96113
DataStoreTypes.ChangeSet<DataStoreTypes.ItemDescriptor> converted = FDv2ChangeSetTranslator.toChangeSet(
97114
((FDv2ProtocolHandler.FDv2ActionChangeset) res).getChangeset(),
98115
logger,
99-
// TODO: Implement environment ID support.
100-
null
116+
environmentId
101117
);
102-
return FDv2SourceResult.changeSet(converted);
118+
return FDv2SourceResult.changeSet(converted, fdv1Fallback);
103119
} catch (Exception e) {
104120
// TODO: Do we need to be more specific about the exception type here?
105121
DataSourceStatusProvider.ErrorInfo info = new DataSourceStatusProvider.ErrorInfo(
@@ -108,7 +124,7 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
108124
e.toString(),
109125
new Date().toInstant()
110126
);
111-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
127+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
112128
}
113129
case ERROR: {
114130
FDv2ProtocolHandler.FDv2ActionError error = ((FDv2ProtocolHandler.FDv2ActionError) res);
@@ -117,10 +133,10 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
117133
0,
118134
error.getReason(),
119135
new Date().toInstant());
120-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
136+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
121137
}
122138
case GOODBYE:
123-
return FDv2SourceResult.goodbye(((FDv2ProtocolHandler.FDv2ActionGoodbye) res).getReason());
139+
return FDv2SourceResult.goodbye(((FDv2ProtocolHandler.FDv2ActionGoodbye) res).getReason(), fdv1Fallback);
124140
case NONE:
125141
break;
126142
case INTERNAL_ERROR: {
@@ -141,7 +157,7 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
141157
0,
142158
"Internal error occurred during polling",
143159
new Date().toInstant());
144-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
160+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
145161
}
146162
}
147163
}
@@ -152,7 +168,7 @@ protected CompletableFuture<FDv2SourceResult> poll(Selector selector, boolean on
152168
"Unexpected end of polling response",
153169
new Date().toInstant()
154170
);
155-
return oneShot ? FDv2SourceResult.terminalError(info) : FDv2SourceResult.interrupted(info);
171+
return oneShot ? FDv2SourceResult.terminalError(info, fdv1Fallback) : FDv2SourceResult.interrupted(info, fdv1Fallback);
156172
}));
157173
}
158174
}

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingInitializerImpl.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.launchdarkly.sdk.server;
22

33
import com.launchdarkly.logging.LDLogger;
4-
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
54
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
65
import com.launchdarkly.sdk.server.datasources.Initializer;
76
import com.launchdarkly.sdk.server.datasources.SelectorSource;

0 commit comments

Comments
 (0)