Skip to content

Commit 2df75d4

Browse files
authored
feat: add config metadata parsing through wasm (#182)
* feat: add config metadata parsing through wasm * fix tests * update the metadata classes to have proper decorators * remove no args decorator * update the metadata classes to have NoArgConstructor for parsing
1 parent efd4ddc commit 2df75d4

File tree

8 files changed

+69
-80
lines changed

8 files changed

+69
-80
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ sourceCompatibility = JavaVersion.VERSION_11
8484
targetCompatibility = JavaVersion.VERSION_11
8585

8686
def wasmResourcePath = "$projectDir/src/main/resources"
87-
def wasmVersion = "1.41.0"
87+
def wasmVersion = "1.42.1"
8888
def wasmUrl = "https://unpkg.com/@devcycle/bucketing-assembly-script@$wasmVersion/build/bucketing-lib.release.wasm"
8989
task downloadDVCBucketingWASM(type: Download) {
9090
src wasmUrl

src/main/java/com/devcycle/sdk/server/local/bucketing/LocalBucketing.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,5 +382,15 @@ private int getSDKKeyAddress(String sdkKey) {
382382

383383
return sdkKeyAddresses.get(sdkKey);
384384
}
385+
386+
public String getConfigMetadata(String sdkKey) {
387+
int sdkKeyAddress = getSDKKeyAddress(sdkKey);
388+
Func getConfigMetadataPtr = linker.get(store, "", "getConfigMetadata").get().func();
389+
WasmFunctions.Function1<Integer, Integer> getConfigMetadata = WasmFunctions.func(
390+
store, getConfigMetadataPtr, I32, I32);
391+
392+
int resultAddress = getConfigMetadata.call(sdkKeyAddress);
393+
return readWasmString(resultAddress);
394+
}
385395
}
386396

src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public final class EnvironmentConfigManager {
3939
private final DevCycleLocalOptions options;
4040

4141
private ProjectConfig config;
42-
private ConfigMetadata configMetadata;
42+
private String configETag = "";
43+
private String configLastModified = "";
4344

4445
private final String sdkKey;
4546
private final int pollingIntervalMS;
@@ -73,9 +74,7 @@ public void run() {
7374
}
7475
} catch (DevCycleException e) {
7576
DevCycleLogger.error("Failed to load config: " + e.getMessage(), e);
76-
} catch (Exception e) {
77-
DevCycleLogger.error("Unexpected error during config fetch: " + e.getMessage(), e);
78-
}
77+
}
7978
}
8079
};
8180

@@ -84,15 +83,7 @@ public boolean isConfigInitialized() {
8483
}
8584

8685
private ProjectConfig getConfig() throws DevCycleException {
87-
// Handle initial request where configMetadata might be null
88-
String etag = null;
89-
String lastModified = null;
90-
if (this.configMetadata != null) {
91-
etag = this.configMetadata.configETag;
92-
lastModified = this.configMetadata.configLastModified;
93-
}
94-
95-
Call<ProjectConfig> config = this.configApiClient.getConfig(this.sdkKey, etag, lastModified);
86+
Call<ProjectConfig> config = this.configApiClient.getConfig(this.sdkKey, this.configETag, this.configLastModified);
9687
ProjectConfig fetchedConfig = getResponseWithRetries(config, 1);
9788
this.config = fetchedConfig;
9889

@@ -204,16 +195,13 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DevCycl
204195
String currentETag = response.headers().get("ETag");
205196
String headerLastModified = response.headers().get("Last-Modified");
206197

207-
// Check if we should skip this config due to older timestamp (only if configMetadata exists)
208-
if (this.configMetadata != null &&
209-
!this.configMetadata.configLastModified.isEmpty() &&
210-
headerLastModified != null && !headerLastModified.isEmpty()) {
198+
if (!this.configLastModified.isEmpty() && headerLastModified != null && !headerLastModified.isEmpty()) {
211199
ZonedDateTime parsedLastModified = ZonedDateTime.parse(
212200
headerLastModified,
213201
DateTimeFormatter.RFC_1123_DATE_TIME
214202
);
215203
ZonedDateTime configLastModified = ZonedDateTime.parse(
216-
this.configMetadata.configLastModified,
204+
this.configLastModified,
217205
DateTimeFormatter.RFC_1123_DATE_TIME
218206
);
219207

@@ -229,23 +217,18 @@ private ProjectConfig getConfigResponse(Call<ProjectConfig> call) throws DevCycl
229217
localBucketing.storeConfig(sdkKey, mapper.writeValueAsString(config));
230218
} catch (JsonProcessingException e) {
231219
if (this.config != null) {
232-
String currentConfigInfo = (this.configMetadata != null) ?
233-
" etag " + this.configMetadata.configETag + " last-modified: " + this.configMetadata.configLastModified :
234-
"";
235-
DevCycleLogger.error("Unable to parse config with etag: " + currentETag + ". Using cache," + currentConfigInfo);
220+
DevCycleLogger.error("Unable to parse config with etag: " + currentETag + ". Using cache, etag " + this.configETag + " last-modified: " + this.configLastModified);
236221
return this.config;
237222
} else {
238223
errorResponse.setMessage(e.getMessage());
239224
throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse);
240225
}
241226
}
242-
this.configMetadata = new ConfigMetadata(currentETag, headerLastModified, config.getProject(), config.getEnvironment());
227+
this.configETag = currentETag;
228+
this.configLastModified = headerLastModified;
243229
return response.body();
244230
} else if (httpResponseCode == HttpResponseCode.NOT_MODIFIED) {
245-
String cacheInfo = (this.configMetadata != null) ?
246-
" etag: " + this.configMetadata.configETag + " last-modified: " + this.configMetadata.configLastModified :
247-
" (no metadata available)";
248-
DevCycleLogger.debug("Config not modified, using cache," + cacheInfo);
231+
DevCycleLogger.debug("Config not modified, using cache, etag: " + this.configETag + " last-modified: " + this.configLastModified);
249232
return this.config;
250233
} else {
251234
if (response.errorBody() != null) {
@@ -291,6 +274,12 @@ public void cleanup() {
291274
}
292275

293276
public ConfigMetadata getConfigMetadata() {
294-
return configMetadata;
277+
String configMetadata = localBucketing.getConfigMetadata(this.sdkKey);
278+
try {
279+
return OBJECT_MAPPER.readValue(configMetadata, ConfigMetadata.class);
280+
} catch (JsonProcessingException e) {
281+
DevCycleLogger.warning("Unable to parse config metadata: " + e.getMessage());
282+
return new ConfigMetadata();
283+
}
295284
}
296285
}
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
package com.devcycle.sdk.server.local.model;
22

3+
import lombok.AllArgsConstructor;
4+
import lombok.NoArgsConstructor;
5+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
6+
7+
@AllArgsConstructor
8+
@NoArgsConstructor
9+
@JsonIgnoreProperties(ignoreUnknown = true)
310
public class ConfigMetadata {
411

5-
public final String configETag;
6-
public final String configLastModified;
7-
public final ProjectMetadata project;
8-
public final EnvironmentMetadata environment;
12+
public ProjectMetadata project;
13+
public EnvironmentMetadata environment;
914

10-
public ConfigMetadata(String currentETag, String headerLastModified, Project project, Environment environment) {
11-
this.configETag = currentETag;
12-
this.configLastModified = headerLastModified;
13-
this.project = new ProjectMetadata(project._id, project.key);
14-
this.environment = new EnvironmentMetadata(environment._id, environment.key);
15-
}
1615
}
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.devcycle.sdk.server.local.model;
22

3-
public class EnvironmentMetadata {
4-
public final String id;
5-
public final String key;
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.NoArgsConstructor;
6+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
67

7-
public EnvironmentMetadata(String id, String key) {
8-
this.id = id;
9-
this.key = key;
10-
}
8+
@AllArgsConstructor
9+
@NoArgsConstructor
10+
@JsonIgnoreProperties(ignoreUnknown = true)
11+
public class EnvironmentMetadata {
12+
@JsonProperty("id")
13+
public String id;
14+
@JsonProperty("key")
15+
public String key;
1116
}
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.devcycle.sdk.server.local.model;
22

3-
public class ProjectMetadata {
4-
public final String id;
5-
public final String key;
3+
import lombok.AllArgsConstructor;
4+
import lombok.NoArgsConstructor;
5+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
67

7-
public ProjectMetadata(String id, String key) {
8-
this.id = id;
9-
this.key = key;
10-
}
8+
@AllArgsConstructor
9+
@NoArgsConstructor
10+
@JsonIgnoreProperties(ignoreUnknown = true)
11+
public class ProjectMetadata {
12+
@JsonProperty("id")
13+
public String id;
14+
@JsonProperty("key")
15+
public String key;
1116
}
1.47 KB
Binary file not shown.

src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import com.devcycle.sdk.server.local.model.ConfigMetadata;
3838
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
3939
import com.devcycle.sdk.server.local.model.Environment;
40+
import com.devcycle.sdk.server.local.model.EnvironmentMetadata;
4041
import com.devcycle.sdk.server.local.model.Project;
42+
import com.devcycle.sdk.server.local.model.ProjectMetadata;
4143

4244
@RunWith(MockitoJUnitRunner.class)
4345
public class DevCycleLocalClientTest {
@@ -1035,14 +1037,9 @@ public void after(HookContext<String> ctx, Variable<String> variable) {
10351037

10361038
// Check that config metadata has the expected structure
10371039
ConfigMetadata metadata = ctx.getMetadata();
1038-
Assert.assertNotNull("Config ETag should not be null", metadata.configETag);
1039-
Assert.assertNotNull("Config last modified should not be null", metadata.configLastModified);
10401040
Assert.assertNotNull("Project metadata should not be null", metadata.project);
10411041
Assert.assertNotNull("Environment metadata should not be null", metadata.environment);
10421042

1043-
// Verify basic metadata structure is present
1044-
Assert.assertFalse("Config ETag should not be empty", metadata.configETag.isEmpty());
1045-
Assert.assertFalse("Config last modified should not be empty", metadata.configLastModified.isEmpty());
10461043

10471044
metadataChecked[0] = true;
10481045
}
@@ -1089,11 +1086,9 @@ public void onFinally(HookContext<String> ctx, Optional<Variable<String>> variab
10891086

10901087
// Verify metadata is consistent across all hook stages
10911088
Assert.assertEquals("Before and after metadata should be the same",
1092-
capturedMetadata[0].configETag, capturedMetadata[1].configETag);
1089+
capturedMetadata[0], capturedMetadata[1]);
10931090
Assert.assertEquals("Before and finally metadata should be the same",
1094-
capturedMetadata[0].configETag, capturedMetadata[2].configETag);
1095-
Assert.assertEquals("Metadata timestamps should be consistent",
1096-
capturedMetadata[0].configLastModified, capturedMetadata[1].configLastModified);
1091+
capturedMetadata[0], capturedMetadata[2]);
10971092
}
10981093

10991094
@Test
@@ -1115,8 +1110,6 @@ public void error(HookContext<String> ctx, Throwable error) {
11151110
// Verify metadata is accessible even in error hook
11161111
Assert.assertNotNull("Metadata should be accessible in error hook", ctx.getMetadata());
11171112
ConfigMetadata metadata = ctx.getMetadata();
1118-
Assert.assertNotNull("Config ETag should not be null in error hook", metadata.configETag);
1119-
Assert.assertNotNull("Config last modified should not be null in error hook", metadata.configLastModified);
11201113
Assert.assertNotNull("Project metadata should not be null in error hook", metadata.project);
11211114
Assert.assertNotNull("Environment metadata should not be null in error hook", metadata.environment);
11221115
metadataCheckedInError[0] = true;
@@ -1152,15 +1145,10 @@ public void after(HookContext<String> ctx, Variable<String> variable) {
11521145
ConfigMetadata directMetadata = client.getMetadata();
11531146
Assert.assertNotNull("Direct metadata should not be null", directMetadata);
11541147

1155-
// The metadata in hooks should match the current client metadata
1156-
Assert.assertEquals("Hook metadata ETag should match current metadata",
1157-
directMetadata.configETag, capturedMetadata[0].configETag);
1158-
Assert.assertEquals("Hook metadata timestamp should match current metadata",
1159-
directMetadata.configLastModified, capturedMetadata[0].configLastModified);
11601148
Assert.assertEquals("Hook metadata project should match current metadata",
1161-
directMetadata.project, capturedMetadata[0].project);
1149+
directMetadata.project.id, capturedMetadata[0].project.id);
11621150
Assert.assertEquals("Hook metadata environment should match current metadata",
1163-
directMetadata.environment, capturedMetadata[0].environment);
1151+
directMetadata.environment.id, capturedMetadata[0].environment.id);
11641152
}
11651153

11661154
@Test
@@ -1177,7 +1165,6 @@ public void variable_withMultipleHooks_allReceiveMetadata() throws DevCycleExcep
11771165
public void after(HookContext<String> ctx, Variable<String> variable) {
11781166
Assert.assertNotNull("First hook should receive metadata", ctx.getMetadata());
11791167
Assert.assertNotNull("First hook metadata should have project", ctx.getMetadata().project);
1180-
Assert.assertNotNull("First hook metadata should have config ETag", ctx.getMetadata().configETag);
11811168
metadataChecked[0] = true;
11821169
}
11831170
});
@@ -1188,7 +1175,6 @@ public void after(HookContext<String> ctx, Variable<String> variable) {
11881175
public void after(HookContext<String> ctx, Variable<String> variable) {
11891176
Assert.assertNotNull("Second hook should receive metadata", ctx.getMetadata());
11901177
Assert.assertNotNull("Second hook metadata should have environment", ctx.getMetadata().environment);
1191-
Assert.assertNotNull("Second hook metadata should have last modified", ctx.getMetadata().configLastModified);
11921178
metadataChecked[1] = true;
11931179
}
11941180
});
@@ -1212,16 +1198,12 @@ public void configMetadata_canBeConstructedWithMockData() {
12121198

12131199
// Test ConfigMetadata construction
12141200
ConfigMetadata metadata = new ConfigMetadata(
1215-
"test-etag-12345",
1216-
"2023-10-01T12:00:00Z",
1217-
mockProject,
1218-
mockEnvironment
1201+
new ProjectMetadata(mockProject._id, mockProject.key),
1202+
new EnvironmentMetadata(mockEnvironment._id, mockEnvironment.key)
12191203
);
12201204

12211205
// Verify metadata is properly constructed
12221206
Assert.assertNotNull("Metadata should not be null", metadata);
1223-
Assert.assertEquals("Config ETag should match", "test-etag-12345", metadata.configETag);
1224-
Assert.assertEquals("Config last modified should match", "2023-10-01T12:00:00Z", metadata.configLastModified);
12251207
Assert.assertNotNull("Project metadata should not be null", metadata.project);
12261208
Assert.assertNotNull("Environment metadata should not be null", metadata.environment);
12271209

@@ -1231,6 +1213,5 @@ public void configMetadata_canBeConstructedWithMockData() {
12311213

12321214
Assert.assertNotNull("HookContext should not be null", contextWithMetadata);
12331215
Assert.assertEquals("Metadata should be accessible from context", metadata, contextWithMetadata.getMetadata());
1234-
Assert.assertEquals("Config ETag should be accessible", "test-etag-12345", contextWithMetadata.getMetadata().configETag);
12351216
}
12361217
}

0 commit comments

Comments
 (0)