Skip to content

Commit 7f34bd9

Browse files
authored
consistent-config-caching (#14)
1 parent 65206be commit 7f34bd9

File tree

12 files changed

+167
-48
lines changed

12 files changed

+167
-48
lines changed

.github/workflows/cpp-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
CTEST_OUTPUT_ON_FAILURE: 1
2222

2323
steps:
24-
- uses: actions/checkout@v2
24+
- uses: actions/checkout@v3
2525
- name: Install dependencies on ubuntu
2626
if: startsWith(matrix.os, 'ubuntu')
2727
run: |
@@ -58,7 +58,7 @@ jobs:
5858
CXXFLAGS: "--coverage -fno-inline"
5959

6060
steps:
61-
- uses: actions/checkout@v2
61+
- uses: actions/checkout@v3
6262
- name: Install dependencies
6363
run: |
6464
git clone https://github.com/microsoft/vcpkg build/vcpkg

include/configcat/config.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,23 +184,27 @@ struct ConfigEntry {
184184
static constexpr char kConfig[] = "config";
185185
static constexpr char kETag[] = "etag";
186186
static constexpr char kFetchTime[] = "fetch_time";
187+
static constexpr char kSerializationFormatVersion[] = "v2";
187188

188189
static inline std::shared_ptr<ConfigEntry> empty = std::make_shared<ConfigEntry>(Config::empty, "empty");
189190

190191
ConfigEntry(std::shared_ptr<Config> config = Config::empty,
191192
const std::string& eTag = "",
193+
const std::string& configJsonString = "{}",
192194
double fetchTime = kDistantPast):
193195
config(config),
194196
eTag(eTag),
197+
configJsonString(configJsonString),
195198
fetchTime(fetchTime) {
196199
}
197200
ConfigEntry(const ConfigEntry&) = delete; // Disable copy
198201

199-
static std::shared_ptr<ConfigEntry> fromJson(const std::string& jsonString);
200-
std::string toJson() const;
202+
static std::shared_ptr<ConfigEntry> fromString(const std::string& text);
203+
std::string serialize() const;
201204

202205
std::shared_ptr<Config> config;
203206
std::string eTag;
207+
std::string configJsonString;
204208
double fetchTime;
205209
};
206210

src/config.cpp

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "configcat/config.h"
22
#include <nlohmann/json.hpp>
33
#include <fstream>
4+
#include <cmath>
45

56
using namespace std;
67
using json = nlohmann::json;
@@ -165,20 +166,39 @@ shared_ptr<Config> Config::fromFile(const string& filePath) {
165166
return config;
166167
}
167168

168-
shared_ptr<ConfigEntry> ConfigEntry::fromJson(const std::string& jsonString) {
169-
json configEntryObj = json::parse(jsonString);
170-
auto config = make_shared<Config>();
171-
auto configObj = configEntryObj.at(kConfig);
172-
configObj.get_to(*config);
173-
return make_shared<ConfigEntry>(config, configEntryObj.value(kETag, ""), configEntryObj.value(kFetchTime, kDistantPast));
169+
shared_ptr<ConfigEntry> ConfigEntry::fromString(const string& text) {
170+
if (text.empty())
171+
return ConfigEntry::empty;
172+
173+
auto fetchTimeIndex = text.find('\n');
174+
auto eTagIndex = text.find('\n', fetchTimeIndex + 1);
175+
if (fetchTimeIndex == string::npos || eTagIndex == string::npos) {
176+
throw std::invalid_argument("Number of values is fewer than expected.");
177+
}
178+
179+
auto fetchTimeString = text.substr(0, fetchTimeIndex);
180+
double fetchTime;
181+
try {
182+
fetchTime = std::stod(fetchTimeString);
183+
} catch (const std::exception& e) {
184+
throw std::invalid_argument("Invalid fetch time: " + fetchTimeString + ". " + e.what());
185+
}
186+
187+
auto eTag = text.substr(fetchTimeIndex + 1, eTagIndex - fetchTimeIndex - 1);
188+
if (eTag.empty()) {
189+
throw std::invalid_argument("Empty eTag value");
190+
}
191+
192+
auto configJsonString = text.substr(eTagIndex + 1);
193+
try {
194+
return make_shared<ConfigEntry>(Config::fromJson(configJsonString), eTag, configJsonString, fetchTime / 1000.0);
195+
} catch (const std::exception& e) {
196+
throw std::invalid_argument("Invalid config JSON: " + configJsonString + ". " + e.what());
197+
}
174198
}
175199

176-
string ConfigEntry::toJson() const {
177-
return "{"s +
178-
'"' + kConfig + '"' + ":" + (config ? config->toJson() : "{}") +
179-
"," + '"' + kETag + '"' + ":" + '"' + eTag + '"' +
180-
"," + '"' + kFetchTime + '"' + ":" + to_string(fetchTime) +
181-
"}";
200+
string ConfigEntry::serialize() const {
201+
return to_string(static_cast<uint64_t>(floor(fetchTime * 1000))) + "\n" + eTag + "\n" + configJsonString;
182202
}
183203

184204
} // namespace configcat

src/configfetcher.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) {
178178
try {
179179
auto config = Config::fromJson(response.text);
180180
LOG_DEBUG << "Fetch was successful: new config fetched.";
181-
return FetchResponse(fetched, make_shared<ConfigEntry>(config, eTag, getUtcNowSecondsSinceEpoch()));
181+
return FetchResponse(fetched, make_shared<ConfigEntry>(config, eTag, response.text, getUtcNowSecondsSinceEpoch()));
182182
} catch (exception& exception) {
183183
LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1105);
184184
logEntry <<

src/configservice.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ConfigService::ConfigService(const string& sdkKey,
2121
pollingMode(options.pollingMode ? options.pollingMode : PollingMode::autoPoll()),
2222
cachedEntry(ConfigEntry::empty),
2323
configCache(configCache) {
24-
cacheKey = SHA1()(string("cpp_") + ConfigFetcher::kConfigJsonName + "_" + sdkKey);
24+
cacheKey = generateCacheKey(sdkKey);
2525
configFetcher = make_unique<ConfigFetcher>(sdkKey, logger, pollingMode->getPollingIdentifier(), options);
2626
offline = options.offline;
2727
startTime = chrono::steady_clock::now();
@@ -110,6 +110,9 @@ void ConfigService::setOffline() {
110110
LOG_INFO(5200) << "Switched to OFFLINE mode.";
111111
}
112112

113+
string ConfigService::generateCacheKey(const string& sdkKey) {
114+
return SHA1()(sdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion);
115+
}
113116
tuple<shared_ptr<ConfigEntry>, string> ConfigService::fetchIfOlder(double time, bool preferCache) {
114117
{
115118
lock_guard<mutex> lock(fetchMutex);
@@ -189,7 +192,7 @@ shared_ptr<ConfigEntry> ConfigService::readCache() {
189192
}
190193

191194
cachedEntryString = jsonString;
192-
return ConfigEntry::fromJson(jsonString);
195+
return ConfigEntry::fromString(jsonString);
193196
} catch (exception& exception) {
194197
LOG_ERROR(2200) << "Error occurred while reading the cache. " << exception.what();
195198
return ConfigEntry::empty;
@@ -198,7 +201,7 @@ shared_ptr<ConfigEntry> ConfigService::readCache() {
198201

199202
void ConfigService::writeCache(const std::shared_ptr<ConfigEntry>& configEntry) {
200203
try {
201-
configCache->write(cacheKey, configEntry->toJson());
204+
configCache->write(cacheKey, configEntry->serialize());
202205
} catch (exception& exception) {
203206
LOG_ERROR(2201) << "Error occurred while writing the cache. " << exception.what();
204207
}

src/configservice.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class ConfigService {
3737
void setOffline();
3838
bool isOffline() { return offline; }
3939

40+
static std::string generateCacheKey(const std::string& sdkKey);
41+
4042
private:
4143
// Returns the ConfigEntry object and error message in case of any error.
4244
std::tuple<std::shared_ptr<ConfigEntry>, std::string> fetchIfOlder(double time, bool preferCache = false);

src/version.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#pragma once
22

3-
#define CONFIGCAT_VERSION "2.0.1"
3+
#define CONFIGCAT_VERSION "3.0.0"

test/test-autopolling.cpp

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,13 @@ TEST_F(AutoPollingTest, Cache) {
174174
}
175175

176176
TEST_F(AutoPollingTest, ReturnCachedConfigWhenCacheIsNotExpired) {
177-
auto mockCache = make_shared<SingleValueCache>(R"({"config":)"s
178-
+ string_format(kTestJsonFormat, R"("test")") + R"(,"etag":"test-etag")"
179-
+ R"(,"fetch_time":)" + to_string(getUtcNowSecondsSinceEpoch()) + "}");
177+
auto jsonString = string_format(kTestJsonFormat, R"("test")");
178+
auto mockCache = make_shared<SingleValueCache>(ConfigEntry(
179+
Config::fromJson(jsonString),
180+
"test-etag",
181+
jsonString,
182+
getUtcNowSecondsSinceEpoch()).serialize()
183+
);
180184

181185
configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")};
182186
mockHttpSessionAdapter->enqueueResponse(firstResponse);
@@ -212,9 +216,13 @@ TEST_F(AutoPollingTest, ReturnCachedConfigWhenCacheIsNotExpired) {
212216
TEST_F(AutoPollingTest, FetchConfigWhenCacheIsExpired) {
213217
auto pollIntervalSeconds = 2;
214218
auto maxInitWaitTimeSeconds = 1;
215-
auto mockCache = make_shared<SingleValueCache>(R"({"config":)"s
216-
+ string_format(kTestJsonFormat, R"("test")") + R"(,"etag":"test-etag")"
217-
+ R"(,"fetch_time":)" + to_string(getUtcNowSecondsSinceEpoch() - pollIntervalSeconds) + "}");
219+
auto jsonString = string_format(kTestJsonFormat, R"("test")");
220+
auto mockCache = make_shared<SingleValueCache>(ConfigEntry(
221+
Config::fromJson(jsonString),
222+
"test-etag",
223+
jsonString,
224+
getUtcNowSecondsSinceEpoch() - pollIntervalSeconds).serialize()
225+
);
218226

219227
configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")};
220228
mockHttpSessionAdapter->enqueueResponse(firstResponse);
@@ -232,9 +240,13 @@ TEST_F(AutoPollingTest, FetchConfigWhenCacheIsExpired) {
232240
TEST_F(AutoPollingTest, initWaitTimeReturnCached) {
233241
auto pollIntervalSeconds = 60;
234242
auto maxInitWaitTimeSeconds = 1;
235-
auto mockCache = make_shared<SingleValueCache>(R"({"config":)"s
236-
+ string_format(kTestJsonFormat, R"("test")") + R"(,"etag":"test-etag")"
237-
+ R"(,"fetch_time":)" + to_string(getUtcNowSecondsSinceEpoch() - 2 * pollIntervalSeconds) + "}");
243+
auto jsonString = string_format(kTestJsonFormat, R"("test")");
244+
auto mockCache = make_shared<SingleValueCache>(ConfigEntry(
245+
Config::fromJson(jsonString),
246+
"test-etag",
247+
jsonString,
248+
getUtcNowSecondsSinceEpoch() - 2 * pollIntervalSeconds).serialize()
249+
);
238250

239251
configcat::Response response = {200, string_format(kTestJsonFormat, R"("test2")")};
240252
constexpr int responseDelay = 5;

test/test-configcache.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#include <gtest/gtest.h>
2+
#include "mock.h"
3+
#include "utils.h"
4+
#include "configservice.h"
5+
#include "configcat/configcatoptions.h"
6+
#include "configcat/configcatclient.h"
7+
8+
9+
using namespace configcat;
10+
using namespace std;
11+
12+
TEST(ConfigCacheTest, CacheKey) {
13+
EXPECT_EQ("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", ConfigService::generateCacheKey("test1"));
14+
EXPECT_EQ("c09513b1756de9e4bc48815ec7a142b2441ed4d5", ConfigService::generateCacheKey("test2"));
15+
}
16+
17+
TEST(ConfigCacheTest, CachePayload) {
18+
double nowInSeconds = 1686756435.8449;
19+
std::string etag = "test-etag";
20+
ConfigEntry entry(Config::fromJson(kTestJsonString), etag, kTestJsonString, nowInSeconds);
21+
EXPECT_EQ("1686756435844\n" + etag + "\n" + kTestJsonString, entry.serialize());
22+
}
23+
24+
TEST(ConfigCatTest, InvalidCacheContent) {
25+
static constexpr char kTestJsonFormat[] = R"({ "f": { "testKey": { "v": %s, "p": [], "r": [] } } })";
26+
HookCallbacks hookCallbacks;
27+
auto hooks = make_shared<Hooks>();
28+
hooks->addOnError([&](const string& error) { hookCallbacks.onError(error); });
29+
auto configJsonString = string_format(kTestJsonFormat, R"("test")");
30+
auto configCache = make_shared<SingleValueCache>(ConfigEntry(
31+
Config::fromJson(configJsonString),
32+
"test-etag",
33+
configJsonString,
34+
getUtcNowSecondsSinceEpoch()).serialize()
35+
);
36+
37+
ConfigCatOptions options;
38+
options.pollingMode = PollingMode::manualPoll();
39+
options.configCache = configCache;
40+
options.hooks = hooks;
41+
auto client = ConfigCatClient::get("test", &options);
42+
43+
EXPECT_EQ("test", client->getValue("testKey", "default"));
44+
EXPECT_EQ(0, hookCallbacks.errorCallCount);
45+
46+
// Invalid fetch time in cache
47+
configCache->value = "text\n"s + "test-etag\n" + string_format(kTestJsonFormat, R"("test2")");
48+
EXPECT_EQ("test", client->getValue("testKey", "default"));
49+
EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Invalid fetch time: text") != std::string::npos);
50+
51+
// Number of values is fewer than expected
52+
configCache->value = std::to_string(getUtcNowSecondsSinceEpoch()) + "\n" + string_format(kTestJsonFormat, R"("test2")");
53+
EXPECT_EQ("test", client->getValue("testKey", "default"));
54+
EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Number of values is fewer than expected.") != std::string::npos);
55+
56+
// Invalid config JSON
57+
configCache->value = std::to_string(getUtcNowSecondsSinceEpoch()) + "\n" + "test-etag\n" + "wrong-json";
58+
EXPECT_EQ("test", client->getValue("testKey", "default"));
59+
EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Invalid config JSON: wrong-json.") != std::string::npos);
60+
61+
ConfigCatClient::close(client);
62+
}

test/test-configcatclient.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,11 @@ TEST_F(ConfigCatClientTest, FailingAutoPoll) {
250250

251251
TEST_F(ConfigCatClientTest, FromCacheOnly) {
252252
auto mockCache = make_shared<InMemoryConfigCache>();
253-
auto cacheKey = SHA1()(string("cpp_") + ConfigFetcher::kConfigJsonName + "_" + kTestSdkKey);
254-
auto config = Config::fromJson(string_format(kTestJsonFormat, R"("fake")"));
255-
auto configEntry = ConfigEntry(config);
256-
mockCache->write(cacheKey, configEntry.toJson());
253+
auto cacheKey = SHA1()(""s + kTestSdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion);
254+
auto jsonString = string_format(kTestJsonFormat, R"("fake")");
255+
auto config = Config::fromJson(jsonString);
256+
auto configEntry = ConfigEntry(config, "test-etag", jsonString, getUtcNowSecondsSinceEpoch());
257+
mockCache->write(cacheKey, configEntry.serialize());
257258
mockHttpSessionAdapter->enqueueResponse({500, ""});
258259

259260
ConfigCatOptions options;
@@ -268,10 +269,11 @@ TEST_F(ConfigCatClientTest, FromCacheOnly) {
268269

269270
TEST_F(ConfigCatClientTest, FromCacheOnlyRefresh) {
270271
auto mockCache = make_shared<InMemoryConfigCache>();
271-
auto cacheKey = SHA1()(string("cpp_") + ConfigFetcher::kConfigJsonName + "_" + kTestSdkKey);
272-
auto config = Config::fromJson(string_format(kTestJsonFormat, R"("fake")"));
273-
auto configEntry = ConfigEntry(config);
274-
mockCache->write(cacheKey, configEntry.toJson());
272+
auto cacheKey = SHA1()(""s + kTestSdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion);
273+
auto jsonString = string_format(kTestJsonFormat, R"("fake")");
274+
auto config = Config::fromJson(jsonString);
275+
auto configEntry = ConfigEntry(config, "test-etag", jsonString, getUtcNowSecondsSinceEpoch());
276+
mockCache->write(cacheKey, configEntry.serialize());
275277
mockHttpSessionAdapter->enqueueResponse({500, ""});
276278

277279
ConfigCatOptions options;

test/test-hooks.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
#include "configcat/configcatclient.h"
66
#include "configcat/configcatoptions.h"
77
#include "configcat/configcatlogger.h"
8-
#include "configcat/consolelogger.h"
9-
#include <thread>
108
#include <chrono>
119

1210
using namespace configcat;
@@ -32,7 +30,11 @@ TEST_F(HooksTest, Init) {
3230

3331
ConfigCatOptions options;
3432
options.pollingMode = PollingMode::manualPoll();
35-
options.configCache = make_shared<SingleValueCache>(R"({"config":)"s + kTestJsonString + R"(,"etag":"test-etag"})");
33+
options.configCache = make_shared<SingleValueCache>(ConfigEntry(
34+
Config::fromJson(kTestJsonString),
35+
"test-etag",
36+
kTestJsonString).serialize()
37+
);
3638
options.hooks = hooks;
3739
auto client = ConfigCatClient::get("test", &options);
3840

@@ -70,7 +72,11 @@ TEST_F(HooksTest, Subscribe) {
7072

7173
ConfigCatOptions options;
7274
options.pollingMode = PollingMode::manualPoll();
73-
options.configCache = make_shared<SingleValueCache>(R"({"config":)"s + kTestJsonString + R"(,"etag":"test-etag"})");
75+
options.configCache = make_shared<SingleValueCache>(ConfigEntry(
76+
Config::fromJson(kTestJsonString),
77+
"test-etag",
78+
kTestJsonString).serialize()
79+
);
7480
options.hooks = hooks;
7581
auto client = ConfigCatClient::get("test", &options);
7682

test/test-lazyloading.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ TEST_F(LazyLoadingTest, Cache) {
105105
}
106106

107107
TEST_F(LazyLoadingTest, ReturnCachedConfigWhenCacheIsNotExpired) {
108-
auto mockCache = make_shared<SingleValueCache>(R"({"config":)"s
109-
+ string_format(kTestJsonFormat, R"("test")") + R"(,"etag":"test-etag")"
110-
+ R"(,"fetch_time":)" + to_string(getUtcNowSecondsSinceEpoch()) + "}");
108+
auto jsonString = string_format(kTestJsonFormat, R"("test")");
109+
auto mockCache = make_shared<SingleValueCache>(ConfigEntry(
110+
Config::fromJson(jsonString),
111+
"test-etag",
112+
jsonString,
113+
getUtcNowSecondsSinceEpoch()).serialize()
114+
);
111115

112116
configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")};
113117
mockHttpSessionAdapter->enqueueResponse(firstResponse);
@@ -132,9 +136,13 @@ TEST_F(LazyLoadingTest, ReturnCachedConfigWhenCacheIsNotExpired) {
132136

133137
TEST_F(LazyLoadingTest, FetchConfigWhenCacheIsExpired) {
134138
auto cacheTimeToLiveSeconds = 1;
135-
auto mockCache = make_shared<SingleValueCache>(R"({"config":)"s
136-
+ string_format(kTestJsonFormat, R"("test")") + R"(,"etag":"test-etag")"
137-
+ R"(,"fetch_time":)" + to_string(getUtcNowSecondsSinceEpoch() - cacheTimeToLiveSeconds) + "}");
139+
auto jsonString = string_format(kTestJsonFormat, R"("test")");
140+
auto mockCache = make_shared<SingleValueCache>(ConfigEntry(
141+
Config::fromJson(jsonString),
142+
"test-etag",
143+
jsonString,
144+
getUtcNowSecondsSinceEpoch() - cacheTimeToLiveSeconds).serialize()
145+
);
138146

139147
configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")};
140148
mockHttpSessionAdapter->enqueueResponse(firstResponse);

0 commit comments

Comments
 (0)