Locking expiration date proposal
MODIFY
src/locking/LockingManager.ts
src/locking/LockingManager.ts
@@ -7,7 +7,6 @@ import Logging from '../utils/Logging';
import { ServerAction } from '../types/Server';
import Utils from '../utils/Utils';
import chalk from 'chalk';
-import moment from 'moment';
const MODULE_NAME = 'LockingManager';
@@ -17,34 +16,15 @@ const MODULE_NAME = 'LockingManager';
* - E = mutually exclusive
*/
export default class LockingManager {
- public static createExclusiveLock(tenantID: string, entity: LockEntity, key: string, expirationTimeSeconds?: number): Lock {
- return this.createLock(tenantID, entity, key, LockType.EXCLUSIVE, expirationTimeSeconds);
+ public static createExclusiveLock(tenantID: string, entity: LockEntity, key: string, lockValiditySecs?: number): Lock {
+ return this.createLock(tenantID, entity, key, LockType.EXCLUSIVE, lockValiditySecs);
}
- public static async acquire(lock: Lock, timeout?: number): Promise<boolean> {
+ public static async acquire(lock: Lock, timeoutMs?: number, retry = true): Promise<boolean> {
try {
switch (lock.type) {
case LockType.EXCLUSIVE:
- if (timeout > 0) {
- let timeoutReached = false;
- setTimeout(() => {
- timeoutReached = true;
- }, timeout);
- // Busy loop tries
- while (!timeoutReached) {
- try {
- await LockingStorage.insertLock(lock);
- break;
- } catch {
- await Utils.sleep(1000);
- }
- }
- if (timeoutReached) {
- throw Error(`Lock acquisition timeout ${timeout}ms reached`);
- }
- } else {
- await LockingStorage.insertLock(lock);
- }
+ await LockingManager.acquireExclusiveLock(lock, timeoutMs);
break;
default:
throw new BackendError({
@@ -61,44 +41,20 @@ export default class LockingManager {
message: `Acquired successfully the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}'`,
detailedMessages: { lock }
});
- Utils.isDevelopmentEnv() && console.debug(chalk.green(`Acquire the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID '${lock.tenantID}'`));
+ Utils.isDevelopmentEnv() && console.debug(chalk.green(`Acquired successfully the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID '${lock.tenantID}'`));
return true;
} catch (error) {
- await Logging.logWarning({
- tenantID: lock.tenantID,
- module: MODULE_NAME, method: 'acquire',
- action: ServerAction.LOCKING,
- message: `Cannot acquire the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID}`,
- detailedMessages: { lock, error: error.message, stack: error.stack }
- });
// Check if specific lock for the asset has an expiration date
- if (lock.expirationDate) {
- // Check if lock is existing with same ID:
- const lockInDB = await LockingStorage.getLock(lock.id);
- // Check if log is existing and expired
- if (lockInDB?.expirationDate && moment().isAfter(lockInDB.expirationDate)) {
- try {
- await LockingManager.release(lockInDB);
+ if (retry && await LockingManager.checkAndReleaseExpiredLock(lock)) {
+ return LockingManager.acquire(lock, timeoutMs, false);
+ }
await Logging.logWarning({
tenantID: lock.tenantID,
module: MODULE_NAME, method: 'acquire',
action: ServerAction.LOCKING,
- message: `The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} is expired. It was released automatically`,
- detailedMessages: { lock, error: error.message, stack: error.stack }
- });
- await LockingStorage.insertLock(lock);
- return true;
- } catch {
- await Logging.logError({
- tenantID: lock.tenantID,
- module: MODULE_NAME, method: 'acquire',
- action: ServerAction.LOCKING,
- message: `The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} is expired. It was not possible to release it automatically`,
+ message: `Cannot acquire the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID}`,
detailedMessages: { lock, error: error.message, stack: error.stack }
});
- }
- }
- }
Utils.isDevelopmentEnv() && console.error(chalk.red(`Cannot acquire the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID '${lock.tenantID}'`));
return false;
}
@@ -135,7 +91,7 @@ export default class LockingManager {
}
}
- private static createLock(tenantID: string, entity: LockEntity, key: string, type: LockType = LockType.EXCLUSIVE, expirationTimeSeconds?: number): Lock {
+ private static createLock(tenantID: string, entity: LockEntity, key: string, type: LockType = LockType.EXCLUSIVE, lockValiditySecs?: number): Lock {
if (!tenantID) {
throw new BackendError({
action: ServerAction.LOCKING,
@@ -171,10 +127,64 @@ export default class LockingManager {
hostname: Utils.getHostname()
};
// Set expiration date
- if (expirationTimeSeconds > 0) {
- lock.expirationDate = moment(lock.timestamp).add(expirationTimeSeconds, 'seconds').toDate();
+ if (lockValiditySecs > 0) {
+ lock.expirationDate = new Date(lock.timestamp.getTime() + (lockValiditySecs * 1000));
}
// Return the built lock
return lock;
}
+
+ private static async acquireExclusiveLock(lock: Lock, timeoutMs: number) {
+ if (timeoutMs > 0) {
+ let timeoutReached = false;
+ setTimeout(() => {
+ timeoutReached = true;
+ }, timeoutMs);
+ // Busy loop tries
+ while (!timeoutReached) {
+ try {
+ await LockingStorage.insertLock(lock);
+ break;
+ } catch {
+ await Utils.sleep(1000);
+ }
+ }
+ if (timeoutReached) {
+ throw Error(`Lock acquisition timeout ${timeoutMs}ms reached`);
+ }
+ } else {
+ await LockingStorage.insertLock(lock);
+ }
+ }
+
+ private static async checkAndReleaseExpiredLock(lock: Lock): Promise<boolean> {
+ // Check if lock is existing with same ID:
+ const lockInDB = await LockingStorage.getLock(lock.id);
+ // Check if log is existing and expired
+ if (lockInDB?.expirationDate && lockInDB.expirationDate.getTime() < new Date().getTime()) {
+ try {
+ // Remove the lock
+ await LockingManager.release(lockInDB);
+ await Logging.logWarning({
+ tenantID: lock.tenantID,
+ module: MODULE_NAME, method: 'acquire',
+ action: ServerAction.LOCKING,
+ message: `The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} has expired and was released successfully`,
+ detailedMessages: { lock }
+ });
+ Utils.isDevelopmentEnv() && console.warn(chalk.yellow(`The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} has expired and was released successfully`));
+ return true;
+ } catch (error) {
+ await Logging.logError({
+ tenantID: lock.tenantID,
+ module: MODULE_NAME, method: 'acquire',
+ action: ServerAction.LOCKING,
+ message: `The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} has expired and cannot be released`,
+ detailedMessages: { lock, error: error.message, stack: error.stack }
+ });
+ Utils.isDevelopmentEnv() && console.error(chalk.red(`The lock '${lock.entity}' ('${lock.key}') of type '${lock.type}' in Tenant ID ${lock.tenantID} has expired and cannot be released`));
+ }
+ }
+ return false;
+ }
}

MODIFY
src/start.ts
src/start.ts
@@ -9,6 +9,7 @@ import Configuration from './utils/Configuration';
import Constants from './utils/Constants';
import I18nManager from './utils/I18nManager';
import JsonCentralSystemServer from './server/ocpp/json/JsonCentralSystemServer';
+import { LockEntity } from './types/Locking';
import LockingManager from './locking/LockingManager';
import Logging from './utils/Logging';
import MigrationConfiguration from './types/configuration/MigrationConfiguration';
@@ -147,6 +148,24 @@ export default class Bootstrap {
// -------------------------------------------------------------------------
// 1 - Charging station templates
await ChargingStationStorage.updateChargingStationTemplatesFromFile();
+
+ let lock = LockingManager.createExclusiveLock(Constants.DEFAULT_TENANT, LockEntity.ASSET, '123456', 1);
+ console.log('Acquire Lock should be OK');
+ console.log(await LockingManager.acquire(lock));
+ await Utils.sleep(2000);
+
+ console.log('Acquire Lock should be OK with expiration date');
+ lock = LockingManager.createExclusiveLock(Constants.DEFAULT_TENANT, LockEntity.ASSET, '123456', 1);
+ console.log(await LockingManager.acquire(lock));
+
+ console.log('Acquire Lock should be KO');
+ lock = LockingManager.createExclusiveLock(Constants.DEFAULT_TENANT, LockEntity.ASSET, '123456', 1);
+ console.log(await LockingManager.acquire(lock));
+ await Utils.sleep(2000);
+
+ console.log('Acquire Lock should be OK with expiration date');
+ lock = LockingManager.createExclusiveLock(Constants.DEFAULT_TENANT, LockEntity.ASSET, '123456', 1);
+ console.log(await LockingManager.acquire(lock));
}
} catch (error) {
// Log

MODIFY
src/storage/mongodb/LockingStorage.ts
src/storage/mongodb/LockingStorage.ts
import global, { FilterParams } from '../../types/GlobalType';
import Constants from '../../utils/Constants';
+import { DataResult } from '../../types/DataResult';
import DatabaseUtils from './DatabaseUtils';
import DbParams from '../../types/database/DbParams';
import Lock from '../../types/Locking';
@@ -10,7 +11,7 @@ import Utils from '../../utils/Utils';
const MODULE_NAME = 'LockingStorage';
export default class LockingStorage {
- public static async getLocks(params: { id?: string; }, dbParams: DbParams): Promise<Lock[]> {
+ public static async getLocks(params: { lockIDs?: string[]; }, dbParams: DbParams): Promise<DataResult<Lock>> {
// Debug
const uniqueTimerID = Logging.traceStart(Constants.DEFAULT_TENANT, MODULE_NAME, 'getLocks');
// Clone before updating the values
@@ -21,8 +22,8 @@ export default class LockingStorage {
dbParams.skip = Utils.checkRecordSkip(dbParams.skip);
// Query by Lock id
const filters: FilterParams = {};
- if (params.id) {
- filters._id = params.id;
+ if (params.lockIDs) {
+ filters._id = { $in: params.lockIDs };
}
const aggregation = [];
if (filters) {
@@ -30,6 +31,26 @@ export default class LockingStorage {
$match: filters
});
}
+ // Limit records?
+ if (!dbParams.onlyRecordCount) {
+ // Always limit the nbr of record to avoid perfs issues
+ aggregation.push({ $limit: Constants.DB_RECORD_COUNT_CEIL });
+ }
+ // Count Records
+ const logsCountMDB = await global.database.getCollection<any>(Constants.DEFAULT_TENANT, 'locks')
+ .aggregate([...aggregation, { $count: 'count' }], { allowDiskUse: true })
+ .toArray();
+ // Check if only the total count is requested
+ if (dbParams.onlyRecordCount) {
+ // Return only the count
+ await Logging.traceEnd(Constants.DEFAULT_TENANT, MODULE_NAME, 'getLocks', uniqueTimerID, logsCountMDB);
+ return {
+ count: (logsCountMDB.length > 0 ? logsCountMDB[0].count : 0),
+ result: []
+ };
+ }
+ // Remove the limit
+ aggregation.pop();
// Handle the ID
DatabaseUtils.pushRenameDatabaseID(aggregation);
DatabaseUtils.pushConvertObjectIDToString(aggregation, 'tenantID');
@@ -48,12 +69,19 @@ export default class LockingStorage {
// Debug
await Logging.traceEnd(Constants.DEFAULT_TENANT, MODULE_NAME, 'getLocks', uniqueTimerID, locksMDB);
// Ok
- return locksMDB;
+ return {
+ count: (logsCountMDB.length > 0 ?
+ (logsCountMDB[0].count === Constants.DB_RECORD_COUNT_CEIL ? -1 : logsCountMDB[0].count) : 0),
+ result: locksMDB
+ };
}
- public static async getLock(id: string): Promise<Lock> {
- const locksMDB = await LockingStorage.getLocks({ id: id }, Constants.DB_PARAMS_SINGLE_RECORD);
- return locksMDB[0] ?? null;
+ public static async getLock(lockID: string): Promise<Lock> {
+ const locksMDB = await LockingStorage.getLocks(
+ { lockIDs: [lockID] },
+ Constants.DB_PARAMS_SINGLE_RECORD
+ );
+ return locksMDB.count === 1 ? locksMDB.result[0] : null;
}
public static async insertLock(lockToSave: Lock): Promise<void> {

Locking expiration date proposal