Skip to content

Commit 30cf64b

Browse files
committed
refactor: Move instance-id generation to ClientContext
Generating X-LaunchDarkly-Instance-Id inside HttpConfigurationBuilderImpl.build() tied the value to a specific subsystem builder. Any code path that goes through HttpConfiguration would see a value, but other subsystems built from ClientContext (which is the canonical per-LDClient state holder) had no way to read the same id. Add an instanceId field, getter, and a nine-argument constructor variant to ClientContext. The eight-argument constructor auto-generates a v4 UUID for any caller that does not need to pass an explicit value; the copy constructor propagates the existing value, so derived ClientContext instances stay in sync. ClientContextImpl.fromConfig now generates the UUID once and threads it through each of the three ClientContext variants it constructs during LDClient init. HttpConfigurationBuilderImpl reads from ClientContext.getInstanceId() instead of generating its own. Existing tests in DefaultFeatureRequestorTest / LDConfigTest / StreamProcessorTest that already accounted for per-build randomness continue to work because each `clientContext(...)` they construct gets its own auto-generated id. Update HttpConfigurationBuilderTest to assert the new contract: the builder mirrors whatever id the context provides; two distinct ClientContexts produce distinct ids; one context produces a stable id across multiple builds. Verified locally with `./gradlew test` against lib/sdk/server: all 17 of 17 test classes pass, including 15 of 15 HttpConfigurationBuilderTest cases.
1 parent 12002c0 commit 30cf64b

4 files changed

Lines changed: 111 additions & 29 deletions

File tree

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
99
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
1010

11+
import java.util.UUID;
1112
import java.util.concurrent.Executors;
1213
import java.util.concurrent.ScheduledExecutorService;
1314

@@ -37,7 +38,7 @@ private ClientContextImpl(
3738
) {
3839
super(baseContext.getSdkKey(), baseContext.getApplicationInfo(), baseContext.getHttp(),
3940
baseContext.getLogging(), baseContext.isOffline(), baseContext.getServiceEndpoints(),
40-
baseContext.getThreadPriority(), baseContext.getWrapperInfo());
41+
baseContext.getThreadPriority(), baseContext.getWrapperInfo(), baseContext.getInstanceId());
4142
this.sharedExecutor = sharedExecutor;
4243
this.diagnosticStore = diagnosticStore;
4344
this.dataSourceUpdateSink = null;
@@ -79,22 +80,29 @@ static ClientContextImpl fromConfig(
7980
LDConfig config,
8081
ScheduledExecutorService sharedExecutor
8182
) {
83+
// Generate the instance ID once and thread it through every ClientContext we build for this
84+
// LDClient. Subsystems built from any of these contexts will all observe the same value.
85+
String instanceId = UUID.randomUUID().toString();
86+
8287
ClientContext minimalContext = new ClientContext(sdkKey, config.applicationInfo, null,
83-
null, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo);
88+
null, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo,
89+
instanceId);
8490
LoggingConfiguration loggingConfig = config.logging.build(minimalContext);
85-
91+
8692
ClientContext contextWithLogging = new ClientContext(sdkKey, config.applicationInfo, null,
87-
loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo);
93+
loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority,
94+
config.wrapperInfo, instanceId);
8895
HttpConfiguration httpConfig = config.http.build(contextWithLogging);
89-
96+
9097
if (httpConfig.getProxy() != null) {
9198
contextWithLogging.getBaseLogger().info("Using proxy: {} {} authentication.",
9299
httpConfig.getProxy(),
93100
httpConfig.getProxyAuthentication() == null ? "without" : "with");
94101
}
95-
96-
ClientContext contextWithHttpAndLogging = new ClientContext(sdkKey, config.applicationInfo, httpConfig,
97-
loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo);
102+
103+
ClientContext contextWithHttpAndLogging = new ClientContext(sdkKey, config.applicationInfo,
104+
httpConfig, loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority,
105+
config.wrapperInfo, instanceId);
98106

99107
// Create a diagnostic store only if diagnostics are enabled. Diagnostics are enabled as long as 1. the
100108
// opt-out property was not set in the config, and 2. we are using the standard event processor.

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
import java.time.Duration;
4848
import java.util.HashMap;
4949
import java.util.Map;
50-
import java.util.UUID;
5150
import java.util.concurrent.CompletableFuture;
5251
import java.util.concurrent.Future;
5352

@@ -350,12 +349,13 @@ else if (wrapperName != null) {
350349
headers.put("X-LaunchDarkly-Wrapper", wrapperId);
351350
}
352351

353-
// Per SCMP-server-connection-minutes-polling, every polling request must carry a per-instance
354-
// GUID v4. We attach it to the default headers (rather than only on the poller) so that it is
355-
// also present on streaming and event requests; this matches the cross-SDK contract tests and
356-
// keeps the GUID stable for the lifetime of the SDK instance, since the default headers map
357-
// is built once per HttpConfiguration and never modified afterwards.
358-
headers.put(INSTANCE_ID_HEADER, UUID.randomUUID().toString());
352+
// The instance ID originates on ClientContext (generated once when LDClient is constructed)
353+
// so every subsystem built from the same context observes a consistent value for the
354+
// lifetime of the SDK instance.
355+
String instanceId = clientContext.getInstanceId();
356+
if (instanceId != null && !instanceId.isEmpty()) {
357+
headers.put(INSTANCE_ID_HEADER, instanceId);
358+
}
359359

360360
// For consistency with other SDKs, custom headers are allowed to overwrite headers such as
361361
// User-Agent and Authorization.

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints;
99
import com.launchdarkly.sdk.server.interfaces.WrapperInfo;
1010

11+
import java.util.UUID;
12+
1113
/**
1214
* Context information provided by the {@link com.launchdarkly.sdk.server.LDClient} when creating components.
1315
* <p>
@@ -31,11 +33,18 @@ public class ClientContext {
3133
private final boolean offline;
3234
private final ServiceEndpoints serviceEndpoints;
3335
private final int threadPriority;
36+
private final String instanceId;
3437
private WrapperInfo wrapperInfo;
3538

3639
/**
37-
* Constructor that sets all properties. All should be non-null.
38-
*
40+
* Constructor that sets all properties including an explicit instance ID. All should be
41+
* non-null.
42+
*
43+
* <p>The instance ID is sent on every outbound request in the {@code X-LaunchDarkly-Instance-Id}
44+
* header. It must be generated once per LDClient and remain stable for the client's lifetime.
45+
* The eight-argument constructor auto-generates a v4 UUID for callers that do not need to
46+
* supply their own value.
47+
*
3948
* @param sdkKey the SDK key
4049
* @param applicationInfo application metadata properties from
4150
* {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)}
@@ -46,6 +55,7 @@ public class ClientContext {
4655
* {@link Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder)}
4756
* @param threadPriority worker thread priority from {@link Builder#threadPriority(int)}
4857
* @param wrapperInfo wrapper configuration from {@link Builder#wrapper(com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder)}
58+
* @param instanceId per-LDClient identifier for the {@code X-LaunchDarkly-Instance-Id} header
4959
*/
5060
public ClientContext(
5161
String sdkKey,
@@ -55,7 +65,8 @@ public ClientContext(
5565
boolean offline,
5666
ServiceEndpoints serviceEndpoints,
5767
int threadPriority,
58-
WrapperInfo wrapperInfo
68+
WrapperInfo wrapperInfo,
69+
String instanceId
5970
) {
6071
this.sdkKey = sdkKey;
6172
this.applicationInfo = applicationInfo;
@@ -65,19 +76,51 @@ public ClientContext(
6576
this.serviceEndpoints = serviceEndpoints;
6677
this.threadPriority = threadPriority;
6778
this.wrapperInfo = wrapperInfo;
68-
79+
this.instanceId = instanceId;
80+
6981
this.baseLogger = logging == null ? LDLogger.none() :
7082
LDLogger.withAdapter(logging.getLogAdapter(), logging.getBaseLoggerName());
7183
}
72-
84+
85+
/**
86+
* Constructor that sets all properties. All should be non-null. Auto-generates a v4 UUID for
87+
* the instance ID; use the nine-argument constructor if you need to thread an existing value
88+
* through (for example, when copying a context for an in-flight LDClient).
89+
*
90+
* @param sdkKey the SDK key
91+
* @param applicationInfo application metadata properties from
92+
* {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)}
93+
* @param http HTTP configuration properties from {@link Builder#http(ComponentConfigurer)}
94+
* @param logging logging configuration properties from {@link Builder#logging(ComponentConfigurer)}
95+
* @param offline true if the SDK should be entirely offline
96+
* @param serviceEndpoints service endpoint URI properties from
97+
* {@link Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder)}
98+
* @param threadPriority worker thread priority from {@link Builder#threadPriority(int)}
99+
* @param wrapperInfo wrapper configuration from {@link Builder#wrapper(com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder)}
100+
*/
101+
public ClientContext(
102+
String sdkKey,
103+
ApplicationInfo applicationInfo,
104+
HttpConfiguration http,
105+
LoggingConfiguration logging,
106+
boolean offline,
107+
ServiceEndpoints serviceEndpoints,
108+
int threadPriority,
109+
WrapperInfo wrapperInfo
110+
) {
111+
this(sdkKey, applicationInfo, http, logging, offline, serviceEndpoints, threadPriority,
112+
wrapperInfo, UUID.randomUUID().toString());
113+
}
114+
73115
/**
74116
* Copy constructor.
75-
*
117+
*
76118
* @param copyFrom the instance to copy from
77119
*/
78120
protected ClientContext(ClientContext copyFrom) {
79121
this(copyFrom.sdkKey, copyFrom.applicationInfo, copyFrom.http, copyFrom.logging,
80-
copyFrom.offline, copyFrom.serviceEndpoints, copyFrom.threadPriority, copyFrom.wrapperInfo);
122+
copyFrom.offline, copyFrom.serviceEndpoints, copyFrom.threadPriority, copyFrom.wrapperInfo,
123+
copyFrom.instanceId);
81124
}
82125

83126
/**
@@ -199,13 +242,24 @@ public ServiceEndpoints getServiceEndpoints() {
199242
/**
200243
* Returns the worker thread priority that is set by
201244
* {@link Builder#threadPriority(int)}.
202-
*
245+
*
203246
* @return the thread priority
204247
*/
205248
public int getThreadPriority() {
206249
return threadPriority;
207250
}
208251

252+
/**
253+
* Returns the per-LDClient instance identifier sent in the {@code X-LaunchDarkly-Instance-Id}
254+
* header on outbound requests. The value is generated once when the {@link ClientContext} is
255+
* constructed and is stable for the client's lifetime.
256+
*
257+
* @return the instance ID
258+
*/
259+
public String getInstanceId() {
260+
return instanceId;
261+
}
262+
209263
/**
210264
* Returns the wrapper information.
211265
*

lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,46 @@ public void testCanSetCustomHeaders() {
106106
}
107107

108108
@Test
109-
public void testInstanceIdHeaderIsUuidV4() {
109+
public void testInstanceIdHeaderMirrorsClientContext() {
110+
// The HTTP builder emits whatever instance ID ClientContext provides; generation is the
111+
// LDClient/ClientContext's responsibility, not the builder's.
110112
HttpConfiguration hc = Components.httpConfiguration().build(BASIC_CONTEXT);
111-
assertHasInstanceIdHeader(hc);
113+
String headerValue = assertHasInstanceIdHeader(hc);
114+
assertEquals(BASIC_CONTEXT.getInstanceId(), headerValue);
115+
}
116+
117+
@Test
118+
public void testInstanceIdIsDifferentBetweenClientContexts() {
119+
// Each ClientContext auto-generates its own instance ID, so building HttpConfigurations
120+
// from two distinct contexts produces distinct header values. This is the
121+
// cross-SDK-instance uniqueness property the contract tests assert against.
122+
ClientContext context1 = new ClientContext(SDK_KEY);
123+
ClientContext context2 = new ClientContext(SDK_KEY);
124+
HttpConfiguration hc1 = Components.httpConfiguration().build(context1);
125+
HttpConfiguration hc2 = Components.httpConfiguration().build(context2);
126+
String id1 = assertHasInstanceIdHeader(hc1);
127+
String id2 = assertHasInstanceIdHeader(hc2);
128+
assertNotEquals("each SDK instance should have its own instance id", id1, id2);
112129
}
113130

114131
@Test
115-
public void testInstanceIdIsDifferentBetweenHttpConfigurations() {
116-
// Each call to build() represents a new SDK instance; each must get its own GUID.
132+
public void testInstanceIdHeaderIsStableAcrossBuildsFromSameContext() {
133+
// Multiple build() calls against the same ClientContext (which is what happens during
134+
// LDClient initialization when ClientContextImpl rebuilds the context across logging/HTTP
135+
// stages) must produce the same instance id.
117136
HttpConfiguration hc1 = Components.httpConfiguration().build(BASIC_CONTEXT);
118137
HttpConfiguration hc2 = Components.httpConfiguration().build(BASIC_CONTEXT);
119138
String id1 = assertHasInstanceIdHeader(hc1);
120139
String id2 = assertHasInstanceIdHeader(hc2);
121-
assertNotEquals("each SDK instance should generate its own instance id", id1, id2);
140+
assertEquals(id1, id2);
141+
assertEquals(BASIC_CONTEXT.getInstanceId(), id1);
122142
}
123143

124144
@Test
125145
public void testInstanceIdHeaderIsNotOverriddenByCustomHeaders() {
126146
// The default-headers map is built once per HttpConfiguration; a user-supplied custom header
127147
// for X-LaunchDarkly-Instance-Id is allowed to replace the SDK-generated value, but absent
128-
// that, the SDK's generated UUID must come through.
148+
// that, the context-supplied UUID must come through.
129149
HttpConfiguration hc = Components.httpConfiguration()
130150
.addCustomHeader("X-Some-Other-Header", "value")
131151
.build(BASIC_CONTEXT);

0 commit comments

Comments
 (0)