mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
opening transactions only on write operations which enforces exclusive lock only there to improve concurrency, custom handling of sync request timeouts, #1093, #1018
This commit is contained in:
parent
d09b021487
commit
5d47c2b23e
@ -8,8 +8,8 @@ async function syncNow() {
|
|||||||
toastService.showMessage("Sync finished successfully.");
|
toastService.showMessage("Sync finished successfully.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (result.message.length > 50) {
|
if (result.message.length > 100) {
|
||||||
result.message = result.message.substr(0, 50);
|
result.message = result.message.substr(0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
toastService.showError("Sync failed: " + result.message);
|
toastService.showError("Sync failed: " + result.message);
|
||||||
|
@ -316,11 +316,11 @@ function dynamicRequire(moduleName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeLimit(cb, limitMs) {
|
function timeLimit(promise, limitMs) {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|
||||||
cb().then(() => {
|
promise.then(() => {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
|
|
||||||
res();
|
res();
|
||||||
@ -328,7 +328,7 @@ function timeLimit(cb, limitMs) {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
rej('Process exceeded time limit ' + limitMs);
|
rej(new Error('Process exceeded time limit ' + limitMs));
|
||||||
}
|
}
|
||||||
}, limitMs);
|
}, limitMs);
|
||||||
});
|
});
|
||||||
|
@ -157,7 +157,7 @@ async function consumeSyncData() {
|
|||||||
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id));
|
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedSyncIds.has(sync.id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await utils.timeLimit(async () => await processSyncRows(nonProcessedSyncRows), 5000);
|
await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 5000);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
|
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
|
||||||
|
@ -35,7 +35,7 @@ const TPL = `
|
|||||||
|
|
||||||
<a class="dropdown-item sync-now-button" title="Trigger sync">
|
<a class="dropdown-item sync-now-button" title="Trigger sync">
|
||||||
<span class="bx bx-refresh"></span>
|
<span class="bx bx-refresh"></span>
|
||||||
Sync (<span id="outstanding-syncs-count">0</span>)
|
Sync now (<span id="outstanding-syncs-count">0</span>)
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="dropdown-item" data-trigger-command="openNewWindow">
|
<a class="dropdown-item" data-trigger-command="openNewWindow">
|
||||||
|
@ -55,6 +55,8 @@ async function checkSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncNow() {
|
async function syncNow() {
|
||||||
|
log.info("Received request to trigger sync now.");
|
||||||
|
|
||||||
return await syncService.sync();
|
return await syncService.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ function exec(opts) {
|
|||||||
host: parsedTargetUrl.hostname,
|
host: parsedTargetUrl.hostname,
|
||||||
port: parsedTargetUrl.port,
|
port: parsedTargetUrl.port,
|
||||||
path: parsedTargetUrl.path,
|
path: parsedTargetUrl.path,
|
||||||
timeout: opts.timeout,
|
timeout: opts.timeout, // works only for node.js client
|
||||||
headers,
|
headers,
|
||||||
agent: proxyAgent
|
agent: proxyAgent
|
||||||
});
|
});
|
||||||
@ -104,7 +104,7 @@ async function getImage(imageUrl) {
|
|||||||
host: parsedTargetUrl.hostname,
|
host: parsedTargetUrl.hostname,
|
||||||
port: parsedTargetUrl.port,
|
port: parsedTargetUrl.port,
|
||||||
path: parsedTargetUrl.path,
|
path: parsedTargetUrl.path,
|
||||||
timeout: opts.timeout,
|
timeout: opts.timeout, // works only for node client
|
||||||
headers: {},
|
headers: {},
|
||||||
agent: proxyAgent
|
agent: proxyAgent
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ const optionService = require('./options');
|
|||||||
const syncOptions = require('./sync_options');
|
const syncOptions = require('./sync_options');
|
||||||
const request = require('./request');
|
const request = require('./request');
|
||||||
const appInfo = require('./app_info');
|
const appInfo = require('./app_info');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
async function hasSyncServerSchemaAndSeed() {
|
async function hasSyncServerSchemaAndSeed() {
|
||||||
const response = await requestToSyncServer('GET', '/api/setup/status');
|
const response = await requestToSyncServer('GET', '/api/setup/status');
|
||||||
@ -43,13 +44,15 @@ async function sendSeedToSyncServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestToSyncServer(method, path, body = null) {
|
async function requestToSyncServer(method, path, body = null) {
|
||||||
return await request.exec({
|
const timeout = await syncOptions.getSyncTimeout();
|
||||||
|
|
||||||
|
return utils.timeLimit(request.exec({
|
||||||
method,
|
method,
|
||||||
url: await syncOptions.getSyncServerHost() + path,
|
url: await syncOptions.getSyncServerHost() + path,
|
||||||
body,
|
body,
|
||||||
proxy: await syncOptions.getSyncProxy(),
|
proxy: await syncOptions.getSyncProxy(),
|
||||||
timeout: await syncOptions.getSyncTimeout()
|
timeout: timeout
|
||||||
});
|
}), timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
|
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
|
||||||
|
@ -64,15 +64,15 @@ async function upsert(tableName, primaryKey, rec) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function beginTransaction() {
|
async function beginTransaction() {
|
||||||
return await execute("BEGIN");
|
return await dbConnection.run("BEGIN");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function commit() {
|
async function commit() {
|
||||||
return await execute("COMMIT");
|
return await dbConnection.run("COMMIT");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rollback() {
|
async function rollback() {
|
||||||
return await execute("ROLLBACK");
|
return await dbConnection.run("ROLLBACK");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRow(query, params = []) {
|
async function getRow(query, params = []) {
|
||||||
@ -150,6 +150,8 @@ async function getColumn(query, params = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function execute(query, params = []) {
|
async function execute(query, params = []) {
|
||||||
|
await startTransactionIfNecessary();
|
||||||
|
|
||||||
return await wrap(async db => db.run(query, ...params), query);
|
return await wrap(async db => db.run(query, ...params), query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,11 +160,15 @@ async function executeNoWrap(query, params = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeMany(query, params) {
|
async function executeMany(query, params) {
|
||||||
|
await startTransactionIfNecessary();
|
||||||
|
|
||||||
// essentially just alias
|
// essentially just alias
|
||||||
await getManyRows(query, params);
|
await getManyRows(query, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeScript(query) {
|
async function executeScript(query) {
|
||||||
|
await startTransactionIfNecessary();
|
||||||
|
|
||||||
return await wrap(async db => db.exec(query), query);
|
return await wrap(async db => db.exec(query), query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,62 +205,66 @@ 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;
|
let transactionActive = false;
|
||||||
|
// resolves when current transaction ends with either COMMIT or ROLLBACK
|
||||||
let transactionPromise = null;
|
let transactionPromise = null;
|
||||||
|
let transactionPromiseResolve = null;
|
||||||
|
|
||||||
async function transactional(func) {
|
async function startTransactionIfNecessary() {
|
||||||
if (cls.namespace.get('isInTransaction')) {
|
if (!cls.namespace.get('isTransactional')
|
||||||
return await func();
|
|| cls.namespace.get('isInTransaction')) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (transactionActive) {
|
while (transactionActive) {
|
||||||
await transactionPromise;
|
await transactionPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = null;
|
|
||||||
const thisError = new Error(); // to capture correct stack trace in case of exception
|
|
||||||
|
|
||||||
transactionActive = true;
|
|
||||||
transactionPromise = new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
await beginTransaction();
|
await beginTransaction();
|
||||||
|
|
||||||
cls.namespace.set('isInTransaction', true);
|
cls.namespace.set('isInTransaction', true);
|
||||||
|
transactionActive = true;
|
||||||
|
transactionPromise = new Promise(res => transactionPromiseResolve = res);
|
||||||
|
}
|
||||||
|
|
||||||
ret = await func();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
await commit();
|
||||||
|
|
||||||
// note that sync rows sent from this action will be sent again by scheduled periodic ping
|
// note that sync rows sent from this action will be sent again by scheduled periodic ping
|
||||||
require('./ws.js').sendPingToAllClients();
|
require('./ws.js').sendPingToAllClients();
|
||||||
|
|
||||||
transactionActive = false;
|
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);
|
cls.namespace.set('isInTransaction', false);
|
||||||
}
|
transactionPromiseResolve();
|
||||||
});
|
|
||||||
|
|
||||||
if (transactionActive) {
|
|
||||||
await transactionPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (transactionActive) {
|
||||||
|
await rollback();
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
setDbConnection,
|
setDbConnection,
|
||||||
|
@ -70,7 +70,7 @@ async function sync() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info("sync failed: " + e.message + e.stack);
|
log.info("sync failed: " + e.message + "\nstack: " + e.stack);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -97,7 +97,6 @@ async function doLogin() {
|
|||||||
const hash = utils.hmac(documentSecret, timestamp);
|
const hash = utils.hmac(documentSecret, timestamp);
|
||||||
|
|
||||||
const syncContext = { cookieJar: {} };
|
const syncContext = { cookieJar: {} };
|
||||||
|
|
||||||
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
|
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
syncVersion: appInfo.syncVersion,
|
syncVersion: appInfo.syncVersion,
|
||||||
@ -259,14 +258,18 @@ async function checkContentHash(syncContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncRequest(syncContext, method, requestPath, body) {
|
async function syncRequest(syncContext, method, requestPath, body) {
|
||||||
return await request.exec({
|
const timeout = await syncOptions.getSyncTimeout();
|
||||||
|
|
||||||
|
const opts = {
|
||||||
method,
|
method,
|
||||||
url: await syncOptions.getSyncServerHost() + requestPath,
|
url: await syncOptions.getSyncServerHost() + requestPath,
|
||||||
cookieJar: syncContext.cookieJar,
|
cookieJar: syncContext.cookieJar,
|
||||||
timeout: await syncOptions.getSyncTimeout(),
|
timeout: timeout,
|
||||||
body,
|
body,
|
||||||
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null
|
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return await utils.timeLimit(request.exec(opts), timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryKeys = {
|
const primaryKeys = {
|
||||||
|
@ -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 = {
|
module.exports = {
|
||||||
randomSecureToken,
|
randomSecureToken,
|
||||||
randomString,
|
randomString,
|
||||||
@ -245,5 +263,6 @@ module.exports = {
|
|||||||
isStringNote,
|
isStringNote,
|
||||||
quoteRegex,
|
quoteRegex,
|
||||||
replaceAll,
|
replaceAll,
|
||||||
formatDownloadTitle
|
formatDownloadTitle,
|
||||||
|
timeLimit
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user