Skip to content

Commit cb3d224

Browse files
Cache fetch time rework (#34)
* Cache fetchtime rework * fetchTime removed from FetchResponse.java * Change Entry fetchTime to serialize in milliseconds * Fix CONFIG_JSON_NAME and add extra serialization tests * Fix payloadSerializationPlatformIndependent expected value
1 parent 7b761c0 commit cb3d224

File tree

10 files changed

+82
-86
lines changed

10 files changed

+82
-86
lines changed

src/main/java/com/configcat/ConfigFetcher.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,46 +107,42 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) {
107107
}
108108
logger.error(logEventId, message, e);
109109
}
110-
future.complete(FetchResponse.failed(message, false, null));
110+
future.complete(FetchResponse.failed(message, false));
111111
}
112112

113113
@Override
114114
public void onResponse(@NotNull Call call, @NotNull Response response) {
115115
try (ResponseBody body = response.body()) {
116-
String fetchTime = response.headers().get("date");
117-
if (fetchTime == null || fetchTime.isEmpty() || DateTimeUtils.isValidDate(fetchTime)) {
118-
fetchTime = DateTimeUtils.format(System.currentTimeMillis());
119-
}
120116
if (response.isSuccessful() && body != null) {
121117
String content = body.string();
122118
String eTag = response.header("ETag");
123119
Result<Config> result = deserializeConfig(content);
124120
if (result.error() != null) {
125-
future.complete(FetchResponse.failed(result.error(), false, null));
121+
future.complete(FetchResponse.failed(result.error(), false));
126122
return;
127123
}
128124
logger.debug("Fetch was successful: new config fetched.");
129-
future.complete(FetchResponse.fetched(new Entry(result.value(), eTag, content, fetchTime), fetchTime));
125+
future.complete(FetchResponse.fetched(new Entry(result.value(), eTag, content, System.currentTimeMillis())));
130126
} else if (response.code() == 304) {
131127
logger.debug("Fetch was successful: config not modified.");
132-
future.complete(FetchResponse.notModified(fetchTime));
128+
future.complete(FetchResponse.notModified());
133129
} else if (response.code() == 403 || response.code() == 404) {
134130
String message = ConfigCatLogMessages.FETCH_FAILED_DUE_TO_INVALID_SDK_KEY_ERROR;
135131
logger.error(1100, message);
136-
future.complete(FetchResponse.failed(message, true, fetchTime));
132+
future.complete(FetchResponse.failed(message, true));
137133
} else {
138134
String message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedHttpResponse(response.code(), response.message());
139135
logger.error(1101, message);
140-
future.complete(FetchResponse.failed(message, false, null));
136+
future.complete(FetchResponse.failed(message, false));
141137
}
142138
} catch (SocketTimeoutException e) {
143139
String message = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpClient.connectTimeoutMillis(), httpClient.readTimeoutMillis(), httpClient.writeTimeoutMillis());
144140
logger.error(1102, message, e);
145-
future.complete(FetchResponse.failed(message, false, null));
141+
future.complete(FetchResponse.failed(message, false));
146142
} catch (Exception e) {
147143
String message = ConfigCatLogMessages.FETCH_FAILED_DUE_TO_UNEXPECTED_ERROR;
148144
logger.error(1103, message, e);
149-
future.complete(FetchResponse.failed(message, false, null));
145+
future.complete(FetchResponse.failed(message, false));
150146
}
151147
}
152148
});
@@ -170,7 +166,7 @@ public void close() throws IOException {
170166
}
171167

172168
Request getRequest(String etag) {
173-
String url = this.url + "/configuration-files/" + this.sdkKey + "/" + Constants.CONFIG_JSON_NAME + ".json";
169+
String url = this.url + "/configuration-files/" + this.sdkKey + "/" + Constants.CONFIG_JSON_NAME;
174170
Request.Builder builder = new Request.Builder()
175171
.addHeader("X-ConfigCat-UserAgent", "ConfigCat-Java/" + this.mode + "-" + Constants.VERSION);
176172

src/main/java/com/configcat/ConfigService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ private void processResponse(FetchResponse response) {
209209
completeRunningTask(Result.success(entry));
210210
} else {
211211
if (response.isFetchTimeUpdatable()) {
212-
cachedEntry = cachedEntry.withFetchTime(response.getFetchTime());
212+
cachedEntry = cachedEntry.withFetchTime(System.currentTimeMillis());
213213
writeCache(cachedEntry);
214214
}
215215
completeRunningTask(response.isFailed()

src/main/java/com/configcat/Constants.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ private Constants() { /* prevent from instantiation*/ }
55

66
static final long DISTANT_FUTURE = Long.MAX_VALUE;
77
static final long DISTANT_PAST = 0;
8-
static final String CONFIG_JSON_NAME = "config_v5";
9-
static final String SERIALIZATION_FORMAT_VERSION = "v1";
8+
static final String CONFIG_JSON_NAME = "config_v5.json";
9+
static final String SERIALIZATION_FORMAT_VERSION = "v2";
1010

1111
static final String VERSION = "8.1.0";
1212
}
Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,15 @@
11
package com.configcat;
22

3-
import java.time.Instant;
4-
import java.time.ZoneId;
5-
import java.time.format.DateTimeFormatter;
6-
import java.time.format.DateTimeParseException;
7-
import java.time.temporal.ChronoUnit;
8-
import java.util.Locale;
9-
103
public class DateTimeUtils {
114

125
private DateTimeUtils() { /* prevent from instantiation*/ }
136

14-
/**
15-
* HTTP Date header formatter. Date: day-name, day month year hour:minute:second GMT
16-
*
17-
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date">mdn docs</a>
18-
*/
19-
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH).withZone(ZoneId.of("GMT"));
20-
217
public static boolean isValidDate(String date) {
228
try {
23-
DATE_TIME_FORMATTER.parse(date);
24-
} catch (DateTimeParseException e) {
9+
Long.parseLong(date);
10+
} catch (NumberFormatException e) {
2511
return false;
2612
}
2713
return true;
2814
}
29-
30-
public static long parseToMillis(String dateTime) {
31-
return Instant.EPOCH.until(Instant.from(DateTimeUtils.DATE_TIME_FORMATTER.parse(dateTime)), ChronoUnit.MILLIS);
32-
33-
}
34-
35-
public static String format(long timeInMilliseconds) {
36-
return DateTimeUtils.DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(timeInMilliseconds));
37-
}
3815
}
39-

src/main/java/com/configcat/Entry.java

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ public class Entry {
44
private final Config config;
55
private final String eTag;
66
private final String configJson;
7-
private final String fetchTimeRaw;
7+
private final long fetchTime;
88

99
public Config getConfig() {
1010
return config;
@@ -15,36 +15,32 @@ public String getETag() {
1515
}
1616

1717
public long getFetchTime() {
18-
return fetchTimeRaw == null || fetchTimeRaw.isEmpty() ? 0 : DateTimeUtils.parseToMillis(fetchTimeRaw);
18+
return fetchTime;
1919
}
2020

2121
public String getConfigJson() {
2222
return configJson;
2323
}
2424

25-
public String getFetchTimeRaw() {
26-
return fetchTimeRaw;
25+
public Entry withFetchTime(long fetchTime) {
26+
return new Entry(getConfig(), getETag(), getConfigJson(), fetchTime);
2727
}
2828

29-
public Entry withFetchTime(String fetchTimeRaw) {
30-
return new Entry(getConfig(), getETag(), getConfigJson(), fetchTimeRaw);
31-
}
32-
33-
public Entry(Config config, String eTag, String configJson, String fetchTimeRaw) {
29+
public Entry(Config config, String eTag, String configJson, long fetchTime) {
3430
this.config = config;
3531
this.eTag = eTag;
3632
this.configJson = configJson;
37-
this.fetchTimeRaw = fetchTimeRaw;
33+
this.fetchTime = fetchTime;
3834
}
3935

4036
boolean isEmpty() {
4137
return EMPTY.equals(this);
4238
}
4339

44-
public static final Entry EMPTY = new Entry(Config.EMPTY, "", "", null);
40+
public static final Entry EMPTY = new Entry(Config.EMPTY, "", "", Constants.DISTANT_PAST);
4541

4642
public String serialize() {
47-
return getFetchTimeRaw() + "\n" + getETag() + "\n" + getConfigJson();
43+
return getFetchTime() + "\n" + getETag() + "\n" + getConfigJson();
4844
}
4945

5046
public static Entry fromString(String cacheValue) throws IllegalArgumentException {
@@ -61,6 +57,8 @@ public static Entry fromString(String cacheValue) throws IllegalArgumentExceptio
6157
if (!DateTimeUtils.isValidDate(fetchTimeRaw)) {
6258
throw new IllegalArgumentException("Invalid fetch time: " + fetchTimeRaw);
6359
}
60+
long fetchTimeUnixMillis = Long.parseLong(fetchTimeRaw);
61+
6462

6563
String eTag = cacheValue.substring(fetchTimeIndex + 1, eTagIndex);
6664
if (eTag.isEmpty()) {
@@ -72,7 +70,7 @@ public static Entry fromString(String cacheValue) throws IllegalArgumentExceptio
7270
}
7371
try {
7472
Config config = Utils.gson.fromJson(configJson, Config.class);
75-
return new Entry(config, eTag, configJson, fetchTimeRaw);
73+
return new Entry(config, eTag, configJson, fetchTimeUnixMillis);
7674
} catch (Exception e) {
7775
throw new IllegalArgumentException("Invalid config JSON content: " + configJson);
7876
}

src/main/java/com/configcat/FetchResponse.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ public enum Status {
1212
private final String error;
1313
private final boolean fetchTimeUpdatable;
1414

15-
private final String fetchTime;
16-
1715
public boolean isFetched() {
1816
return this.status == Status.FETCHED;
1917
}
@@ -38,27 +36,22 @@ public String error() {
3836
return this.error;
3937
}
4038

41-
public String getFetchTime() {
42-
return this.fetchTime;
43-
}
44-
45-
FetchResponse(Status status, Entry entry, String error, boolean fetchTimeUpdatable, String fetchTime) {
39+
FetchResponse(Status status, Entry entry, String error, boolean fetchTimeUpdatable) {
4640
this.status = status;
4741
this.entry = entry;
4842
this.error = error;
4943
this.fetchTimeUpdatable = fetchTimeUpdatable;
50-
this.fetchTime = fetchTime;
5144
}
5245

53-
public static FetchResponse fetched(Entry entry, String fetchTime) {
54-
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false, fetchTime);
46+
public static FetchResponse fetched(Entry entry) {
47+
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false);
5548
}
5649

57-
public static FetchResponse notModified(String fetchTime) {
58-
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true, fetchTime);
50+
public static FetchResponse notModified() {
51+
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true);
5952
}
6053

61-
public static FetchResponse failed(String error, boolean fetchTimeUpdatable, String fetchTime) {
62-
return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable, fetchTime);
54+
public static FetchResponse failed(String error, boolean fetchTimeUpdatable) {
55+
return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable);
6356
}
6457
}

src/test/java/com/configcat/ConfigCatClientTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.junit.jupiter.api.Test;
77

88
import java.io.IOException;
9+
import java.lang.reflect.Field;
910
import java.time.Duration;
1011
import java.time.Instant;
1112
import java.util.List;
@@ -749,5 +750,30 @@ void testOnFlagEvaluationError() throws IOException {
749750
cl.close();
750751
}
751752

753+
@Test
754+
void testCacheKey() throws NoSuchFieldException, IllegalAccessException {
755+
//Test Data: SDKKey "test1", HASH "147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6"
756+
ConfigCatClient clTest1 = ConfigCatClient.get("test1");
757+
758+
String test1SdkKeyCacheKeyWithReflection = getCacheKeyWithReflection(clTest1);
759+
assertEquals("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", test1SdkKeyCacheKeyWithReflection);
760+
761+
//Test Data: SDKKey "test2", HASH "c09513b1756de9e4bc48815ec7a142b2441ed4d5"
762+
ConfigCatClient clTest2 = ConfigCatClient.get("test2");
763+
764+
String test2SdkKeyCacheKeyWithReflection = getCacheKeyWithReflection(clTest2);
765+
assertEquals("c09513b1756de9e4bc48815ec7a142b2441ed4d5", test2SdkKeyCacheKeyWithReflection);
766+
}
752767

768+
private static String getCacheKeyWithReflection(ConfigCatClient cl) throws NoSuchFieldException, IllegalAccessException {
769+
Field configServiceField = ConfigCatClient.class.getDeclaredField("configService");
770+
configServiceField.setAccessible(true);
771+
772+
ConfigService configService = (ConfigService) configServiceField.get(cl);
773+
774+
Field cacheKeyField = ConfigService.class.getDeclaredField("cacheKey");
775+
cacheKeyField.setAccessible(true);
776+
777+
return (String) cacheKeyField.get(configService);
778+
}
753779
}

src/test/java/com/configcat/ConfigFetcherTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public void fetchedETagNotUpdatesCache() throws Exception {
8787

8888
Gson gson = new GsonBuilder().create();
8989
Config config = gson.fromJson(TEST_JSON, Config.class);
90-
Entry entry = new Entry(config, "fakeETag", TEST_JSON, "");
90+
Entry entry = new Entry(config, "fakeETag", TEST_JSON, Constants.DISTANT_PAST);
9191

9292
ConfigCache cache = mock(ConfigCache.class);
9393
when(cache.read(anyString())).thenReturn(gson.toJson(entry));
@@ -113,7 +113,7 @@ public void fetchedSameResponseNotUpdatesCache() throws Exception {
113113

114114
Gson gson = new GsonBuilder().create();
115115
Config config = gson.fromJson(TEST_JSON, Config.class);
116-
Entry entry = new Entry(config, "fakeETag", TEST_JSON, "");
116+
Entry entry = new Entry(config, "fakeETag", TEST_JSON, Constants.DISTANT_PAST);
117117

118118
ConfigCache cache = mock(ConfigCache.class);
119119
when(cache.read(anyString())).thenReturn(gson.toJson(entry));

src/test/java/com/configcat/EntrySerializationTest.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,41 @@ public class EntrySerializationTest {
1414
void serialize() {
1515
String json = String.format(TEST_JSON, "test", "1");
1616
Config config = Utils.gson.fromJson(json, Config.class);
17-
String fetchTimeRaw = DateTimeUtils.format(System.currentTimeMillis());
18-
Entry entry = new Entry(config, "fakeTag", json, fetchTimeRaw);
17+
long fetchTime = System.currentTimeMillis();
18+
Entry entry = new Entry(config, "fakeTag", json, fetchTime);
1919

2020
String serializedString = entry.serialize();
2121

22-
assertEquals(String.format(SERIALIZED_DATA, fetchTimeRaw, "fakeTag", json), serializedString);
22+
assertEquals(String.format(SERIALIZED_DATA, fetchTime, "fakeTag", json), serializedString);
2323
}
2424

2525
@Test
26-
void deserialize() throws Exception {
26+
void payloadSerializationPlatformIndependent() {
27+
String payloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}";
28+
29+
Config config = Utils.gson.fromJson(payloadTestConfigJson, Config.class);
30+
Entry entry = new Entry(config, "test-etag", payloadTestConfigJson, 1686756435844L);
31+
String serializedString = entry.serialize();
32+
33+
assertEquals("1686756435844\ntest-etag\n" + payloadTestConfigJson, serializedString);
34+
}
35+
36+
@Test
37+
void deserialize() {
2738
String json = String.format(TEST_JSON, "test", "1");
2839
long currentTimeMillis = System.currentTimeMillis();
29-
String fetchTimeRaw = DateTimeUtils.format(currentTimeMillis);
3040

31-
Entry entry = Entry.fromString(String.format(SERIALIZED_DATA, fetchTimeRaw, "fakeTag", json));
41+
Entry entry = Entry.fromString(String.format(SERIALIZED_DATA, currentTimeMillis, "fakeTag", json));
3242

3343
assertNotNull(entry);
34-
assertEquals(fetchTimeRaw, entry.getFetchTimeRaw());
3544
assertEquals("fakeTag", entry.getETag());
3645
assertEquals(json, entry.getConfigJson());
3746
assertEquals(1, entry.getConfig().getEntries().size());
38-
assertEquals(Math.ceil(currentTimeMillis / 1000) * 1000, entry.getFetchTime());
47+
assertEquals(currentTimeMillis, entry.getFetchTime());
3948
}
4049

4150
@Test
42-
void deserializeMissingValue() throws Exception {
51+
void deserializeMissingValue() {
4352
Entry deserializeNull = Entry.fromString(null);
4453
assertTrue(deserializeNull.isEmpty());
4554
Entry deserializeEmpty = Entry.fromString("");
@@ -65,21 +74,19 @@ void deserializeInvalidDate() {
6574
@Test
6675
void deserializeInvalidETag() {
6776
long currentTimeMillis = System.currentTimeMillis();
68-
String fetchTimeRaw = DateTimeUtils.format(currentTimeMillis);
69-
Exception assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, fetchTimeRaw, "", "json")));
77+
Exception assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, currentTimeMillis / 1000, "", "json")));
7078

7179
assertEquals("Empty eTag value.", assertThrows.getMessage());
7280
}
7381

7482
@Test
7583
void deserializeInvalidJson() {
7684
long currentTimeMillis = System.currentTimeMillis();
77-
String fetchTimeRaw = DateTimeUtils.format(currentTimeMillis);
78-
Exception assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, fetchTimeRaw, "fakeTag", "")));
85+
Exception assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, currentTimeMillis / 1000, "fakeTag", "")));
7986

8087
assertEquals("Empty config jsom value.", assertThrows.getMessage());
8188

82-
assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, fetchTimeRaw, "fakeTag", "wrongjson")));
89+
assertThrows = assertThrows(Exception.class, () -> Entry.fromString(String.format(SERIALIZED_DATA, currentTimeMillis / 1000, "fakeTag", "wrongjson")));
8390

8491
assertEquals("Invalid config JSON content: wrongjson", assertThrows.getMessage());
8592

src/test/java/com/configcat/Helpers.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ final class Helpers {
66

77
static String cacheValueFromConfigJson(String json) {
88
Config config = Utils.gson.fromJson(json, Config.class);
9-
Entry entry = new Entry(config, "fakeTag", json, DateTimeUtils.format(System.currentTimeMillis()));
9+
Entry entry = new Entry(config, "fakeTag", json, System.currentTimeMillis());
1010
return entry.serialize();
1111
}
1212

0 commit comments

Comments
 (0)