diff --git a/.github/workflows/java-ci.yml b/.github/workflows/java-ci.yml index 8a0a0cb..ffbd2e0 100644 --- a/.github/workflows/java-ci.yml +++ b/.github/workflows/java-ci.yml @@ -42,7 +42,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: zulu cache: gradle diff --git a/gradle.properties b/gradle.properties index f3ba253..3767aff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=8.3.0 +version=8.4.0 diff --git a/src/main/java/com/configcat/ConfigService.java b/src/main/java/com/configcat/ConfigService.java index 277f59f..40e2c85 100644 --- a/src/main/java/com/configcat/ConfigService.java +++ b/src/main/java/com/configcat/ConfigService.java @@ -26,7 +26,7 @@ public class ConfigService implements Closeable { private ScheduledExecutorService pollScheduler; private ScheduledExecutorService initScheduler; private CompletableFuture> runningTask; - private boolean initialized = false; + private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean closed = new AtomicBoolean(false); private final AtomicBoolean offline; private final ReentrantLock lock = new ReentrantLock(true); @@ -57,8 +57,7 @@ public ConfigService(String sdkKey, this.initScheduler.schedule(() -> { lock.lock(); try { - if (!initialized) { - initialized = true; + if (initialized.compareAndSet(false, true)) { this.configCatHooks.invokeOnClientReady(); String message = ConfigCatLogMessages.getAutoPollMaxInitWaitTimeReached(autoPollingMode.getMaxInitWaitTimeSeconds()); this.logger.warn(4200, message); @@ -75,8 +74,7 @@ public ConfigService(String sdkKey, } private void setInitialized() { - if (!initialized) { - initialized = true; + if (initialized.compareAndSet(false, true)) { configCatHooks.invokeOnClientReady(); } } @@ -107,7 +105,7 @@ public CompletableFuture getSettings() { ? new SettingResult(entryResult.value().getConfig().getEntries(), entryResult.value().getFetchTime()) : SettingResult.EMPTY); } else { - return fetchIfOlder(Constants.DISTANT_PAST, true) + return fetchIfOlder(Constants.DISTANT_PAST, initialized.get()) // If we are initialized, we prefer the cached results .thenApply(entryResult -> !entryResult.value().isEmpty() ? new SettingResult(entryResult.value().getConfig().getEntries(), entryResult.value().getFetchTime()) : SettingResult.EMPTY); @@ -115,35 +113,26 @@ public CompletableFuture getSettings() { } - private CompletableFuture> fetchIfOlder(long time, boolean preferCached) { + private CompletableFuture> fetchIfOlder(long threshold, boolean preferCached) { lock.lock(); try { // Sync up with the cache and use it when it's not expired. - if (cachedEntry.isEmpty() || cachedEntry.getFetchTime() > time) { - Entry fromCache = readCache(); - if (!fromCache.isEmpty() && !fromCache.getETag().equals(cachedEntry.getETag())) { - configCatHooks.invokeOnConfigChanged(fromCache.getConfig().getEntries()); - cachedEntry = fromCache; - } - // Cache isn't expired - if (cachedEntry.getFetchTime() > time) { - setInitialized(); - return CompletableFuture.completedFuture(Result.success(cachedEntry)); - } + Entry fromCache = readCache(); + if (!fromCache.isEmpty() && !fromCache.getETag().equals(cachedEntry.getETag())) { + configCatHooks.invokeOnConfigChanged(fromCache.getConfig().getEntries()); + cachedEntry = fromCache; } - // Use cache anyway (get calls on auto & manual poll must not initiate fetch). - // The initialized check ensures that we subscribe for the ongoing fetch during the - // max init wait time window in case of auto poll. - if (preferCached && initialized) { + // Cache isn't expired + if (cachedEntry.getFetchTime() > threshold) { + setInitialized(); return CompletableFuture.completedFuture(Result.success(cachedEntry)); } - // If we are in offline mode we are not allowed to initiate fetch. - if (offline.get()) { + // If we are in offline mode or the caller prefers cached values, do not initiate fetch. + if (offline.get() || preferCached) { return CompletableFuture.completedFuture(Result.success(cachedEntry)); } - if (runningTask == null) { - // No fetch is running, initiate a new one. + if (runningTask == null) { // No fetch is running, initiate a new one. runningTask = new CompletableFuture<>(); configFetcher.fetchAsync(cachedEntry.getETag()) .thenAccept(this::processResponse); diff --git a/src/main/java/com/configcat/Constants.java b/src/main/java/com/configcat/Constants.java index 7a44fe3..ad55013 100644 --- a/src/main/java/com/configcat/Constants.java +++ b/src/main/java/com/configcat/Constants.java @@ -8,5 +8,5 @@ private Constants() { /* prevent from instantiation*/ } static final String CONFIG_JSON_NAME = "config_v5.json"; static final String SERIALIZATION_FORMAT_VERSION = "v2"; - static final String VERSION = "8.3.0"; + static final String VERSION = "8.4.0"; } \ No newline at end of file diff --git a/src/test/java/com/configcat/AutoPollingPolicyTest.java b/src/test/java/com/configcat/AutoPollingTest.java similarity index 99% rename from src/test/java/com/configcat/AutoPollingPolicyTest.java rename to src/test/java/com/configcat/AutoPollingTest.java index 9fc3c44..27e2675 100644 --- a/src/test/java/com/configcat/AutoPollingPolicyTest.java +++ b/src/test/java/com/configcat/AutoPollingTest.java @@ -17,9 +17,9 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -public class AutoPollingPolicyTest { +public class AutoPollingTest { private MockWebServer server; - private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(AutoPollingPolicyTest.class), LogLevel.WARNING); + private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(AutoPollingTest.class), LogLevel.WARNING); private static final String TEST_JSON = "{ f: { fakeKey: { v: %s, p: [] ,r: [] } } }"; @BeforeEach diff --git a/src/test/java/com/configcat/Helpers.java b/src/test/java/com/configcat/Helpers.java index 26997b0..ec2ffd6 100644 --- a/src/test/java/com/configcat/Helpers.java +++ b/src/test/java/com/configcat/Helpers.java @@ -10,6 +10,12 @@ static String cacheValueFromConfigJson(String json) { return entry.serialize(); } + static String cacheValueFromConfigJsonWithEtag(String json, String etag) { + Config config = Utils.gson.fromJson(json, Config.class); + Entry entry = new Entry(config, etag, json, System.currentTimeMillis()); + return entry.serialize(); + } + static void waitFor(Supplier predicate) throws InterruptedException { waitFor(3000, predicate); } diff --git a/src/test/java/com/configcat/LazyLoadingPolicyTest.java b/src/test/java/com/configcat/LazyLoadingTest.java similarity index 87% rename from src/test/java/com/configcat/LazyLoadingPolicyTest.java rename to src/test/java/com/configcat/LazyLoadingTest.java index 9f7c5ab..b4f7091 100644 --- a/src/test/java/com/configcat/LazyLoadingPolicyTest.java +++ b/src/test/java/com/configcat/LazyLoadingTest.java @@ -14,10 +14,10 @@ import static org.junit.jupiter.api.Assertions.*; -public class LazyLoadingPolicyTest { +public class LazyLoadingTest { private ConfigService configService; private MockWebServer server; - private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(LazyLoadingPolicyTest.class)); + private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(LazyLoadingTest.class)); private static final String TEST_JSON = "{ f: { fakeKey: { v: %s, p: [] ,r: [] } } }"; @BeforeEach @@ -146,6 +146,27 @@ void testCacheExpirationRespectedInTTLCalc304() throws InterruptedException, Exe assertEquals(1, this.server.getRequestCount()); } + @Test + void testCacheTTLRespectsExternalCache() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(200).setBody(String.format(TEST_JSON, "test-remote"))); + + ConfigCache cache = new SingleValueCache(Helpers.cacheValueFromConfigJsonWithEtag(String.format(TEST_JSON, "test-local"), "etag")); + + PollingMode mode = PollingModes + .lazyLoad(1); + ConfigFetcher fetcher = new ConfigFetcher(new OkHttpClient.Builder().build(), logger, "", this.server.url("/").toString(), false, mode.getPollingIdentifier()); + ConfigService service = new ConfigService("", fetcher, mode, cache, logger, false, new ConfigCatHooks()); + + assertEquals("test-local", service.getSettings().get().settings().get("fakeKey").getValue().getAsString()); + assertEquals(0, this.server.getRequestCount()); + Thread.sleep(1000); + + cache.write("", Helpers.cacheValueFromConfigJsonWithEtag(String.format(TEST_JSON, "test-local2"), "etag2")); + assertEquals("test-local2", service.getSettings().get().settings().get("fakeKey").getValue().getAsString()); + + assertEquals(0, this.server.getRequestCount()); + } + @Test void testOnlineOffline() throws Exception { this.server.enqueue(new MockResponse().setResponseCode(200).setBody(String.format(TEST_JSON, "test"))); diff --git a/src/test/java/com/configcat/ManualPollingPolicyTest.java b/src/test/java/com/configcat/ManualPollingTest.java similarity index 98% rename from src/test/java/com/configcat/ManualPollingPolicyTest.java rename to src/test/java/com/configcat/ManualPollingTest.java index c09d6c4..0c02bf8 100644 --- a/src/test/java/com/configcat/ManualPollingPolicyTest.java +++ b/src/test/java/com/configcat/ManualPollingTest.java @@ -15,10 +15,10 @@ import static org.junit.jupiter.api.Assertions.*; -public class ManualPollingPolicyTest { +public class ManualPollingTest { private ConfigService configService; private MockWebServer server; - private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(ManualPollingPolicyTest.class)); + private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(ManualPollingTest.class)); private static final String TEST_JSON = "{ f: { fakeKey: { v: %s, p: [] ,r: [] } } }"; @BeforeEach