feat: ensure charging station add op return its station info
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 20 Mar 2024 19:29:37 +0000 (20:29 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 20 Mar 2024 19:29:37 +0000 (20:29 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
README.md
src/charging-station/Bootstrap.ts
src/charging-station/ChargingStationWorker.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/types/ChargingStationWorker.ts
src/worker/WorkerAbstract.ts
src/worker/WorkerDynamicPool.ts
src/worker/WorkerFactory.ts
src/worker/WorkerFixedPool.ts
src/worker/WorkerSet.ts

index 739f33f31ac705f26bdaa3305ccc7f1046d97c67..8f55b2cc72a66336d44af2ef446e056e69badfe4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -638,6 +638,8 @@ Set the Websocket header _Sec-Websocket-Protocol_ to `ui0.0.1`.
 - Response:  
   `PDU`: {  
    `status`: 'success' | 'failure'  
+   `hashIdsSucceeded`: charging station unique identifier strings array (optional),  
+   `hashIdsFailed`: charging station unique identifier strings array (optional)  
   }
 
 ###### Delete Charging Stations
index 34ac90f3f3e4ba2e211d47ef3639907e5ccff066..4f72d6dfdffef0ca7a65cb24a2103f19558ced6f 100644 (file)
@@ -15,6 +15,7 @@ import { BaseError } from '../exception/index.js'
 import { type Storage, StorageFactory } from '../performance/index.js'
 import {
   type ChargingStationData,
+  type ChargingStationInfo,
   type ChargingStationOptions,
   type ChargingStationWorkerData,
   type ChargingStationWorkerEventError,
@@ -59,7 +60,7 @@ enum exitCodes {
 
 export class Bootstrap extends EventEmitter {
   private static instance: Bootstrap | null = null
-  private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
+  private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
   private readonly uiServer: AbstractUIServer
   private storage?: Storage
   private readonly templateStatistics: Map<string, TemplateStatistics>
@@ -346,7 +347,10 @@ export class Bootstrap extends EventEmitter {
             : 1
         break
     }
-    this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
+    this.workerImplementation = WorkerFactory.getWorkerImplementation<
+    ChargingStationWorkerData,
+    ChargingStationInfo
+    >(
       join(
         dirname(fileURLToPath(import.meta.url)),
         `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
@@ -541,13 +545,13 @@ export class Bootstrap extends EventEmitter {
     index: number,
     templateFile: string,
     options?: ChargingStationOptions
-  ): Promise<void> {
+  ): Promise<ChargingStationInfo | undefined> {
     if (!this.started && !this.starting) {
       throw new BaseError(
         'Cannot add charging station while the charging stations simulator is not started'
       )
     }
-    await this.workerImplementation?.addElement({
+    const stationInfo = await this.workerImplementation?.addElement({
       index,
       templateFile: join(
         dirname(fileURLToPath(import.meta.url)),
@@ -561,6 +565,7 @@ export class Bootstrap extends EventEmitter {
     const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))!
     ++templateStatistics.added
     templateStatistics.indexes.add(index)
+    return stationInfo
   }
 
   private gracefulShutdown (): void {
index 35a7e534353f5ffaafbae6daba93948e9803d407..042d3d5bfc2b3bc913484c5a0f5d0982b982148f 100644 (file)
@@ -6,23 +6,24 @@ import { ThreadWorker } from 'poolifier'
 
 import { BaseError } from '../exception/index.js'
 import type {
-  ChargingStationData,
+  ChargingStationInfo,
   ChargingStationWorkerData,
   ChargingStationWorkerEventError,
   ChargingStationWorkerMessage
 } from '../types/index.js'
-import { buildChargingStationDataPayload, Configuration } from '../utils/index.js'
+import { Configuration } from '../utils/index.js'
 import { type WorkerMessage, WorkerMessageEvents } from '../worker/index.js'
 import { ChargingStation } from './ChargingStation.js'
 
 export let chargingStationWorker: object
 if (Configuration.workerPoolInUse()) {
-  chargingStationWorker = new ThreadWorker<ChargingStationWorkerData>(
-    (data?: ChargingStationWorkerData): void => {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, no-new
-      new ChargingStation(data!.index, data!.templateFile, data!.options)
-    }
-  )
+  chargingStationWorker = new ThreadWorker<
+  ChargingStationWorkerData,
+  ChargingStationInfo | undefined
+  >((data?: ChargingStationWorkerData): ChargingStationInfo | undefined => {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, no-new
+    return new ChargingStation(data!.index, data!.templateFile, data!.options).stationInfo
+  })
 } else {
   // eslint-disable-next-line @typescript-eslint/no-extraneous-class
   class ChargingStationWorker<Data extends ChargingStationWorkerData> {
@@ -38,13 +39,14 @@ if (Configuration.workerPoolInUse()) {
               )
               parentPort?.postMessage({
                 event: WorkerMessageEvents.addedWorkerElement,
-                data: buildChargingStationDataPayload(chargingStation)
-              } satisfies ChargingStationWorkerMessage<ChargingStationData>)
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                data: chargingStation.stationInfo!
+              } satisfies ChargingStationWorkerMessage<ChargingStationInfo>)
             } catch (error) {
               parentPort?.postMessage({
                 event: WorkerMessageEvents.workerElementError,
                 data: {
-                  event: WorkerMessageEvents.addWorkerElement,
+                  event: message.event,
                   name: (error as Error).name,
                   message: (error as Error).message,
                   stack: (error as Error).stack
index 6aed70a1b555cfcf5718cc153d02edc3e25da894..6652def2ad2f64e5a43cf30fb1ab0831a9dfdcda 100644 (file)
@@ -2,6 +2,7 @@ import { BaseError, type OCPPError } from '../../../exception/index.js'
 import {
   BroadcastChannelProcedureName,
   type BroadcastChannelRequestPayload,
+  type ChargingStationInfo,
   type ChargingStationOptions,
   ConfigurationSection,
   type JsonObject,
@@ -275,23 +276,30 @@ export abstract class AbstractUIService {
         errorMessage: `Template '${template}' not found`
       } satisfies ResponsePayload
     }
+    const stationInfos: ChargingStationInfo[] = []
     for (let i = 0; i < numberOfStations; i++) {
+      let stationInfo: ChargingStationInfo | undefined
       try {
-        await Bootstrap.getInstance().addChargingStation(
+        stationInfo = await Bootstrap.getInstance().addChargingStation(
           Bootstrap.getInstance().getLastIndex(template) + 1,
           `${template}.json`,
           options
         )
+        if (stationInfo != null) {
+          stationInfos.push(stationInfo)
+        }
       } catch (error) {
         return {
           status: ResponseStatus.FAILURE,
+          ...(stationInfo?.hashId != null && { hashIdsFailed: [stationInfo.hashId] }),
           errorMessage: (error as Error).message,
           errorStack: (error as Error).stack
         } satisfies ResponsePayload
       }
     }
     return {
-      status: ResponseStatus.SUCCESS
+      status: ResponseStatus.SUCCESS,
+      hashIdsSucceeded: stationInfos.map(stationInfo => stationInfo.hashId)
     }
   }
 
index 861d447ea03be6b9d5146b3233c67b41e65cf379..c6fd0af2afdb84bb83cd354a4e00f8a3369d85c8 100644 (file)
@@ -70,6 +70,7 @@ export interface ChargingStationWorkerEventError extends WorkerData {
 }
 
 export type ChargingStationWorkerMessageData =
+  | ChargingStationInfo
   | ChargingStationData
   | Statistics
   | ChargingStationWorkerEventError
index 907d0bd0e580273bbba8f54c60cfd0b73d4a6c7c..f6713f8976d2cdda47d74f5951880f8d1fdfd48d 100644 (file)
@@ -5,7 +5,7 @@ import type { PoolInfo } from 'poolifier'
 
 import type { SetInfo, WorkerData, WorkerOptions } from './WorkerTypes.js'
 
-export abstract class WorkerAbstract<T extends WorkerData> {
+export abstract class WorkerAbstract<D extends WorkerData, R extends WorkerData> {
   protected readonly workerScript: string
   protected readonly workerOptions: WorkerOptions
   public abstract readonly info: PoolInfo | SetInfo
@@ -49,5 +49,5 @@ export abstract class WorkerAbstract<T extends WorkerData> {
    *
    * @param elementData -
    */
-  public abstract addElement (elementData: T): Promise<void>
+  public abstract addElement (elementData: D): Promise<R>
 }
index 2043297318c38e8d298a76c9d660b4d215bc8d71..5217d7fe16a6772a81b946969702fe1189f0d5dc 100644 (file)
@@ -6,8 +6,11 @@ import { WorkerAbstract } from './WorkerAbstract.js'
 import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
-export class WorkerDynamicPool extends WorkerAbstract<WorkerData> {
-  private readonly pool: DynamicThreadPool<WorkerData>
+export class WorkerDynamicPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
+D,
+R
+> {
+  private readonly pool: DynamicThreadPool<D, R>
 
   /**
    * Creates a new `WorkerDynamicPool`.
@@ -17,7 +20,7 @@ export class WorkerDynamicPool extends WorkerAbstract<WorkerData> {
    */
   constructor (workerScript: string, workerOptions: WorkerOptions) {
     super(workerScript, workerOptions)
-    this.pool = new DynamicThreadPool<WorkerData>(
+    this.pool = new DynamicThreadPool<D, R>(
       this.workerOptions.poolMinSize,
       this.workerOptions.poolMaxSize,
       this.workerScript,
@@ -52,12 +55,13 @@ export class WorkerDynamicPool extends WorkerAbstract<WorkerData> {
   }
 
   /** @inheritDoc */
-  public async addElement (elementData: WorkerData): Promise<void> {
-    await this.pool.execute(elementData)
+  public async addElement (elementData: D): Promise<R> {
+    const response = await this.pool.execute(elementData)
     // Start element sequentially to optimize memory at startup
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     this.workerOptions.elementAddDelay! > 0 &&
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+    return response
   }
 }
index 67da00d88a5b2d32572fa2f8a5f422fe3b73a843..73bcd56dc62467c7bb942157d88554014b8240af 100644 (file)
@@ -15,30 +15,25 @@ export class WorkerFactory {
     // This is intentional
   }
 
-  public static getWorkerImplementation<T extends WorkerData>(
+  public static getWorkerImplementation<D extends WorkerData, R extends WorkerData>(
     workerScript: string,
     workerProcessType: WorkerProcessType,
     workerOptions?: WorkerOptions
-  ): WorkerAbstract<T> | undefined {
+  ): WorkerAbstract<D, R> {
     if (!isMainThread) {
       throw new Error('Cannot get a worker implementation outside the main thread')
     }
     workerOptions = mergeDeepRight<WorkerOptions>(DEFAULT_WORKER_OPTIONS, workerOptions ?? {})
-    let workerImplementation: WorkerAbstract<T>
     switch (workerProcessType) {
       case WorkerProcessType.workerSet:
-        workerImplementation = new WorkerSet(workerScript, workerOptions)
-        break
+        return new WorkerSet<D, R>(workerScript, workerOptions)
       case WorkerProcessType.fixedPool:
-        workerImplementation = new WorkerFixedPool(workerScript, workerOptions)
-        break
+        return new WorkerFixedPool<D, R>(workerScript, workerOptions)
       case WorkerProcessType.dynamicPool:
-        workerImplementation = new WorkerDynamicPool(workerScript, workerOptions)
-        break
+        return new WorkerDynamicPool<D, R>(workerScript, workerOptions)
       default:
         // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
         throw new Error(`Worker implementation type '${workerProcessType}' not found`)
     }
-    return workerImplementation
   }
 }
index 22290666c17546e1f3c0f461a0cbca39e0841de2..96060854743112a438dc408dcc8285842e1bbd19 100644 (file)
@@ -6,8 +6,11 @@ import { WorkerAbstract } from './WorkerAbstract.js'
 import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
-export class WorkerFixedPool extends WorkerAbstract<WorkerData> {
-  private readonly pool: FixedThreadPool<WorkerData>
+export class WorkerFixedPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
+D,
+R
+> {
+  private readonly pool: FixedThreadPool<D, R>
 
   /**
    * Creates a new `WorkerFixedPool`.
@@ -17,7 +20,7 @@ export class WorkerFixedPool extends WorkerAbstract<WorkerData> {
    */
   constructor (workerScript: string, workerOptions: WorkerOptions) {
     super(workerScript, workerOptions)
-    this.pool = new FixedThreadPool(
+    this.pool = new FixedThreadPool<D, R>(
       this.workerOptions.poolMaxSize,
       this.workerScript,
       this.workerOptions.poolOptions
@@ -51,12 +54,13 @@ export class WorkerFixedPool extends WorkerAbstract<WorkerData> {
   }
 
   /** @inheritDoc */
-  public async addElement (elementData: WorkerData): Promise<void> {
-    await this.pool.execute(elementData)
+  public async addElement (elementData: D): Promise<R> {
+    const response = await this.pool.execute(elementData)
     // Start element sequentially to optimize memory at startup
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     this.workerOptions.elementAddDelay! > 0 &&
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+    return response
   }
 }
index d0f80b6ed59a54342430991ff4d318d86e0a5402..ba2b10add0706b4c43d3a0010544ba06cc41276e 100644 (file)
@@ -16,7 +16,7 @@ import {
 } from './WorkerTypes.js'
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
-export class WorkerSet extends WorkerAbstract<WorkerData> {
+export class WorkerSet<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<D, R> {
   public readonly emitter: EventEmitterAsyncResource | undefined
   private readonly workerSet: Set<WorkerSetElement>
   private started: boolean
@@ -102,22 +102,37 @@ export class WorkerSet extends WorkerAbstract<WorkerData> {
   }
 
   /** @inheritDoc */
-  public async addElement (elementData: WorkerData): Promise<void> {
+  public async addElement (elementData: D): Promise<R> {
     if (!this.started) {
       throw new Error('Cannot add a WorkerSet element: not started')
     }
     const workerSetElement = await this.getWorkerSetElement()
+    const waitForAddedWorkerElement = new Promise<R>((resolve, reject) => {
+      const messageHandler = (message: WorkerMessage<R>): void => {
+        if (message.event === WorkerMessageEvents.addedWorkerElement) {
+          ++workerSetElement.numberOfWorkerElements
+          resolve(message.data)
+          workerSetElement.worker.off('message', messageHandler)
+        } else if (message.event === WorkerMessageEvents.workerElementError) {
+          // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
+          reject(message.data)
+          workerSetElement.worker.off('message', messageHandler)
+        }
+      }
+      workerSetElement.worker.on('message', messageHandler)
+    })
     workerSetElement.worker.postMessage({
       event: WorkerMessageEvents.addWorkerElement,
       data: elementData
     })
-    ++workerSetElement.numberOfWorkerElements
+    const response = await waitForAddedWorkerElement
     // Add element sequentially to optimize memory at startup
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     if (this.workerOptions.elementAddDelay! > 0) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       await sleep(randomizeDelay(this.workerOptions.elementAddDelay!))
     }
+    return response
   }
 
   /**
@@ -130,7 +145,7 @@ export class WorkerSet extends WorkerAbstract<WorkerData> {
       ...this.workerOptions.poolOptions?.workerOptions
     })
     worker.on('message', this.workerOptions.poolOptions?.messageHandler ?? EMPTY_FUNCTION)
-    worker.on('message', (message: WorkerMessage<WorkerData>) => {
+    worker.on('message', (message: WorkerMessage<R>) => {
       if (message.event === WorkerMessageEvents.addedWorkerElement) {
         this.emitter?.emit(WorkerSetEvents.elementAdded, this.info)
       } else if (message.event === WorkerMessageEvents.workerElementError) {