diff --git a/build.gradle b/build.gradle index 359b541a..9eaa6752 100644 --- a/build.gradle +++ b/build.gradle @@ -84,7 +84,7 @@ sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 def wasmResourcePath = "$projectDir/src/main/resources" -def wasmVersion = "1.41.0" +def wasmVersion = "1.42.1" def wasmUrl = "https://unpkg.com/@devcycle/bucketing-assembly-script@$wasmVersion/build/bucketing-lib.release.wasm" task downloadDVCBucketingWASM(type: Download) { src wasmUrl diff --git a/src/main/java/com/devcycle/sdk/server/local/bucketing/LocalBucketing.java b/src/main/java/com/devcycle/sdk/server/local/bucketing/LocalBucketing.java index ff97a7bb..923b647a 100644 --- a/src/main/java/com/devcycle/sdk/server/local/bucketing/LocalBucketing.java +++ b/src/main/java/com/devcycle/sdk/server/local/bucketing/LocalBucketing.java @@ -382,5 +382,15 @@ private int getSDKKeyAddress(String sdkKey) { return sdkKeyAddresses.get(sdkKey); } + + public String getConfigMetadata(String sdkKey) { + int sdkKeyAddress = getSDKKeyAddress(sdkKey); + Func getConfigMetadataPtr = linker.get(store, "", "getConfigMetadata").get().func(); + WasmFunctions.Function1 getConfigMetadata = WasmFunctions.func( + store, getConfigMetadataPtr, I32, I32); + + int resultAddress = getConfigMetadata.call(sdkKeyAddress); + return readWasmString(resultAddress); + } } diff --git a/src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java b/src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java index 2dafbc3f..5348da05 100644 --- a/src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java +++ b/src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.java @@ -39,7 +39,8 @@ public final class EnvironmentConfigManager { private final DevCycleLocalOptions options; private ProjectConfig config; - private ConfigMetadata configMetadata; + private String configETag = ""; + private String configLastModified = ""; private final String sdkKey; private final int pollingIntervalMS; @@ -73,9 +74,7 @@ public void run() { } } catch (DevCycleException e) { DevCycleLogger.error("Failed to load config: " + e.getMessage(), e); - } catch (Exception e) { - DevCycleLogger.error("Unexpected error during config fetch: " + e.getMessage(), e); - } + } } }; @@ -84,15 +83,7 @@ public boolean isConfigInitialized() { } private ProjectConfig getConfig() throws DevCycleException { - // Handle initial request where configMetadata might be null - String etag = null; - String lastModified = null; - if (this.configMetadata != null) { - etag = this.configMetadata.configETag; - lastModified = this.configMetadata.configLastModified; - } - - Call config = this.configApiClient.getConfig(this.sdkKey, etag, lastModified); + Call config = this.configApiClient.getConfig(this.sdkKey, this.configETag, this.configLastModified); ProjectConfig fetchedConfig = getResponseWithRetries(config, 1); this.config = fetchedConfig; @@ -204,16 +195,13 @@ private ProjectConfig getConfigResponse(Call call) throws DevCycl String currentETag = response.headers().get("ETag"); String headerLastModified = response.headers().get("Last-Modified"); - // Check if we should skip this config due to older timestamp (only if configMetadata exists) - if (this.configMetadata != null && - !this.configMetadata.configLastModified.isEmpty() && - headerLastModified != null && !headerLastModified.isEmpty()) { + if (!this.configLastModified.isEmpty() && headerLastModified != null && !headerLastModified.isEmpty()) { ZonedDateTime parsedLastModified = ZonedDateTime.parse( headerLastModified, DateTimeFormatter.RFC_1123_DATE_TIME ); ZonedDateTime configLastModified = ZonedDateTime.parse( - this.configMetadata.configLastModified, + this.configLastModified, DateTimeFormatter.RFC_1123_DATE_TIME ); @@ -229,23 +217,18 @@ private ProjectConfig getConfigResponse(Call call) throws DevCycl localBucketing.storeConfig(sdkKey, mapper.writeValueAsString(config)); } catch (JsonProcessingException e) { if (this.config != null) { - String currentConfigInfo = (this.configMetadata != null) ? - " etag " + this.configMetadata.configETag + " last-modified: " + this.configMetadata.configLastModified : - ""; - DevCycleLogger.error("Unable to parse config with etag: " + currentETag + ". Using cache," + currentConfigInfo); + DevCycleLogger.error("Unable to parse config with etag: " + currentETag + ". Using cache, etag " + this.configETag + " last-modified: " + this.configLastModified); return this.config; } else { errorResponse.setMessage(e.getMessage()); throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse); } } - this.configMetadata = new ConfigMetadata(currentETag, headerLastModified, config.getProject(), config.getEnvironment()); + this.configETag = currentETag; + this.configLastModified = headerLastModified; return response.body(); } else if (httpResponseCode == HttpResponseCode.NOT_MODIFIED) { - String cacheInfo = (this.configMetadata != null) ? - " etag: " + this.configMetadata.configETag + " last-modified: " + this.configMetadata.configLastModified : - " (no metadata available)"; - DevCycleLogger.debug("Config not modified, using cache," + cacheInfo); + DevCycleLogger.debug("Config not modified, using cache, etag: " + this.configETag + " last-modified: " + this.configLastModified); return this.config; } else { if (response.errorBody() != null) { @@ -291,6 +274,12 @@ public void cleanup() { } public ConfigMetadata getConfigMetadata() { - return configMetadata; + String configMetadata = localBucketing.getConfigMetadata(this.sdkKey); + try { + return OBJECT_MAPPER.readValue(configMetadata, ConfigMetadata.class); + } catch (JsonProcessingException e) { + DevCycleLogger.warning("Unable to parse config metadata: " + e.getMessage()); + return new ConfigMetadata(); + } } } \ No newline at end of file diff --git a/src/main/java/com/devcycle/sdk/server/local/model/ConfigMetadata.java b/src/main/java/com/devcycle/sdk/server/local/model/ConfigMetadata.java index 120474b2..c3a73ec3 100644 --- a/src/main/java/com/devcycle/sdk/server/local/model/ConfigMetadata.java +++ b/src/main/java/com/devcycle/sdk/server/local/model/ConfigMetadata.java @@ -1,16 +1,15 @@ package com.devcycle.sdk.server.local.model; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ConfigMetadata { - public final String configETag; - public final String configLastModified; - public final ProjectMetadata project; - public final EnvironmentMetadata environment; + public ProjectMetadata project; + public EnvironmentMetadata environment; - public ConfigMetadata(String currentETag, String headerLastModified, Project project, Environment environment) { - this.configETag = currentETag; - this.configLastModified = headerLastModified; - this.project = new ProjectMetadata(project._id, project.key); - this.environment = new EnvironmentMetadata(environment._id, environment.key); - } } diff --git a/src/main/java/com/devcycle/sdk/server/local/model/EnvironmentMetadata.java b/src/main/java/com/devcycle/sdk/server/local/model/EnvironmentMetadata.java index 515907cb..c0a781fb 100644 --- a/src/main/java/com/devcycle/sdk/server/local/model/EnvironmentMetadata.java +++ b/src/main/java/com/devcycle/sdk/server/local/model/EnvironmentMetadata.java @@ -1,11 +1,16 @@ package com.devcycle.sdk.server.local.model; -public class EnvironmentMetadata { - public final String id; - public final String key; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - public EnvironmentMetadata(String id, String key) { - this.id = id; - this.key = key; - } +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class EnvironmentMetadata { + @JsonProperty("id") + public String id; + @JsonProperty("key") + public String key; } diff --git a/src/main/java/com/devcycle/sdk/server/local/model/ProjectMetadata.java b/src/main/java/com/devcycle/sdk/server/local/model/ProjectMetadata.java index 464dc2a9..8068f30e 100644 --- a/src/main/java/com/devcycle/sdk/server/local/model/ProjectMetadata.java +++ b/src/main/java/com/devcycle/sdk/server/local/model/ProjectMetadata.java @@ -1,11 +1,16 @@ package com.devcycle.sdk.server.local.model; -public class ProjectMetadata { - public final String id; - public final String key; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; - public ProjectMetadata(String id, String key) { - this.id = id; - this.key = key; - } +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProjectMetadata { + @JsonProperty("id") + public String id; + @JsonProperty("key") + public String key; } diff --git a/src/main/resources/bucketing-lib.release.wasm b/src/main/resources/bucketing-lib.release.wasm index 80994caa..bb185566 100644 Binary files a/src/main/resources/bucketing-lib.release.wasm and b/src/main/resources/bucketing-lib.release.wasm differ diff --git a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java index 30156a65..4b209d1a 100644 --- a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java @@ -37,7 +37,9 @@ import com.devcycle.sdk.server.local.model.ConfigMetadata; import com.devcycle.sdk.server.local.model.DevCycleLocalOptions; import com.devcycle.sdk.server.local.model.Environment; +import com.devcycle.sdk.server.local.model.EnvironmentMetadata; import com.devcycle.sdk.server.local.model.Project; +import com.devcycle.sdk.server.local.model.ProjectMetadata; @RunWith(MockitoJUnitRunner.class) public class DevCycleLocalClientTest { @@ -1035,14 +1037,9 @@ public void after(HookContext ctx, Variable variable) { // Check that config metadata has the expected structure ConfigMetadata metadata = ctx.getMetadata(); - Assert.assertNotNull("Config ETag should not be null", metadata.configETag); - Assert.assertNotNull("Config last modified should not be null", metadata.configLastModified); Assert.assertNotNull("Project metadata should not be null", metadata.project); Assert.assertNotNull("Environment metadata should not be null", metadata.environment); - // Verify basic metadata structure is present - Assert.assertFalse("Config ETag should not be empty", metadata.configETag.isEmpty()); - Assert.assertFalse("Config last modified should not be empty", metadata.configLastModified.isEmpty()); metadataChecked[0] = true; } @@ -1089,11 +1086,9 @@ public void onFinally(HookContext ctx, Optional> variab // Verify metadata is consistent across all hook stages Assert.assertEquals("Before and after metadata should be the same", - capturedMetadata[0].configETag, capturedMetadata[1].configETag); + capturedMetadata[0], capturedMetadata[1]); Assert.assertEquals("Before and finally metadata should be the same", - capturedMetadata[0].configETag, capturedMetadata[2].configETag); - Assert.assertEquals("Metadata timestamps should be consistent", - capturedMetadata[0].configLastModified, capturedMetadata[1].configLastModified); + capturedMetadata[0], capturedMetadata[2]); } @Test @@ -1115,8 +1110,6 @@ public void error(HookContext ctx, Throwable error) { // Verify metadata is accessible even in error hook Assert.assertNotNull("Metadata should be accessible in error hook", ctx.getMetadata()); ConfigMetadata metadata = ctx.getMetadata(); - Assert.assertNotNull("Config ETag should not be null in error hook", metadata.configETag); - Assert.assertNotNull("Config last modified should not be null in error hook", metadata.configLastModified); Assert.assertNotNull("Project metadata should not be null in error hook", metadata.project); Assert.assertNotNull("Environment metadata should not be null in error hook", metadata.environment); metadataCheckedInError[0] = true; @@ -1152,15 +1145,10 @@ public void after(HookContext ctx, Variable variable) { ConfigMetadata directMetadata = client.getMetadata(); Assert.assertNotNull("Direct metadata should not be null", directMetadata); - // The metadata in hooks should match the current client metadata - Assert.assertEquals("Hook metadata ETag should match current metadata", - directMetadata.configETag, capturedMetadata[0].configETag); - Assert.assertEquals("Hook metadata timestamp should match current metadata", - directMetadata.configLastModified, capturedMetadata[0].configLastModified); Assert.assertEquals("Hook metadata project should match current metadata", - directMetadata.project, capturedMetadata[0].project); + directMetadata.project.id, capturedMetadata[0].project.id); Assert.assertEquals("Hook metadata environment should match current metadata", - directMetadata.environment, capturedMetadata[0].environment); + directMetadata.environment.id, capturedMetadata[0].environment.id); } @Test @@ -1177,7 +1165,6 @@ public void variable_withMultipleHooks_allReceiveMetadata() throws DevCycleExcep public void after(HookContext ctx, Variable variable) { Assert.assertNotNull("First hook should receive metadata", ctx.getMetadata()); Assert.assertNotNull("First hook metadata should have project", ctx.getMetadata().project); - Assert.assertNotNull("First hook metadata should have config ETag", ctx.getMetadata().configETag); metadataChecked[0] = true; } }); @@ -1188,7 +1175,6 @@ public void after(HookContext ctx, Variable variable) { public void after(HookContext ctx, Variable variable) { Assert.assertNotNull("Second hook should receive metadata", ctx.getMetadata()); Assert.assertNotNull("Second hook metadata should have environment", ctx.getMetadata().environment); - Assert.assertNotNull("Second hook metadata should have last modified", ctx.getMetadata().configLastModified); metadataChecked[1] = true; } }); @@ -1212,16 +1198,12 @@ public void configMetadata_canBeConstructedWithMockData() { // Test ConfigMetadata construction ConfigMetadata metadata = new ConfigMetadata( - "test-etag-12345", - "2023-10-01T12:00:00Z", - mockProject, - mockEnvironment + new ProjectMetadata(mockProject._id, mockProject.key), + new EnvironmentMetadata(mockEnvironment._id, mockEnvironment.key) ); // Verify metadata is properly constructed Assert.assertNotNull("Metadata should not be null", metadata); - Assert.assertEquals("Config ETag should match", "test-etag-12345", metadata.configETag); - Assert.assertEquals("Config last modified should match", "2023-10-01T12:00:00Z", metadata.configLastModified); Assert.assertNotNull("Project metadata should not be null", metadata.project); Assert.assertNotNull("Environment metadata should not be null", metadata.environment); @@ -1231,6 +1213,5 @@ public void configMetadata_canBeConstructedWithMockData() { Assert.assertNotNull("HookContext should not be null", contextWithMetadata); Assert.assertEquals("Metadata should be accessible from context", metadata, contextWithMetadata.getMetadata()); - Assert.assertEquals("Config ETag should be accessible", "test-etag-12345", contextWithMetadata.getMetadata().configETag); } } \ No newline at end of file