mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
128 lines
5.0 KiB
JavaScript
128 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
setTaskId,
|
|
|
|
acquire,
|
|
wait,
|
|
|
|
release,
|
|
releaseAll,
|
|
releaseByTaskId,
|
|
|
|
TYPE_APP_TASK_PREFIX: 'app_task_',
|
|
TYPE_APP_BACKUP_PREFIX: 'app_backup_',
|
|
TYPE_BOX_UPDATE: 'box_update', // for the actual update and after the backup. this allows the backup before update do not block
|
|
TYPE_BOX_UPDATE_TASK: 'box_update_task', // for scheduling the update task
|
|
TYPE_FULL_BACKUP_TASK_PREFIX: 'full_backup_task_', // for scheduling the backup task
|
|
|
|
TYPE_MAIL_SERVER_RESTART: 'mail_restart',
|
|
};
|
|
|
|
const assert = require('node:assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
database = require('./database.js'),
|
|
debug = require('debug')('box:locks'),
|
|
promiseRetry = require('./promise-retry.js');
|
|
|
|
let gTaskId = null;
|
|
|
|
function setTaskId(taskId) {
|
|
assert.strictEqual(typeof taskId, 'string');
|
|
gTaskId = taskId;
|
|
}
|
|
|
|
async function read() {
|
|
const result = await database.query('SELECT version, dataJson FROM locks');
|
|
return { version: result[0].version, data: JSON.parse(result[0].dataJson) };
|
|
}
|
|
|
|
async function write(value) {
|
|
assert.strictEqual(typeof value.version, 'number');
|
|
assert.strictEqual(typeof value.data, 'object');
|
|
|
|
const result = await database.query('UPDATE locks SET dataJson=?, version=version+1 WHERE id=? AND version=?', [ JSON.stringify(value.data), 'platform', value.version ]);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.CONFLICT, 'Someone updated before we did');
|
|
debug(`write: current locks: ${JSON.stringify(value.data)}`);
|
|
}
|
|
|
|
function canAcquire(data, type) {
|
|
assert.strictEqual(typeof data, 'object');
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
if (type in data) return new BoxError(BoxError.BAD_STATE, `Locked by ${data[type]}`);
|
|
|
|
if (type === exports.TYPE_BOX_UPDATE) {
|
|
if (Object.keys(data).some(k => k.startsWith(exports.TYPE_APP_TASK_PREFIX))) return new BoxError(BoxError.BAD_STATE, 'One or more app tasks are active');
|
|
if (Object.keys(data).some(k => k.startsWith(exports.TYPE_APP_BACKUP_PREFIX))) return new BoxError(BoxError.BAD_STATE, 'One or more app backups are active');
|
|
} else if (type.startsWith(exports.TYPE_APP_TASK_PREFIX)) {
|
|
if (exports.TYPE_BOX_UPDATE in data) return new BoxError(BoxError.BAD_STATE, 'Update is active');
|
|
} else if (type.startsWith(exports.TYPE_FULL_BACKUP_TASK_PREFIX)) {
|
|
if (exports.TYPE_BOX_UPDATE_TASK in data) return new BoxError(BoxError.BAD_STATE, 'Update task is active');
|
|
} else if (type === exports.TYPE_BOX_UPDATE_TASK) {
|
|
if (Object.keys(data).some(k => k.startsWith(exports.TYPE_FULL_BACKUP_TASK_PREFIX))) return new BoxError(BoxError.BAD_STATE, 'One or more backup tasks is active');
|
|
}
|
|
|
|
// TYPE_APP_BACKUP_PREFIX , TYPE_MAIL_SERVER_RESTART can co-run with everything except themselves
|
|
|
|
return null;
|
|
}
|
|
|
|
async function acquire(type) {
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
await promiseRetry({ times: Number.MAX_SAFE_INTEGER, interval: 100, debug, retry: (error) => error.reason === BoxError.CONFLICT }, async () => {
|
|
const { version, data } = await read();
|
|
const error = canAcquire(data, type);
|
|
if (error) throw error;
|
|
data[type] = gTaskId;
|
|
await write({ version, data });
|
|
debug(`acquire: ${type}`);
|
|
});
|
|
}
|
|
|
|
async function wait(type) {
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
await promiseRetry({ times: Number.MAX_SAFE_INTEGER, interval: 10000, debug }, async () => await acquire(type));
|
|
}
|
|
|
|
async function release(type) {
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
await promiseRetry({ times: Number.MAX_SAFE_INTEGER, interval: 100, debug, retry: (error) => error.reason === BoxError.CONFLICT }, async () => {
|
|
const { version, data } = await read();
|
|
if (!(type in data)) throw new BoxError(BoxError.BAD_STATE, `Lock ${type} was never acquired`);
|
|
if (data[type] !== gTaskId) throw new BoxError(BoxError.BAD_STATE, `Task ${gTaskId} attempted to release lock ${type} acquired by ${data[type]}`);
|
|
delete data[type];
|
|
await write({ version, data });
|
|
debug(`release: ${type}`);
|
|
});
|
|
}
|
|
|
|
async function releaseAll() {
|
|
await database.query('DELETE FROM locks');
|
|
await database.query('INSERT INTO locks (id, dataJson) VALUES (?, ?)', [ 'platform', JSON.stringify({}) ]);
|
|
debug('releaseAll: all locks released');
|
|
}
|
|
|
|
// identify programming errors in tasks that forgot to clean up locks
|
|
async function releaseByTaskId(taskId) {
|
|
assert.strictEqual(typeof taskId, 'string');
|
|
|
|
await promiseRetry({ times: Number.MAX_SAFE_INTEGER, interval: 100, debug, retry: (error) => error.reason === BoxError.CONFLICT }, async () => {
|
|
const { version, data } = await read();
|
|
|
|
for (const type of Object.keys(data)) {
|
|
if (data[type] === taskId) {
|
|
debug(`releaseByTaskId: task ${taskId} forgot to unlock ${type}`);
|
|
delete data[type];
|
|
}
|
|
}
|
|
|
|
await write({ version, data });
|
|
|
|
debug(`releaseByTaskId: ${taskId}`);
|
|
});
|
|
}
|