Skip to content

Commit db26d02

Browse files
authored
fix(loki): fix a autosave race condition when using asynchronous adapter (#79)
See techfort/LokiJS@59aa3af
1 parent 8081799 commit db26d02

File tree

3 files changed

+69
-29
lines changed

3 files changed

+69
-29
lines changed

packages/loki/spec/generic/dynamic_view.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ describe("dynamicviews", () => {
335335
public message = "TestErrorMessage";
336336
}
337337

338-
db.autosaveEnable();
338+
db["_autosaveEnable"]();
339339
db.on("close", () => {
340340
throw new TestError();
341341
});

packages/loki/spec/generic/persistence.spec.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,30 @@ describe("async adapter tests", () => {
544544
}, 200);
545545
});
546546

547-
it("verify loadDatabase in the middle of throttled saves will wait for queue to drain first", (done) => {
547+
it("verify there is no race condition with dirty-checking", (done) => {
548+
const mem = new MemoryStorage({ asyncResponses: true, asyncTimeout: 50 });
549+
const db = new Loki("sandbox.db");
550+
551+
db.initializePersistence({adapter: mem});
552+
553+
const items = db.addCollection<User & {foo?: string}>("items");
554+
items.insert({ name : "mjolnir", owner: "thor", maker: "dwarves" });
555+
const gungnir = items.insert({ name : "gungnir", owner: "odin", maker: "elves" });
556+
557+
expect(db["_autosaveDirty"]()).toBe(true);
558+
559+
db.saveDatabase().then(() => {
560+
// since an update happened after calling saveDatabase (but before save was commited), db should still be dirty
561+
expect(db["_autosaveDirty"]()).toBe(true);
562+
done();
563+
});
564+
565+
// this happens immediately after saveDatabase is called
566+
gungnir.foo = "bar";
567+
items.update(gungnir);
568+
});
569+
570+
it("verify loadDatabase in the middle of throttled saves will wait for queue to drain first", (done) => {
548571
const mem = new MemoryStorage({asyncResponses: true, asyncTimeout: 75});
549572
const db = new Loki("sandbox.db");
550573
db.initializePersistence({adapter: mem});

packages/loki/src/loki.ts

+44-27
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class Loki extends LokiEventEmitter {
189189
}
190190
}
191191

192-
this.autosaveDisable();
192+
this._autosaveDisable();
193193

194194
// if they want to load database on loki instantiation, now is a good time to load... after adapter set and before
195195
// possible autosave initiation
@@ -202,7 +202,7 @@ export class Loki extends LokiEventEmitter {
202202

203203
return loaded.then(() => {
204204
if (this._autosave) {
205-
this.autosaveEnable();
205+
this._autosaveEnable();
206206
}
207207
});
208208
}
@@ -734,13 +734,9 @@ export class Loki extends LokiEventEmitter {
734734
// for autosave scenarios, we will let close perform final save (if dirty)
735735
// For web use, you might call from window.onbeforeunload to shutdown database, saving pending changes
736736
if (this._autosave) {
737-
this.autosaveDisable();
738-
// Check if collections are dirty.
739-
for (let idx = 0; idx < this._collections.length; idx++) {
740-
if (this._collections[idx].dirty) {
741-
saved = this.saveDatabase();
742-
break;
743-
}
737+
this._autosaveDisable();
738+
if (this._autosaveDirty()) {
739+
saved = this.saveDatabase();
744740
}
745741
}
746742

@@ -929,25 +925,24 @@ export class Loki extends LokiEventEmitter {
929925
return Promise.reject(new Error("persistenceAdapter not configured"));
930926
}
931927

932-
let saved;
933-
934928
// check if the adapter is requesting (and supports) a 'reference' mode export
935929
if (this._persistenceAdapter.mode === "reference" && typeof this._persistenceAdapter.exportDatabase === "function") {
936930
// filename may seem redundant but loadDatabase will need to expect this same filename
937-
saved = this._persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable: true}));
938-
}
939-
// otherwise just pass the serialized database to adapter
940-
else {
941-
saved = this._persistenceAdapter.saveDatabase(this.filename, this.serialize() as string);
931+
return Promise.resolve(this._persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable: true})))
932+
.then(() => {
933+
this._autosaveClearFlags();
934+
this.emit("save");
935+
});
942936
}
943937

944-
return Promise.resolve(saved).then(() => {
945-
// Set all collection not dirty.
946-
for (let idx = 0; idx < this._collections.length; idx++) {
947-
this._collections[idx].dirty = false;
948-
}
949-
this.emit("save");
950-
});
938+
// otherwise just pass the serialized database to adapter
939+
// persistenceAdapter might be asynchronous, so we must clear `dirty` immediately
940+
// or autosave won't work if an update occurs between here and the callback
941+
this._autosaveClearFlags();
942+
return Promise.resolve(this._persistenceAdapter.saveDatabase(this.filename, this.serialize() as string))
943+
.then(() => {
944+
this.emit("save");
945+
});
951946
}
952947

953948
/**
@@ -959,7 +954,7 @@ export class Loki extends LokiEventEmitter {
959954
*
960955
* @returns {Promise} a Promise that resolves after the database is persisted
961956
*/
962-
saveDatabase() {
957+
public saveDatabase() {
963958
if (!this._throttledSaves) {
964959
return this._saveDatabase();
965960
}
@@ -989,7 +984,7 @@ export class Loki extends LokiEventEmitter {
989984
*
990985
* @returns {Promise} a Promise that resolves after the database is deleted
991986
*/
992-
deleteDatabase() {
987+
public deleteDatabase() {
993988
// the persistenceAdapter should be present if all is ok, but check to be sure.
994989
if (this._persistenceAdapter === null) {
995990
return Promise.reject(new Error("persistenceAdapter not configured"));
@@ -998,10 +993,32 @@ export class Loki extends LokiEventEmitter {
998993
return Promise.resolve(this._persistenceAdapter.deleteDatabase(this.filename));
999994
}
1000995

996+
/**
997+
* Check whether any collections are "dirty" meaning we need to save the (entire) database
998+
* @returns {boolean} - true if database has changed since last autosave, otherwise false
999+
*/
1000+
private _autosaveDirty(): boolean {
1001+
for (let idx = 0; idx < this._collections.length; idx++) {
1002+
if (this._collections[idx].dirty) {
1003+
return true;
1004+
}
1005+
}
1006+
return false;
1007+
}
1008+
1009+
/**
1010+
* Resets dirty flags on all collections.
1011+
*/
1012+
private _autosaveClearFlags() {
1013+
for (let idx = 0; idx < this._collections.length; idx++) {
1014+
this._collections[idx].dirty = false;
1015+
}
1016+
}
1017+
10011018
/**
10021019
* Starts periodically saves to the underlying storage adapter.
10031020
*/
1004-
autosaveEnable() {
1021+
private _autosaveEnable(): void {
10051022
if (this._autosaveHandle) {
10061023
return;
10071024
}
@@ -1024,7 +1041,7 @@ export class Loki extends LokiEventEmitter {
10241041
/**
10251042
* Stops the autosave interval timer.
10261043
*/
1027-
autosaveDisable() {
1044+
private _autosaveDisable(): void {
10281045
this._autosave = false;
10291046

10301047
if (this._autosaveHandle) {

0 commit comments

Comments
 (0)