added expiration property and checks to locks
MODIFY
src/locking/LockingHelper.ts
src/locking/LockingHelper.ts
@@ -8,14 +8,6 @@ import OICPEndpoint from '../types/oicp/OICPEndpoint';
import SiteArea from '../types/SiteArea';
export default class LockingHelper {
- public static async tryCreateSiteAreaSmartChargingLock(tenantID: string, siteArea: SiteArea, timeout: number): Promise<Lock | null> {
- const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.SITE_AREA, `${siteArea.id}-smart-charging`);
- if (!(await LockingManager.tryAcquire(lock, timeout))) {
- return null;
- }
- return lock;
- }
-
public static async createAsyncTaskLock(tenantID: string, asyncTask: AsyncTask): Promise<Lock | null> {
const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.ASYNC_TASK, `${asyncTask.id}`);
if (!(await LockingManager.acquire(lock))) {
@@ -24,9 +16,9 @@ export default class LockingHelper {
return lock;
}
- public static async createSiteAreaSmartChargingLock(tenantID: string, siteArea: SiteArea): Promise<Lock | null> {
+ public static async createSiteAreaSmartChargingLock(tenantID: string, siteArea: SiteArea, timeout: number): Promise<Lock | null> {
const lock = LockingManager.createExclusiveLock(tenantID, LockEntity.SITE_AREA, `${siteArea.id}-smart-charging`);
- if (!(await LockingManager.acquire(lock))) {
+ if (!(await LockingManager.acquire(lock, timeout))) {
return null;
}
return lock;

MODIFY
src/locking/LockingManager.ts
src/locking/LockingManager.ts
@@ -7,6 +7,7 @@ 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';
@@ -20,50 +21,15 @@ export default class LockingManager {
return this.createLock(tenantID, entity, key, LockType.EXCLUSIVE);
}
- public static async acquire(lock: Lock): Promise<boolean> {
+ public static async acquire(lock: Lock, timeout?: number): Promise<boolean> {
try {
switch (lock.type) {
case LockType.EXCLUSIVE:
- await LockingStorage.insertLock(lock);
- break;
- default:
- throw new BackendError({
- action: ServerAction.LOCKING,
- module: MODULE_NAME, method: 'acquire',
- message: `Cannot acquire a lock entity '${lock.entity}' ('${lock.key}') with an unknown type '${lock.type}'`,
- detailedMessages: { lock }
- });
- }
- await Logging.logDebug({
- tenantID: lock.tenantID,
- module: MODULE_NAME, method: 'acquire',
- action: ServerAction.LOCKING,
- 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}'`));
- 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 }
- });
- 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;
- }
- }
-
- public static async tryAcquire(lock: Lock, timeout: number): Promise<boolean> {
+ if (timeout > 0) {
let timeoutReached = false;
setTimeout(() => {
timeoutReached = true;
}, timeout);
- try {
- switch (lock.type) {
- case LockType.EXCLUSIVE:
// Busy loop tries
while (!timeoutReached) {
try {
@@ -76,18 +42,21 @@ export default class LockingManager {
if (timeoutReached) {
throw Error(`Lock acquisition timeout ${timeout}ms reached`);
}
+ } else {
+ await LockingStorage.insertLock(lock);
+ }
break;
default:
throw new BackendError({
action: ServerAction.LOCKING,
- module: MODULE_NAME, method: 'tryAcquire',
+ module: MODULE_NAME, method: 'acquire',
message: `Cannot acquire a lock entity '${lock.entity}' ('${lock.key}') with an unknown type '${lock.type}'`,
detailedMessages: { lock }
});
}
await Logging.logDebug({
tenantID: lock.tenantID,
- module: MODULE_NAME, method: 'tryAcquire',
+ module: MODULE_NAME, method: 'acquire',
action: ServerAction.LOCKING,
message: `Acquired successfully the lock entity '${lock.entity}' ('${lock.key}') of type '${lock.type}'`,
detailedMessages: { lock }
@@ -97,11 +66,38 @@ export default class LockingManager {
} catch (error) {
await Logging.logWarning({
tenantID: lock.tenantID,
- module: MODULE_NAME, method: 'tryAcquire',
+ 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);
+ 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 }
+ });
+ } 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`,
+ detailedMessages: { lock, error: error.message, stack: error.stack }
+ });
+ // Send notification to admins (To be discussed)
+ }
+ }
+ }
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;
}
@@ -138,7 +134,7 @@ export default class LockingManager {
}
}
- private static createLock(tenantID: string, entity: LockEntity, key: string, type: LockType = LockType.EXCLUSIVE): Lock {
+ private static createLock(tenantID: string, entity: LockEntity, key: string, type: LockType = LockType.EXCLUSIVE, timeout?: number): Lock {
if (!tenantID) {
throw new BackendError({
action: ServerAction.LOCKING,
@@ -163,8 +159,8 @@ export default class LockingManager {
detailedMessages: { tenantID, entity, key, type }
});
}
- // Return the built lock
- return {
+ // Build lock
+ const lock: Lock = {
id: Cypher.hash(`${tenantID}~${entity}~${key.toLowerCase()}~${type}`),
tenantID,
entity: entity,
@@ -173,5 +169,11 @@ export default class LockingManager {
timestamp: new Date(),
hostname: Utils.getHostname()
};
+ // Set expiration date
+ if (timeout > 0) {
+ lock.expirationDate = moment(lock.timestamp).add(timeout, 'seconds').toDate();
+ }
+ // Return the built lock
+ return lock;
}
}

MODIFY
src/scheduler/tasks/CheckAndComputeSmartChargingTask.ts
src/scheduler/tasks/CheckAndComputeSmartChargingTask.ts
@@ -22,7 +22,7 @@ export default class CheckAndComputeSmartChargingTask extends SchedulerTask {
Constants.DB_PARAMS_MAX_LIMIT);
// Get Site Area
for (const siteArea of siteAreas.result) {
- const siteAreaLock = await LockingHelper.tryCreateSiteAreaSmartChargingLock(tenant.id, siteArea, 30 * 1000);
+ const siteAreaLock = await LockingHelper.createSiteAreaSmartChargingLock(tenant.id, siteArea, 30 * 1000);
if (siteAreaLock) {
try {
// Get implementation

MODIFY
src/server/ocpp/services/OCPPService.ts
src/server/ocpp/services/OCPPService.ts
@@ -1936,7 +1936,7 @@ export default class OCPPService {
// Get Site Area
const siteArea = await SiteAreaStorage.getSiteArea(tenant.id, chargingStation.siteAreaID);
if (siteArea && siteArea.smartCharging) {
- const siteAreaLock = await LockingHelper.tryCreateSiteAreaSmartChargingLock(tenant.id, siteArea, 30 * 1000);
+ const siteAreaLock = await LockingHelper.createSiteAreaSmartChargingLock(tenant.id, siteArea, 30 * 1000);
if (siteAreaLock) {
try {
const smartCharging = await SmartChargingFactory.getSmartChargingImpl(tenant.id);

MODIFY
src/server/rest/v1/service/ChargingStationService.ts
src/server/rest/v1/service/ChargingStationService.ts
@@ -564,7 +564,7 @@ export default class ChargingStationService {
user: req.user
});
}
- const siteAreaLock = await LockingHelper.tryCreateSiteAreaSmartChargingLock(req.user.tenantID, siteArea, 30 * 1000);
+ const siteAreaLock = await LockingHelper.createSiteAreaSmartChargingLock(req.user.tenantID, siteArea, 30 * 1000);
if (siteAreaLock) {
try {
// Call

MODIFY
src/server/rest/v1/service/SiteAreaService.ts
src/server/rest/v1/service/SiteAreaService.ts
@@ -370,7 +370,7 @@ export default class SiteAreaService {
// FIXME: the lock acquisition can wait for 30s before timeout and the whole code execution timeout at 3s
// eslint-disable-next-line @typescript-eslint/no-misused-promises
setTimeout(async () => {
- const siteAreaLock = await LockingHelper.tryCreateSiteAreaSmartChargingLock(req.user.tenantID, siteArea, 30 * 1000);
+ const siteAreaLock = await LockingHelper.createSiteAreaSmartChargingLock(req.user.tenantID, siteArea, 30 * 1000);
if (siteAreaLock) {
try {
const smartCharging = await SmartChargingFactory.getSmartChargingImpl(req.user.tenantID);

MODIFY
src/storage/mongodb/LockingStorage.ts
src/storage/mongodb/LockingStorage.ts
+import global, { FilterParams } from '../../types/GlobalType';
+
import Constants from '../../utils/Constants';
import DatabaseUtils from './DatabaseUtils';
+import DbParams from '../../types/database/DbParams';
import Lock from '../../types/Locking';
import Logging from '../../utils/Logging';
import Utils from '../../utils/Utils';
-import global from '../../types/GlobalType';
const MODULE_NAME = 'LockingStorage';
export default class LockingStorage {
- public static async getLocks(): Promise<Lock[]> {
+ public static async getLocks(params: { id?: string; }, dbParams: DbParams): Promise<Lock[]> {
// Debug
const uniqueTimerID = Logging.traceStart(Constants.DEFAULT_TENANT, MODULE_NAME, 'getLocks');
+ // Clone before updating the values
+ dbParams = Utils.cloneObject(dbParams);
+ // Check Limit
+ dbParams.limit = Utils.checkRecordLimit(dbParams.limit);
+ // Check Skip
+ dbParams.skip = Utils.checkRecordSkip(dbParams.skip);
+ // Query by Lock id
+ const filters: FilterParams = {};
+ if (params.id) {
+ filters._id = params.id;
+ }
const aggregation = [];
+ if (filters) {
+ aggregation.push({
+ $match: filters
+ });
+ }
// Handle the ID
DatabaseUtils.pushRenameDatabaseID(aggregation);
DatabaseUtils.pushConvertObjectIDToString(aggregation, 'tenantID');
+ // Skip
+ aggregation.push({
+ $skip: dbParams.skip
+ });
+ // Limit
+ aggregation.push({
+ $limit: dbParams.limit
+ });
// Read DB
const locksMDB = await global.database.getCollection<Lock>(Constants.DEFAULT_TENANT, 'locks')
.aggregate(aggregation)
@@ -25,6 +51,13 @@ export default class LockingStorage {
return 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 insertLock(lockToSave: Lock): Promise<void> {
// Debug
const uniqueTimerID = Logging.traceStart(Constants.DEFAULT_TENANT, MODULE_NAME, 'insertLock');

MODIFY
src/types/Locking.ts
src/types/Locking.ts
@@ -6,6 +6,7 @@ export default interface Lock {
key: string;
type: LockType;
timestamp: Date;
+ expirationDate?: Date;
hostname: string;
}

MODIFY
test/api/LockingTest.ts
test/api/LockingTest.ts
@@ -100,7 +100,7 @@ describe('Locking Tests', function() {
// Get the Site Area
const siteArea = testData.siteAreaContext.getSiteArea();
// Create and Acquire lock
- testData.siteExclusiveLock = await LockingHelper.tryCreateSiteAreaSmartChargingLock(
+ testData.siteExclusiveLock = await LockingHelper.createSiteAreaSmartChargingLock(
testData.tenantContext.getTenant().id, siteArea, 30 * 1000);
expect(testData.siteExclusiveLock).not.null;
expect(testData.siteExclusiveLock.id).not.null;

added expiration property and checks to locks