diff --git a/src/public/app/services/sync.js b/src/public/app/services/sync.js index ad64d3f46..d3b20e7e5 100644 --- a/src/public/app/services/sync.js +++ b/src/public/app/services/sync.js @@ -8,8 +8,8 @@ async function syncNow() { toastService.showMessage("Sync finished successfully."); } else { - if (result.message.length > 50) { - result.message = result.message.substr(0, 50); + if (result.message.length > 100) { + result.message = result.message.substr(0, 100); } toastService.showError("Sync failed: " + result.message); @@ -25,4 +25,4 @@ async function forceNoteSync(noteId) { export default { syncNow, forceNoteSync -}; \ No newline at end of file +}; diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index d2c11b0b3..54cf0a805 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.js @@ -316,11 +316,11 @@ function dynamicRequire(moduleName) { } } -function timeLimit(cb, limitMs) { +function timeLimit(promise, limitMs) { return new Promise((res, rej) => { let resolved = false; - cb().then(() => { + promise.then(() => { resolved = true; res(); @@ -328,7 +328,7 @@ function timeLimit(cb, limitMs) { setTimeout(() => { if (!resolved) { - rej('Process exceeded time limit ' + limitMs); + rej(new Error('Process exceeded time limit ' + limitMs)); } }, limitMs); }); diff --git a/src/public/app/services/ws.js b/src/public/app/services/ws.js index a461d9722..61c2791b6 100644 --- a/src/public/app/services/ws.js +++ b/src/public/app/services/ws.js @@ -157,7 +157,7 @@ async function consumeSyncData() { const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id)); try { - await utils.timeLimit(async () => await processSyncRows(nonProcessedSyncRows), 5000); + await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 5000); } catch (e) { logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`); diff --git a/src/public/app/widgets/global_menu.js b/src/public/app/widgets/global_menu.js index 1c18b7f5d..509bac94e 100644 --- a/src/public/app/widgets/global_menu.js +++ b/src/public/app/widgets/global_menu.js @@ -35,7 +35,7 @@ const TPL = ` - Sync (0) + Sync now (0) @@ -116,4 +116,4 @@ export default class GlobalMenuWidget extends BasicWidget { return this.$widget; } -} \ No newline at end of file +} diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js index f72943303..88a3c5f40 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.js @@ -55,6 +55,8 @@ async function checkSync() { } async function syncNow() { + log.info("Received request to trigger sync now."); + return await syncService.sync(); } @@ -168,4 +170,4 @@ module.exports = { getStats, syncFinished, queueSector -}; \ No newline at end of file +}; diff --git a/src/services/request.js b/src/services/request.js index 9a3377d96..1c0879068 100644 --- a/src/services/request.js +++ b/src/services/request.js @@ -40,7 +40,7 @@ function exec(opts) { host: parsedTargetUrl.hostname, port: parsedTargetUrl.port, path: parsedTargetUrl.path, - timeout: opts.timeout, + timeout: opts.timeout, // works only for node.js client headers, agent: proxyAgent }); @@ -104,7 +104,7 @@ async function getImage(imageUrl) { host: parsedTargetUrl.hostname, port: parsedTargetUrl.port, path: parsedTargetUrl.path, - timeout: opts.timeout, + timeout: opts.timeout, // works only for node client headers: {}, agent: proxyAgent }); @@ -173,4 +173,4 @@ function generateError(opts, message) { module.exports = { exec, getImage -}; \ No newline at end of file +}; diff --git a/src/services/setup.js b/src/services/setup.js index 397a95ebf..0a8fba991 100644 --- a/src/services/setup.js +++ b/src/services/setup.js @@ -6,6 +6,7 @@ const optionService = require('./options'); const syncOptions = require('./sync_options'); const request = require('./request'); const appInfo = require('./app_info'); +const utils = require('./utils'); async function hasSyncServerSchemaAndSeed() { const response = await requestToSyncServer('GET', '/api/setup/status'); @@ -43,13 +44,15 @@ async function sendSeedToSyncServer() { } async function requestToSyncServer(method, path, body = null) { - return await request.exec({ + const timeout = await syncOptions.getSyncTimeout(); + + return utils.timeLimit(request.exec({ method, url: await syncOptions.getSyncServerHost() + path, body, proxy: await syncOptions.getSyncProxy(), - timeout: await syncOptions.getSyncTimeout() - }); + timeout: timeout + }), timeout); } async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) { @@ -115,4 +118,4 @@ module.exports = { sendSeedToSyncServer, setupSyncFromSyncServer, getSyncSeedOptions -}; \ No newline at end of file +}; diff --git a/src/services/sql.js b/src/services/sql.js index 6f5a8d1e7..dc418757d 100644 --- a/src/services/sql.js +++ b/src/services/sql.js @@ -64,15 +64,15 @@ async function upsert(tableName, primaryKey, rec) { } async function beginTransaction() { - return await execute("BEGIN"); + return await dbConnection.run("BEGIN"); } async function commit() { - return await execute("COMMIT"); + return await dbConnection.run("COMMIT"); } async function rollback() { - return await execute("ROLLBACK"); + return await dbConnection.run("ROLLBACK"); } async function getRow(query, params = []) { @@ -150,6 +150,8 @@ async function getColumn(query, params = []) { } async function execute(query, params = []) { + await startTransactionIfNecessary(); + return await wrap(async db => db.run(query, ...params), query); } @@ -158,11 +160,15 @@ async function executeNoWrap(query, params = []) { } async function executeMany(query, params) { + await startTransactionIfNecessary(); + // essentially just alias await getManyRows(query, params); } async function executeScript(query) { + await startTransactionIfNecessary(); + return await wrap(async db => db.exec(query), query); } @@ -199,61 +205,65 @@ async function wrap(func, query) { } } +// true if transaction is active globally. +// cls.namespace.get('isTransactional') OTOH indicates active transaction in active CLS let transactionActive = false; +// resolves when current transaction ends with either COMMIT or ROLLBACK let transactionPromise = null; +let transactionPromiseResolve = null; -async function transactional(func) { - if (cls.namespace.get('isInTransaction')) { - return await func(); +async function startTransactionIfNecessary() { + if (!cls.namespace.get('isTransactional') + || cls.namespace.get('isInTransaction')) { + return; } while (transactionActive) { await transactionPromise; } - let ret = null; - const thisError = new Error(); // to capture correct stack trace in case of exception - + await beginTransaction(); + cls.namespace.set('isInTransaction', true); transactionActive = true; - transactionPromise = new Promise(async (resolve, reject) => { - try { - await beginTransaction(); + transactionPromise = new Promise(res => transactionPromiseResolve = res); +} - cls.namespace.set('isInTransaction', true); +async function transactional(func) { + // if the CLS is already transactional then the whole transaction is handled by higher level transactional() call + if (cls.namespace.get('isTransactional')) { + return await func(); + } - ret = await func(); + cls.namespace.set('isTransactional', true); // we will need a transaction if there's a write operation + try { + const ret = await func(); + + if (cls.namespace.get('isInTransaction')) { await commit(); // note that sync rows sent from this action will be sent again by scheduled periodic ping require('./ws.js').sendPingToAllClients(); transactionActive = false; - resolve(); - - setTimeout(() => require('./ws').sendPingToAllClients(), 50); - } - catch (e) { - if (transactionActive) { - log.error("Error executing transaction, executing rollback. Inner stack: " + e.stack + "\nOutside stack: " + thisError.stack); - - await rollback(); - - transactionActive = false; - } - - reject(e); - } - finally { cls.namespace.set('isInTransaction', false); + transactionPromiseResolve(); } - }); - if (transactionActive) { - await transactionPromise; + return ret; } + catch (e) { + if (transactionActive) { + await rollback(); - return ret; + transactionActive = false; + cls.namespace.set('isInTransaction', false); + // resolving since this is just semaphore for allowing another write transaction to proceed + transactionPromiseResolve(); + } + + throw e; + } } module.exports = { diff --git a/src/services/sync.js b/src/services/sync.js index 1687c9cca..acc2ccd90 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -70,7 +70,7 @@ async function sync() { }; } else { - log.info("sync failed: " + e.message + e.stack); + log.info("sync failed: " + e.message + "\nstack: " + e.stack); return { success: false, @@ -97,7 +97,6 @@ async function doLogin() { const hash = utils.hmac(documentSecret, timestamp); const syncContext = { cookieJar: {} }; - const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', { timestamp: timestamp, syncVersion: appInfo.syncVersion, @@ -259,14 +258,18 @@ async function checkContentHash(syncContext) { } async function syncRequest(syncContext, method, requestPath, body) { - return await request.exec({ + const timeout = await syncOptions.getSyncTimeout(); + + const opts = { method, url: await syncOptions.getSyncServerHost() + requestPath, cookieJar: syncContext.cookieJar, - timeout: await syncOptions.getSyncTimeout(), + timeout: timeout, body, proxy: proxyToggle ? await syncOptions.getSyncProxy() : null - }); + }; + + return await utils.timeLimit(request.exec(opts), timeout); } const primaryKeys = { @@ -380,4 +383,4 @@ module.exports = { getSyncRecords, stats, getMaxSyncId -}; \ No newline at end of file +}; diff --git a/src/services/utils.js b/src/services/utils.js index 539e82999..36dbd9031 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -217,6 +217,24 @@ function formatDownloadTitle(filename, type, mime) { } } +function timeLimit(promise, limitMs) { + return new Promise((res, rej) => { + let resolved = false; + + promise.then(() => { + resolved = true; + + res(); + }); + + setTimeout(() => { + if (!resolved) { + rej(new Error('Process exceeded time limit ' + limitMs)); + } + }, limitMs); + }); +} + module.exports = { randomSecureToken, randomString, @@ -245,5 +263,6 @@ module.exports = { isStringNote, quoteRegex, replaceAll, - formatDownloadTitle + formatDownloadTitle, + timeLimit };