2024-12-07 14:35:45 +01:00
'use strict' ;
exports = module . exports = {
setTaskId ,
acquire ,
wait ,
release ,
releaseAll ,
releaseByTaskId ,
2025-07-18 13:22:33 +02:00
TYPE _APP _TASK _PREFIX : 'app_task_' ,
2025-07-18 10:56:52 +02:00
TYPE _APP _BACKUP _PREFIX : 'app_backup_' ,
2025-07-18 13:22:33 +02:00
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 : 'full_backup_task' , // for scheduling the backup task
2024-12-16 22:34:50 +01:00
TYPE _MAIL _SERVER _RESTART : 'mail_restart' ,
2024-12-07 14:35:45 +01:00
} ;
const assert = require ( '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' ) ;
2025-07-18 13:22:33 +02:00
if ( type === exports . TYPE _BOX _UPDATE ) {
2025-07-18 10:56:52 +02:00
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' ) ;
2025-07-18 13:22:33 +02:00
} 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 === exports . TYPE _FULL _BACKUP _TASK ) {
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 ( exports . TYPE _FULL _BACKUP _TASK in data ) return new BoxError ( BoxError . BAD _STATE , 'Backup task is active' ) ;
2024-12-07 14:35:45 +01:00
}
2025-07-18 10:56:52 +02:00
// TYPE_MAIL_SERVER_RESTART can co-run with everything
2024-12-07 14:35:45 +01:00
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 ( ) ;
if ( type in data ) throw new BoxError ( BoxError . BAD _STATE , ` Locked by ${ data [ type ] } ` ) ;
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' ) ;
}
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 } ` ) ;
} ) ;
}