Skip to content

Commit 613169d

Browse files
authored
Merge pull request #16 from adam-p/dedup-writes
Reduce datastore writes
2 parents d0f74c9 + c1e9331 commit 613169d

File tree

7 files changed

+163
-23
lines changed

7 files changed

+163
-23
lines changed

datastore.cpp

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ using namespace error;
3434

3535
static string FilePath(const string& file_root, const string& suffix);
3636
static Result<json> FileLoad(const string& file_path);
37-
static Error FileStore(int transaction_depth, const string& file_path, const json& json);
37+
static Error FileStore(const string& file_path, const json& json);
3838

3939
Datastore::Datastore()
4040
: initialized_(false), explicit_lock_(mutex_, std::defer_lock),
41-
transaction_depth_(0), json_(json::object()) {
41+
transaction_depth_(0), transaction_dirty_(false), json_(json::object()) {
4242
}
4343

4444
Error Datastore::Init(const string& file_root, const string& suffix) {
@@ -58,7 +58,8 @@ Error Datastore::Init(const string& file_root, const string& suffix) {
5858
Error Datastore::Reset(const string& file_path, json new_value) {
5959
SYNCHRONIZE(mutex_);
6060
transaction_depth_ = 0;
61-
if (auto err = FileStore(transaction_depth_, file_path, new_value)) {
61+
transaction_dirty_ = false;
62+
if (auto err = FileStore(file_path, new_value)) {
6263
return PassError(err);
6364
}
6465
json_ = new_value;
@@ -80,6 +81,7 @@ void Datastore::BeginTransaction() {
8081
SYNCHRONIZE(mutex_);
8182
// We got a local lock, so we know there's no transaction in progress in any other thread.
8283
if (transaction_depth_ == 0) {
84+
transaction_dirty_ = false;
8385
explicit_lock_.lock();
8486
}
8587
transaction_depth_++;
@@ -100,12 +102,18 @@ Error Datastore::EndTransaction(bool commit) {
100102
return nullerr;
101103
}
102104

103-
// We need to release the explicit lock on exit from ths function, no matter what.
105+
// We need to release the explicit lock on exit from this function, no matter what.
104106
// We will "adopt" the lock into this lock_guard to ensure the unlock happens when it goes out of scope.
105107
std::lock_guard<std::unique_lock<std::recursive_mutex>> lock_releaser(explicit_lock_, std::adopt_lock);
106108

109+
if (!transaction_dirty_) {
110+
// No actual substantive changes were made during this transaction, so we will avoid
111+
// writing to disk. Committing and rolling back are no-ops if there are no changes.
112+
return nullerr;
113+
}
114+
107115
if (commit) {
108-
return PassError(FileStore(transaction_depth_, file_path_, json_));
116+
return PassError(FileStore(file_path_, json_));
109117
}
110118

111119
// We're rolling back -- revert to what's on disk
@@ -126,8 +134,24 @@ error::Result<nlohmann::json> Datastore::Get() const {
126134
Error Datastore::Set(const json::json_pointer& p, json v) {
127135
SYNCHRONIZE(mutex_);
128136
MUST_BE_INITIALIZED;
137+
138+
// We will use the transaction mechanism to do the writing. It will also help prevent
139+
// changes to the stored value between the time we check it and the time we set it.
140+
BeginTransaction();
141+
142+
// Avoid modifying the datastore if the value is the same as what's already there.
143+
bool changed = true;
144+
try {
145+
changed = (json_.at(p) != v);
146+
}
147+
catch (json::out_of_range&) {
148+
// The key doesn't exist, so continue to set it.
149+
}
150+
129151
json_[p] = v;
130-
return PassError(FileStore(transaction_depth_, file_path_, json_));
152+
transaction_dirty_ = changed;
153+
154+
return PassError(EndTransaction(true));
131155
}
132156

133157
static string FilePath(const string& file_root, const string& suffix) {
@@ -171,7 +195,7 @@ static Result<json> FileLoad(const string& file_path) {
171195
if (!utils::FileExists(file_path)) {
172196
// Check that we can write here by trying to store.
173197
auto empty_object = json::object();
174-
if (auto err = FileStore(false, file_path, empty_object)) {
198+
if (auto err = FileStore(file_path, empty_object)) {
175199
return WrapError(err, "file doesn't exist and FileStore failed");
176200
}
177201

@@ -203,11 +227,7 @@ static Result<json> FileLoad(const string& file_path) {
203227
return json;
204228
}
205229

206-
static Error FileStore(int transaction_depth, const string& file_path, const json& json) {
207-
if (transaction_depth > 0) {
208-
return nullerr;
209-
}
210-
230+
static Error FileStore(const string& file_path, const json& json) {
211231
const auto temp_file_path = file_path + TEMP_EXT;
212232
const auto commit_file_path = file_path + COMMIT_EXT;
213233

datastore.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ class Datastore {
6666
/// EndTransaction is called. Transactions are re-enterable, but not nested.
6767
/// NOTE: Failing to call EndTransaction will result in undefined behaviour.
6868
void BeginTransaction();
69-
/// Ends an ongoing transaction writing. If commit is true, it writes the changes
70-
/// immediately; if false it discards the changes.
69+
/// Ends an ongoing transaction. If commit is true, it writes the changes immediately;
70+
/// if false it discards the changes.
7171
/// Committing or rolling back inner transactions does nothing. Any errors during
7272
/// inner transactions that require the outermost transaction to be rolled back must
7373
/// be handled by the caller.
@@ -121,6 +121,7 @@ class Datastore {
121121
mutable std::recursive_mutex mutex_;
122122
std::unique_lock<std::recursive_mutex> explicit_lock_;
123123
int transaction_depth_;
124+
bool transaction_dirty_;
124125

125126
std::string file_path_;
126127
json json_;

datastore_test.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <cstdlib>
2121
#include <ctime>
2222
#include <thread>
23+
#include <filesystem>
2324

2425
#include "gtest/gtest.h"
2526
#include "test_helpers.hpp"
@@ -511,6 +512,100 @@ TEST_F(TestDatastore, SetTypes)
511512
ASSERT_EQ(*gotInt, wantInt);
512513
}
513514

515+
TEST_F(TestDatastore, SetWriteDedup)
516+
{
517+
auto temp_dir = GetTempDir();
518+
auto ds_path = DatastoreFilepath(temp_dir, ds_suffix);
519+
520+
Datastore ds;
521+
auto err = ds.Init(temp_dir.c_str(), ds_suffix);
522+
ASSERT_FALSE(err);
523+
524+
auto k = "/k"_json_pointer;
525+
err = ds.Set(k, "a");
526+
ASSERT_FALSE(err);
527+
auto file_time1 = std::filesystem::last_write_time(ds_path);
528+
529+
// Try setting the same value again
530+
err = ds.Set(k, "a");
531+
ASSERT_FALSE(err);
532+
auto file_time2 = std::filesystem::last_write_time(ds_path);
533+
534+
// The file time should not have changed after setting the same value
535+
ASSERT_EQ(file_time1, file_time2);
536+
537+
// Change to a different value
538+
err = ds.Set(k, "b");
539+
ASSERT_FALSE(err);
540+
auto file_time3 = std::filesystem::last_write_time(ds_path);
541+
542+
// The file should have been updated, so its time should be newer
543+
ASSERT_GT(file_time3, file_time1);
544+
}
545+
546+
TEST_F(TestDatastore, TransactionWriteDedup)
547+
{
548+
auto temp_dir = GetTempDir();
549+
auto ds_path = DatastoreFilepath(temp_dir, ds_suffix);
550+
551+
Datastore ds;
552+
auto err = ds.Init(temp_dir.c_str(), ds_suffix);
553+
ASSERT_FALSE(err);
554+
555+
auto k1 = "/k1"_json_pointer;
556+
auto k2 = "/k2"_json_pointer;
557+
err = ds.Set(k1, "a");
558+
ASSERT_FALSE(err);
559+
err = ds.Set(k2, "a");
560+
ASSERT_FALSE(err);
561+
auto file_time1 = std::filesystem::last_write_time(ds_path);
562+
563+
ds.BeginTransaction();
564+
565+
// Try setting the same values again
566+
err = ds.Set(k1, "a");
567+
ASSERT_FALSE(err);
568+
err = ds.Set(k2, "a");
569+
ASSERT_FALSE(err);
570+
571+
err = ds.EndTransaction(true);
572+
ASSERT_FALSE(err);
573+
auto file_time2 = std::filesystem::last_write_time(ds_path);
574+
575+
// The file time should not have changed after setting the same values
576+
ASSERT_EQ(file_time1, file_time2);
577+
578+
ds.BeginTransaction();
579+
580+
// Change to a different value
581+
err = ds.Set(k1, "b");
582+
ASSERT_FALSE(err);
583+
err = ds.Set(k2, "b");
584+
ASSERT_FALSE(err);
585+
586+
err = ds.EndTransaction(true);
587+
ASSERT_FALSE(err);
588+
auto file_time3 = std::filesystem::last_write_time(ds_path);
589+
590+
// The file should have been updated, so its time should be newer
591+
ASSERT_GT(file_time3, file_time1);
592+
593+
ds.BeginTransaction();
594+
595+
// Changing and then rolling back should still work as expected
596+
err = ds.Set(k1, "c");
597+
ASSERT_FALSE(err);
598+
err = ds.Set(k2, "c");
599+
ASSERT_FALSE(err);
600+
601+
err = ds.EndTransaction(false);
602+
ASSERT_FALSE(err);
603+
auto file_time4 = std::filesystem::last_write_time(ds_path);
604+
605+
// Should not have changed
606+
ASSERT_EQ(file_time3, file_time4);
607+
}
608+
514609
TEST_F(TestDatastore, TypeMismatch)
515610
{
516611
Datastore ds;

psicash.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,17 @@ void PsiCash::SetHTTPRequestFn(MakeHTTPRequestFn make_http_request_fn) {
161161
make_http_request_fn_ = std::move(make_http_request_fn);
162162
}
163163

164-
Error PsiCash::SetRequestMetadataItem(const string& key, const string& value) {
164+
Error PsiCash::SetRequestMetadataItems(const std::map<std::string, std::string>& items) {
165165
MUST_BE_INITIALIZED;
166-
return PassError(user_data_->SetRequestMetadataItem(key, value));
166+
UserData::Transaction transaction(*user_data_);
167+
for (const auto& it : items) {
168+
// Errors won't manifest until we commit
169+
(void)user_data_->SetRequestMetadataItem(it.first, it.second);
170+
}
171+
if (auto err = transaction.Commit()) {
172+
return WrapError(err, "user data write failed");
173+
}
174+
return nullerr;
167175
}
168176

169177
Error PsiCash::SetLocale(const string& locale) {

psicash.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ class PsiCash {
194194

195195
/// Set values that will be included in the request metadata. This includes
196196
/// client_version, client_region, sponsor_id, and propagation_channel_id.
197-
error::Error SetRequestMetadataItem(const std::string& key, const std::string& value);
197+
error::Error SetRequestMetadataItems(const std::map<std::string, std::string>& items);
198198

199199
/// Set current UI locale.
200200
error::Error SetLocale(const std::string& locale);

psicash_test.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,19 +279,27 @@ TEST_F(TestPsiCash, SetHTTPRequestFn) {
279279
}
280280
}
281281

282-
TEST_F(TestPsiCash, SetRequestMetadataItem) {
282+
TEST_F(TestPsiCash, SetRequestMetadataItems) {
283283
PsiCashTester pc;
284284
auto err = pc.Init(TestPsiCash::UserAgent(), GetTempDir().c_str(), nullptr, false);
285285
ASSERT_FALSE(err);
286286

287287
auto j = pc.user_data().GetRequestMetadata();
288288
ASSERT_EQ(j.size(), 0);
289289

290-
err = pc.SetRequestMetadataItem("k", "v");
290+
err = pc.SetRequestMetadataItems({{"k", "v"}});
291291
ASSERT_FALSE(err);
292292

293293
j = pc.user_data().GetRequestMetadata();
294294
ASSERT_EQ(j["k"], "v");
295+
296+
err = pc.SetRequestMetadataItems({{"a", "b"}, {"x", "y"}});
297+
ASSERT_FALSE(err);
298+
299+
j = pc.user_data().GetRequestMetadata();
300+
ASSERT_EQ(j["k"], "v");
301+
ASSERT_EQ(j["a"], "b");
302+
ASSERT_EQ(j["x"], "y");
295303
}
296304

297305
TEST_F(TestPsiCash, SetLocale) {
@@ -934,15 +942,15 @@ TEST_F(TestPsiCash, ModifyLandingPage) {
934942
// With metadata
935943
//
936944

937-
err = pc.SetRequestMetadataItem("k", "v");
945+
err = pc.SetRequestMetadataItems({{"k", "v"}, {"x", "y"}});
938946
ASSERT_FALSE(err);
939947
url_in = {"https://asdf.sadf.gf", "", ""};
940948
res = pc.ModifyLandingPage(url_in.ToString());
941949
ASSERT_TRUE(res);
942950
url_out.Parse(*res);
943951
ASSERT_EQ(url_out.scheme_host_path_, url_in.scheme_host_path_);
944952
ASSERT_EQ(url_out.fragment_, url_in.fragment_);
945-
ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"metadata":{"k":"v"},"tokens":"kEarnerTokenType"})"_json), IsEmpty());
953+
ASSERT_THAT(TokenPayloadsMatch(url_out.query_.substr(key_part.length()), R"({"metadata":{"k":"v","x":"y"},"tokens":"kEarnerTokenType"})"_json), IsEmpty());
946954

947955
//
948956
// Errors
@@ -1077,7 +1085,7 @@ TEST_F(TestPsiCash, GetRewardedActivityData) {
10771085
ASSERT_TRUE(res);
10781086
ASSERT_EQ(*res, base64::B64Encode(utils::Stringer(R"({"metadata":{"user_agent":")", TestPsiCash::UserAgent(), R"(","v":1},"tokens":"kEarnerTokenType","v":1})")));
10791087

1080-
err = pc.SetRequestMetadataItem("k", "v");
1088+
err = pc.SetRequestMetadataItems({{"k", "v"}});
10811089
ASSERT_FALSE(err);
10821090
res = pc.GetRewardedActivityData();
10831091
ASSERT_TRUE(res);

test_helpers.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ class TempDir
6868
return dev ? ".dev" : ".prod";
6969
}
7070

71+
std::string DatastoreFilepath(const std::string& datastore_root, const char* suffix) {
72+
return datastore_root + "/psicashdatastore" + suffix;
73+
}
74+
75+
std::string DatastoreFilepath(const std::string& datastore_root, const std::string& suffix) {
76+
return DatastoreFilepath(datastore_root, suffix.c_str());
77+
}
78+
7179
std::string DatastoreFilepath(const std::string& datastore_root, bool dev) {
72-
return datastore_root + "/psicashdatastore" + GetSuffix(dev);
80+
return DatastoreFilepath(datastore_root, GetSuffix(dev));
7381
}
7482

7583
bool Write(const std::string& datastore_root, bool dev, const std::string& s) {

0 commit comments

Comments
 (0)