2026-02-14 09:53:14 +01:00
import assert from 'node:assert' ;
import BoxError from './boxerror.js' ;
2026-02-14 15:43:24 +01:00
import database from './database.js' ;
2026-03-12 22:55:28 +05:30
import logger from './logger.js' ;
2026-02-14 09:53:14 +01:00
import promiseRetry from './promise-retry.js' ;
2024-12-07 14:35:45 +01:00
2026-03-12 22:55:28 +05:30
const { log , trace } = logger ( 'locks' ) ;
2026-02-14 09:53:14 +01:00
const TYPE _APP _TASK _PREFIX = 'app_task_' ;
const TYPE _APP _BACKUP _PREFIX = 'app_backup_' ;
const TYPE _BOX _UPDATE = 'box_update' ;
const TYPE _BOX _UPDATE _TASK = 'box_update_task' ;
const TYPE _FULL _BACKUP _TASK _PREFIX = 'full_backup_task_' ;
2024-12-07 14:35:45 +01:00
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' ) ;
2026-03-12 22:55:28 +05:30
log ( ` write: current locks: ${ JSON . stringify ( value . data ) } ` ) ;
2024-12-07 14:35:45 +01:00
}
function canAcquire ( data , type ) {
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
2025-07-25 14:46:55 +02:00
if ( type in data ) return new BoxError ( BoxError . BAD _STATE , ` Locked by ${ data [ type ] } ` ) ;
2026-02-14 09:53:14 +01:00
if ( type === TYPE _BOX _UPDATE ) {
if ( Object . keys ( data ) . some ( k => k . startsWith ( 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 ( TYPE _APP _BACKUP _PREFIX ) ) ) return new BoxError ( BoxError . BAD _STATE , 'One or more app backups are active' ) ;
} else if ( type . startsWith ( TYPE _APP _TASK _PREFIX ) ) {
if ( TYPE _BOX _UPDATE in data ) return new BoxError ( BoxError . BAD _STATE , 'Update is active' ) ;
} else if ( type . startsWith ( TYPE _FULL _BACKUP _TASK _PREFIX ) ) {
if ( TYPE _BOX _UPDATE _TASK in data ) return new BoxError ( BoxError . BAD _STATE , 'Update task is active' ) ;
} else if ( type === TYPE _BOX _UPDATE _TASK ) {
if ( Object . keys ( data ) . some ( k => k . startsWith ( TYPE _FULL _BACKUP _TASK _PREFIX ) ) ) return new BoxError ( BoxError . BAD _STATE , 'One or more backup tasks is active' ) ;
2024-12-07 14:35:45 +01:00
}
2025-07-25 14:46:55 +02:00
// TYPE_APP_BACKUP_PREFIX , TYPE_MAIL_SERVER_RESTART can co-run with everything except themselves
2025-07-18 10:56:52 +02:00
2024-12-07 14:35:45 +01:00
return null ;
}
async function acquire ( type ) {
assert . strictEqual ( typeof type , 'string' ) ;
2026-03-12 22:55:28 +05:30
await promiseRetry ( { times : Number . MAX _SAFE _INTEGER , interval : 100 , debug : log , retry : ( error ) => error . reason === BoxError . CONFLICT } , async ( ) => {
2024-12-07 14:35:45 +01:00
const { version , data } = await read ( ) ;
const error = canAcquire ( data , type ) ;
if ( error ) throw error ;
data [ type ] = gTaskId ;
await write ( { version , data } ) ;
2026-03-12 22:55:28 +05:30
log ( ` acquire: ${ type } ` ) ;
2024-12-07 14:35:45 +01:00
} ) ;
}
async function wait ( type ) {
assert . strictEqual ( typeof type , 'string' ) ;
2026-03-12 22:55:28 +05:30
await promiseRetry ( { times : Number . MAX _SAFE _INTEGER , interval : 10000 , debug : log } , async ( ) => await acquire ( type ) ) ;
2024-12-07 14:35:45 +01:00
}
async function release ( type ) {
assert . strictEqual ( typeof type , 'string' ) ;
2026-03-12 22:55:28 +05:30
await promiseRetry ( { times : Number . MAX _SAFE _INTEGER , interval : 100 , debug : log , retry : ( error ) => error . reason === BoxError . CONFLICT } , async ( ) => {
2024-12-07 14:35:45 +01:00
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 } ) ;
2026-03-12 22:55:28 +05:30
log ( ` release: ${ type } ` ) ;
2024-12-07 14:35:45 +01:00
} ) ;
}
async function releaseAll ( ) {
await database . query ( 'DELETE FROM locks' ) ;
await database . query ( 'INSERT INTO locks (id, dataJson) VALUES (?, ?)' , [ 'platform' , JSON . stringify ( { } ) ] ) ;
2026-03-12 22:55:28 +05:30
log ( 'releaseAll: all locks released' ) ;
2024-12-07 14:35:45 +01:00
}
2025-07-18 18:11:56 +02:00
// identify programming errors in tasks that forgot to clean up locks
2024-12-07 14:35:45 +01:00
async function releaseByTaskId ( taskId ) {
assert . strictEqual ( typeof taskId , 'string' ) ;
2026-03-12 22:55:28 +05:30
await promiseRetry ( { times : Number . MAX _SAFE _INTEGER , interval : 100 , debug : log , retry : ( error ) => error . reason === BoxError . CONFLICT } , async ( ) => {
2024-12-07 14:35:45 +01:00
const { version , data } = await read ( ) ;
for ( const type of Object . keys ( data ) ) {
if ( data [ type ] === taskId ) {
2026-03-12 22:55:28 +05:30
log ( ` releaseByTaskId: task ${ taskId } forgot to unlock ${ type } ` ) ;
2024-12-07 14:35:45 +01:00
delete data [ type ] ;
}
}
await write ( { version , data } ) ;
2026-03-12 22:55:28 +05:30
log ( ` releaseByTaskId: ${ taskId } ` ) ;
2024-12-07 14:35:45 +01:00
} ) ;
}
2026-02-14 15:43:24 +01:00
export default {
setTaskId ,
acquire ,
wait ,
release ,
releaseAll ,
releaseByTaskId ,
TYPE _APP _TASK _PREFIX ,
TYPE _APP _BACKUP _PREFIX ,
TYPE _BOX _UPDATE ,
TYPE _BOX _UPDATE _TASK ,
TYPE _FULL _BACKUP _TASK _PREFIX ,
TYPE _MAIL _SERVER _RESTART : 'mail_restart' ,
} ;