Address race conditions when flushing logs
This commit is contained in:
parent
378126e746
commit
ea063ab8b0
1 changed files with 68 additions and 26 deletions
|
@ -99,6 +99,11 @@ class IndexedDBLogStore {
|
||||||
this.id = "instance-" + Math.random() + Date.now();
|
this.id = "instance-" + Math.random() + Date.now();
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
this.db = null;
|
this.db = null;
|
||||||
|
// Promise is not null when a flush is IN PROGRESS
|
||||||
|
this.flushPromise = null;
|
||||||
|
// Promise is not null when flush() is called when one is already in
|
||||||
|
// progress.
|
||||||
|
this.flushAgainPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,35 +153,80 @@ class IndexedDBLogStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Flush logs to disk.
|
||||||
|
*
|
||||||
|
* There are guards to protect against race conditions in order to ensure
|
||||||
|
* that all previous flushes have completed before the most recent flush.
|
||||||
|
* Consider without guards:
|
||||||
|
* - A calls flush() periodically.
|
||||||
|
* - B calls flush() and wants to send logs immediately afterwards.
|
||||||
|
* - If B doesn't wait for A's flush to complete, B will be missing the
|
||||||
|
* contents of A's flush.
|
||||||
|
* To protect against this, we set 'flushPromise' when a flush is ongoing.
|
||||||
|
* Subsequent calls to flush() during this period return a new promise
|
||||||
|
* 'flushAgainPromise' which is chained off the current 'flushPromise'.
|
||||||
|
* Subsequent calls to flush() when the first flush hasn't completed will
|
||||||
|
* return the same 'flushAgainPromise' as we can guarantee that we WILL
|
||||||
|
* do a brand new flush at some point in the future. Once the first flush
|
||||||
|
* has completed, 'flushAgainPromise' becomes 'flushPromise' and can be
|
||||||
|
* chained again.
|
||||||
|
*
|
||||||
|
* This guarantees that we will always eventually do a flush when flush() is
|
||||||
|
* called.
|
||||||
|
*
|
||||||
* @return {Promise} Resolved when the logs have been flushed.
|
* @return {Promise} Resolved when the logs have been flushed.
|
||||||
*/
|
*/
|
||||||
flush() {
|
flush() {
|
||||||
|
if (this.flushPromise) { // a flush is ongoing
|
||||||
|
if (this.flushAgainPromise) { // a flush is queued up, return that.
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
// queue up a new flush
|
||||||
|
this.flushAgainPromise = this.flushPromise.then(() => {
|
||||||
|
// the current flush has completed, so shuffle the promises
|
||||||
|
// around:
|
||||||
|
// flushAgainPromise => flushPromise and null flushAgainPromise.
|
||||||
|
// flushPromise has already nulled itself.
|
||||||
|
this.flushAgainPromise = null;
|
||||||
|
return this.flush();
|
||||||
|
});
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flushPromise = new Promise((resolve, reject) => {
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
// not connected yet or user rejected access for us to r/w to the db
|
// not connected yet or user rejected access for us to r/w to
|
||||||
return Promise.reject(new Error("No connected database"));
|
// the db.
|
||||||
|
this.flushPromise = null;
|
||||||
|
reject(new Error("No connected database"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const lines = this.logger.flush();
|
const lines = this.logger.flush();
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
return Promise.resolve();
|
this.flushPromise = null;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
let txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
let objStore = txn.objectStore("logs");
|
let objStore = txn.objectStore("logs");
|
||||||
objStore.add(this._generateLogEntry(lines));
|
objStore.add(this._generateLogEntry(lines));
|
||||||
let lastModStore = txn.objectStore("logslastmod");
|
let lastModStore = txn.objectStore("logslastmod");
|
||||||
lastModStore.put(this._generateLastModifiedTime());
|
lastModStore.put(this._generateLastModifiedTime());
|
||||||
txn.oncomplete = (event) => {
|
txn.oncomplete = (event) => {
|
||||||
|
this.flushPromise = null;
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
txn.onerror = (event) => {
|
txn.onerror = (event) => {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed to flush logs : ", event
|
"Failed to flush logs : ", event
|
||||||
);
|
);
|
||||||
|
this.flushPromise = null;
|
||||||
reject(
|
reject(
|
||||||
new Error("Failed to write logs: " + event.target.errorCode)
|
new Error("Failed to write logs: " + event.target.errorCode)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return this.flushPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -368,17 +418,6 @@ module.exports = {
|
||||||
return initPromise;
|
return initPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Force-flush the logs to storage.
|
|
||||||
* @return {Promise} Resolved when the logs have been flushed.
|
|
||||||
*/
|
|
||||||
flush: async function() {
|
|
||||||
if (!store) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await store.flush();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up old logs.
|
* Clean up old logs.
|
||||||
* @return Promise Resolves if cleaned logs.
|
* @return Promise Resolves if cleaned logs.
|
||||||
|
@ -422,17 +461,20 @@ module.exports = {
|
||||||
|
|
||||||
// If in incognito mode, store is null, but we still want bug report
|
// If in incognito mode, store is null, but we still want bug report
|
||||||
// sending to work going off the in-memory console logs.
|
// sending to work going off the in-memory console logs.
|
||||||
|
console.log("Sending bug report.");
|
||||||
let logs = [];
|
let logs = [];
|
||||||
if (store) {
|
if (store) {
|
||||||
|
// flush most recent logs
|
||||||
|
await store.flush();
|
||||||
logs = await store.consume();
|
logs = await store.consume();
|
||||||
}
|
}
|
||||||
// and add the most recent console logs which won't be in the store yet.
|
else {
|
||||||
const consoleLogs = logger.flush(); // remove logs from console
|
logs.push({
|
||||||
const currentId = store ? store.id : "-";
|
lines: logger.flush(),
|
||||||
logs.unshift({
|
id: "-",
|
||||||
lines: consoleLogs,
|
|
||||||
id: currentId,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
request({
|
request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
Loading…
Reference in a new issue