properly implement db resetting

This commit is contained in:
Florian Dold 2017-06-05 02:00:03 +02:00
parent 49949de808
commit e95027f377
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 139 additions and 41 deletions

View File

@ -75,3 +75,69 @@ export function every(delayMs: number, callback: () => void): TimerHandle {
export function after(delayMs: number, callback: () => void): TimerHandle {
return new TimeoutHandle(setInterval(callback, delayMs));
}
const nullTimerHandle = {
clear() {
}
};
/**
* Group of timers that can be destroyed at once.
*/
export class TimerGroup {
private stopped: boolean = false;
private timerMap: { [index: number]: TimerHandle } = {};
private idGen = 1;
stopCurrentAndFutureTimers() {
this.stopped = true;
for (const x in this.timerMap) {
if (!this.timerMap.hasOwnProperty(x)) {
continue;
}
this.timerMap[x].clear();
delete this.timerMap[x];
}
}
after(delayMs: number, callback: () => void): TimerHandle {
if (this.stopped) {
console.warn("dropping timer since timer group is stopped");
return nullTimerHandle;
}
const h = after(delayMs, callback);
let myId = this.idGen++;
this.timerMap[myId] = h;
const tm = this.timerMap;
return {
clear() {
h.clear();
delete tm[myId];
},
};
}
every(delayMs: number, callback: () => void): TimerHandle {
if (this.stopped) {
console.warn("dropping timer since timer group is stopped");
return nullTimerHandle;
}
const h = every(delayMs, callback);
let myId = this.idGen++;
this.timerMap[myId] = h;
const tm = this.timerMap;
return {
clear() {
h.clear();
delete tm[myId];
},
};
}
}

View File

@ -43,6 +43,7 @@ import {
QueryRoot,
Store,
} from "./query";
import {TimerGroup} from "./timer";
import {
AmountJson,
Amounts,
@ -346,18 +347,6 @@ const builtinCurrencies: CurrencyRecord[] = [
];
// FIXME: these functions should be dependency-injected
// into the wallet, as this is chrome specific => bad
function setTimeout(f: any, t: number) {
return chrome.extension.getBackgroundPage().setTimeout(f, t);
}
function setInterval(f: any, t: number) {
return chrome.extension.getBackgroundPage().setInterval(f, t);
}
function isWithdrawableDenom(d: DenominationRecord) {
const nowSec = (new Date()).getTime() / 1000;
const stampWithdrawSec = getTalerStampSec(d.stampExpireWithdraw);
@ -583,13 +572,17 @@ interface CoinsForPaymentArgs {
* The platform-independent wallet implementation.
*/
export class Wallet {
private db: IDBDatabase;
/**
* IndexedDB database used by the wallet.
*/
db: IDBDatabase;
private http: HttpRequestLibrary;
private badge: Badge;
private notifier: Notifier;
private cryptoApi: CryptoApi;
private processPreCoinConcurrent = 0;
private processPreCoinThrottle: {[url: string]: number} = {};
private timerGroup: TimerGroup;
/**
* Set of identifiers for running operations.
@ -613,7 +606,9 @@ export class Wallet {
this.fillDefaults();
this.resumePendingFromDb();
setInterval(() => this.updateExchanges(), 1000 * 60 * 15);
this.timerGroup = new TimerGroup();
this.timerGroup.every(1000 * 60 * 15, () => this.updateExchanges());
}
private async fillDefaults() {
@ -1027,8 +1022,7 @@ export class Wallet {
// random, exponential backoff truncated at 3 minutes
const nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), 3000 * 60);
console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
setTimeout(() => this.processReserve(reserveRecord, nextDelay),
retryDelayMs);
this.timerGroup.after(retryDelayMs, () => this.processReserve(reserveRecord, nextDelay))
} finally {
this.stopOperation(opId);
}
@ -1039,8 +1033,7 @@ export class Wallet {
retryDelayMs = 200): Promise<void> {
if (this.processPreCoinConcurrent >= 4 || this.processPreCoinThrottle[preCoin.exchangeBaseUrl]) {
console.log("delaying processPreCoin");
setTimeout(() => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)),
retryDelayMs);
this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, Math.min(retryDelayMs * 2, 5 * 60 * 1000)));
return;
}
console.log("executing processPreCoin");
@ -1098,12 +1091,11 @@ export class Wallet {
"ms", e);
// exponential backoff truncated at one minute
const nextRetryDelayMs = Math.min(retryDelayMs * 2, 5 * 60 * 1000);
setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
retryDelayMs);
this.timerGroup.after(retryDelayMs, () => this.processPreCoin(preCoin, nextRetryDelayMs))
const currentThrottle = this.processPreCoinThrottle[preCoin.exchangeBaseUrl] || 0;
this.processPreCoinThrottle[preCoin.exchangeBaseUrl] = currentThrottle + 1;
setTimeout(() => {this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; }, retryDelayMs);
this.timerGroup.after(retryDelayMs, () => {this.processPreCoinThrottle[preCoin.exchangeBaseUrl]--; });
} finally {
this.processPreCoinConcurrent--;
}
@ -2335,4 +2327,10 @@ export class Wallet {
return await this.q().iter(Stores.reserves).filter((r) => r.hasPayback).toArray();
}
/**
* Stop ongoing processing.
*/
stop() {
this.timerGroup.stopCurrentAndFutureTimers();
}
}

View File

@ -526,7 +526,7 @@ function openExtensionPage(page: string) {
function openTab(page: string) {
return (evt) => {
return (evt: React.SyntheticEvent<any>) => {
evt.preventDefault();
chrome.tabs.create({
url: page,

View File

@ -246,9 +246,9 @@ function handleMessage(db: IDBDatabase,
}
}
async function dispatch(db: IDBDatabase, wallet: Wallet, req: any, sender: any, sendResponse: any): Promise<void> {
async function dispatch(wallet: Wallet, req: any, sender: any, sendResponse: any): Promise<void> {
try {
const p = handleMessage(db, wallet, sender, req.type, req.detail);
const p = handleMessage(wallet.db, wallet, sender, req.type, req.detail);
const r = await p;
try {
sendResponse(r);
@ -421,6 +421,38 @@ function clearRateLimitCache() {
rateLimitCache = {};
}
/**
* Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset.
*/
let currentWallet: Wallet|undefined;
async function reinitWallet() {
if (currentWallet) {
currentWallet.stop();
currentWallet = undefined;
}
chrome.browserAction.setBadgeText({ text: "" });
const badge = new ChromeBadge();
let db: IDBDatabase;
try {
db = await openTalerDb();
} catch (e) {
console.error("could not open database", e);
return;
}
const http = new BrowserHttpLib();
const notifier = new ChromeNotifier();
console.log("setting wallet");
const wallet = new Wallet(db, http, badge, notifier);
// Useful for debugging in the background page.
(window as any).talerWallet = wallet;
currentWallet = wallet;
}
/**
* Main function to run for the WebExtension backend.
*
@ -438,9 +470,6 @@ export async function wxMain() {
logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0);
};
chrome.browserAction.setBadgeText({ text: "" });
const badge = new ChromeBadge();
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
if (!tab.url || !tab.id) {
@ -514,29 +543,28 @@ export async function wxMain() {
chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000);
let db: IDBDatabase;
try {
db = await openTalerDb();
} catch (e) {
console.error("could not open database", e);
return;
}
const http = new BrowserHttpLib();
const notifier = new ChromeNotifier();
console.log("setting wallet");
const wallet = new Wallet(db, http, badge!, notifier);
// Useful for debugging in the background page.
(window as any).talerWallet = wallet;
reinitWallet();
// Handlers for messages coming directly from the content
// script on the page
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
dispatch(db, wallet, req, sender, sendResponse);
const wallet = currentWallet;
if (!wallet) {
console.warn("wallet not available while handling message");
console.warn("dropped request message was", req);
return;
}
dispatch(wallet, req, sender, sendResponse);
return true;
});
// Handlers for catching HTTP requests
chrome.webRequest.onHeadersReceived.addListener((details) => {
const wallet = currentWallet;
if (!wallet) {
console.warn("wallet not available while handling header");
}
if (details.statusCode === 402) {
console.log(`got 402 from ${details.url}`);
return handleHttpPayment(details.responseHeaders || [],
@ -559,9 +587,15 @@ function openTalerDb(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onerror = (e) => {
console.log("taler database error", e);
reject(e);
};
req.onsuccess = (e) => {
req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
console.log(`handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`);
req.result.close();
reinitWallet();
};
resolve(req.result);
};
req.onupgradeneeded = (e) => {