From 6750068cda5b1ef7a4179bd906a00792d844dd16 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 21 May 2026 15:46:47 +0200 Subject: [PATCH 1/7] Extract deployment for calibration in its service and adapt checks --- .../lib/controllers/Deployment.controller.js | 73 +++++++++++ .../lib/controllers/Environment.controller.js | 85 +----------- .../mocha-deployment.controller.js | 124 ++++++++++++++++++ 3 files changed, 198 insertions(+), 84 deletions(-) diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index c9200e6fe..af9a3eb3a 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -19,6 +19,7 @@ const { } = require('@aliceo2/web-ui'); const {User} = require('./../dtos/User.js'); +const LOG_FACILITY = 'cog/deployment-ctrl'; /** * Controller Class for managing deployments via the AliECS system @@ -121,6 +122,78 @@ class DeploymentController { } } + /** + * API - POST endpoint for requesting a new deployment for calibration purposes. + * This is a specific endpoint separated from the generic deployment one as it has specific input requirements and validations. + * @param {Request} req - HTTP Request object which expects a body with the following properties + * @param {string[]} req.body.detectors - list of detectors for which the calibration environment should be deployed. Must contain exactly one detector. + * @param {string} req.body.runType - the type of the calibration run to be performed which determines the workflow template to use + * @param {string} req.body.configurationName - the name of the saved configuration to use for the deployment + * @param {Response} res - HTTP Response object with result of the deployment request + * @returns {void} + */ + async newAsyncDeploymentCalibrationHandler(req, res) { + const {personid, name, username} = req.session; + const user = new User(username, name, personid); + const { detectors, runType, configurationName } = req.body; + + if (detectors?.length !== 1) { + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Exactly one detector must be specified for deployment') + ); + return; + } + const [detector] = detectors; + + if (!configurationName) { + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Missing Configuration Name for deployment') + ); + return; + } + // Retrieve latest configuration version for given name + let variables = undefined; + try { + ({ variables } = await this._workflowService.retrieveWorkflowSavedConfiguration(configurationName)); + if (!variables) { + throw new InvalidInputError(`No configuration variables found for ${configurationName}`); + } + } catch (error) { + this._logger.errorMessage(`Unable to retrieve saved configuration for ${configurationName} due to ${error}`); + updateAndSendExpressResponseFromNativeError(res, error); + return; + } + + // Retrieve latest default workflow to use + let workflowTemplatePath; + try { + const {template, repository, revision} = await this._workflowService.getDefaultTemplateSource(); + workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; + } catch (error) { + this._logger.debug(`Unable to retrieve default workflow template due to ${error}`); + updateAndSendExpressResponseFromNativeError(res, error); + return; + } + // Attempt to deploy environment + try { + this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${configurationName}`, + {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} + ); + const environment = await this._envService.newAutoEnvironment( + workflowTemplatePath, variables, detector, runType, user + ); + res.status(200).json(environment); + } catch (error) { + this._logger.errorMessage( + `Unable to deploy request by username(${username}) for ${configurationName} due to error`, + {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} + ); + updateAndSendExpressResponseFromNativeError(res, error); + } + } + /** * API - DELETE endpoint for acknowledging an environment deployment failure * @param {Request} req - HTTP Request object which expects an `id` as mandatory parameter diff --git a/Control/lib/controllers/Environment.controller.js b/Control/lib/controllers/Environment.controller.js index 1416724a8..8ffa80a80 100644 --- a/Control/lib/controllers/Environment.controller.js +++ b/Control/lib/controllers/Environment.controller.js @@ -12,9 +12,7 @@ * or submit itself to any jurisdiction. */ const {LogManager, LogLevel} = require('@aliceo2/web-ui'); -const { - updateAndSendExpressResponseFromNativeError, InvalidInputError, UnauthorizedAccessError -} = require('@aliceo2/web-ui'); +const {updateAndSendExpressResponseFromNativeError, InvalidInputError} = require('@aliceo2/web-ui'); const LOG_FACILITY = 'cog/env-ctrl'; const {EnvironmentTransitionType} = require('./../common/environmentTransitionType.enum.js'); @@ -165,87 +163,6 @@ class EnvironmentController { this._logger.debug(`DESTROY_ENVIRONMENT,${id},${runNumber},${destroyRequestedAt},${Date.now()}`); } } - - /** - * API - POST endpoint for deploying a new environment based on a given configuration name - * @param {Request} req - HTTP Request object - * @param {Response} res - HTTP Response object with EnvironmentDetails - * @returns {void} - */ - async newAutoEnvironmentHandler(req, res) { - const {personid, name, username} = req.session; - const user = new User(username, name, personid); - const {detector, runType, configurationName} = req.body; - - if (!this._lockService.isLockOwnedByUser(detector, user)) { - updateAndSendExpressResponseFromNativeError(res, new UnauthorizedAccessError('Lock not taken')); - return; - } - - if (!configurationName) { - updateAndSendExpressResponseFromNativeError( - res, - new InvalidInputError('Missing Configuration Name for deployment') - ); - return; - } - - try { - const areDetectorsAvailable = await this._detectorService.areDetectorsAvailable([detector]); - if (!areDetectorsAvailable) { - updateAndSendExpressResponseFromNativeError( - res, - new InvalidInputError(`Detector ${detector} is already active`) - ); - return; - } - } catch (error) { - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - - // Retrieve latest configuration version for given name - let variables; - try { - const configuration = await this._workflowService.retrieveWorkflowSavedConfiguration(configurationName); - if (!configuration.variables) { - throw new InvalidInputError(`No configuration variables found for ${configurationName}`); - } - variables = configuration.variables; - } catch (error) { - this._logger.debug(`Unable to retrieve saved configuration for ${configurationName} due to`); - this._logger.debug(error); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - - // Retrieve latest default workflow to use - let workflowTemplatePath; - try { - const {template, repository, revision} = await this._workflowService.getDefaultTemplateSource(); - workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; - } catch (error) { - this._logger.debug(`Unable to retrieve default workflow template due to ${error}`); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - // Attempt to deploy environment - try { - this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${configurationName}`, - {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} - ); - const environment = await this._envService.newAutoEnvironment( - workflowTemplatePath, variables, detector, runType, user - ); - res.status(200).json(environment); - } catch (error) { - this._logger.errorMessage( - `Unable to deploy request by username(${username}) for ${configurationName} due to error`, - {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} - ); - updateAndSendExpressResponseFromNativeError(res, error); - } - } } module.exports = {EnvironmentController}; diff --git a/Control/test/lib/controllers/mocha-deployment.controller.js b/Control/test/lib/controllers/mocha-deployment.controller.js index bad17c9a4..3f78eaba2 100644 --- a/Control/test/lib/controllers/mocha-deployment.controller.js +++ b/Control/test/lib/controllers/mocha-deployment.controller.js @@ -179,4 +179,128 @@ describe('DeploymentController test suite', function() { assert.ok(res.json.calledWith({ message: 'Environment deployment failure acknowledged' })); }); }); + + describe('newAsyncDeploymentCalibrationHandler - tests', function() { + let mockEnvService; + + beforeEach(function() { + mockWorkflowService.retrieveWorkflowSavedConfiguration = sinon.stub(); + mockEnvService = { newAutoEnvironment: sinon.stub() }; + deploymentController._envService = mockEnvService; + req.body = {}; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub() + }; + }); + + it('should return 400 if detectors list is empty', async function() { + req.body = { detectors: [], configurationName: 'test-config' }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Exactly one detector must be specified for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return 400 if more than one detector is provided', async function() { + req.body = { detectors: ['DET1', 'DET2'], configurationName: 'test-config' }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Exactly one detector must be specified for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return 400 if configurationName is missing', async function() { + req.body = { detectors: ['DET1'] }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Missing Configuration Name for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return error when retrieveWorkflowSavedConfiguration rejects', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.rejects(new Error('Configuration not found')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Configuration not found', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should return 400 when no variables found in the saved configuration', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: null }); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'No configuration variables found for test-config', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return error when getDefaultTemplateSource rejects', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: { key: 'val' } }); + mockWorkflowService.getDefaultTemplateSource.rejects(new Error('Template source unavailable')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Template source unavailable', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should return error when environment deployment fails', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config', runType: 'PHYSICS' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: { key: 'val' } }); + mockWorkflowService.getDefaultTemplateSource.resolves({ template: 'readout', repository: 'repo', revision: '1.0' }); + mockEnvService.newAutoEnvironment.rejects(new Error('Deployment failed')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Deployment failed', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should successfully deploy calibration environment and return 200', async function() { + const variables = { key: 'val' }; + const template = 'readout'; + const repository = 'repo'; + const revision = '1.0'; + const configurationName = 'test-config'; + const detector = 'DET1'; + const runType = 'PHYSICS'; + + req.body = { detectors: [detector], configurationName, runType }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables }); + mockWorkflowService.getDefaultTemplateSource.resolves({ template, repository, revision }); + mockEnvService.newAutoEnvironment.resolves({ id: 'env123' }); + + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + + assert.ok(mockEnvService.newAutoEnvironment.calledOnce); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[0], `${repository}/workflows/${template}@${revision}`); + assert.deepStrictEqual(mockEnvService.newAutoEnvironment.firstCall.args[1], variables); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[2], detector); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[3], runType); + assert.ok(res.status.calledWith(200)); + assert.ok(res.json.calledWith({ id: 'env123' })); + }); + }); }); From 85e457cf2b7f3345edaa001f59d855ed5a033d52 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 16:27:21 +0200 Subject: [PATCH 2/7] Update API for new endpoint and shared middlewares --- Control/lib/api.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Control/lib/api.js b/Control/lib/api.js index ab44267d9..506db71ac 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -179,6 +179,13 @@ module.exports.setup = (http, ws) => { const verifyLockOwnershipMiddleware = getDetectorsLockOwnershipMiddlewareFactory(lockService); const validateConsulServiceMiddleware = validateConsulServiceMiddlewareFactory(consulService); const verifyDetectorsAvailabilityMiddleware = verifyDetectorsAvailabilityMiddlewareFactory(detectorService); + const deploymentMandatoryMiddleware = [ + ...coreMiddleware, + logDeploymentRequestMiddleware, + minimumRoleMiddleware(Role.DETECTOR), + verifyLockOwnershipMiddleware, + verifyDetectorsAvailabilityMiddleware, + ]; ctrlProxy.methods.forEach( (method) => http.post(`/${method}`, coreMiddleware, (req, res) => ctrlService.executeCommand(req, res)), @@ -196,7 +203,7 @@ module.exports.setup = (http, ws) => { http.get('/environments', coreMiddleware, envCtrl.getEnvironmentsHandler.bind(envCtrl), {public: true}); http.get('/environment/:id/:source?', coreMiddleware, envCtrl.getEnvironmentHandler.bind(envCtrl), {public: true}); - http.post('/environment/auto', coreMiddleware, envCtrl.newAutoEnvironmentHandler.bind(envCtrl)); + http.put('/environment/:id', coreMiddleware, minimumRoleMiddleware(Role.DETECTOR), @@ -213,14 +220,8 @@ module.exports.setup = (http, ws) => { envCtrl.destroyEnvironmentHandler.bind(envCtrl), ); - http.post('/deploy', - coreMiddleware, - logDeploymentRequestMiddleware, - minimumRoleMiddleware(Role.DETECTOR), - verifyLockOwnershipMiddleware, - verifyDetectorsAvailabilityMiddleware, - deploymentController.newAsyncDeploymentHandler.bind(deploymentController) - ); + http.post('/deploy', deploymentMandatoryMiddleware, deploymentController.newAsyncDeploymentHandler.bind(deploymentController)); + http.post('/deploy/calibration', deploymentMandatoryMiddleware, deploymentController.newAsyncDeploymentCalibrationHandler.bind(deploymentController)); http.delete('/deploy/:id', minimumRoleMiddleware(Role.DETECTOR), From 55e3c197f1c9348229bb9c69d30431f013d1e660 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 16:29:07 +0200 Subject: [PATCH 3/7] Controller to only check input and let high-level service do rest of data needed gathering --- .../lib/controllers/Deployment.controller.js | 40 +++++-------------- .../CalibrationRuns/CalibrationRuns.model.js | 2 +- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index af9a3eb3a..182bf1f9e 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -135,7 +135,7 @@ class DeploymentController { async newAsyncDeploymentCalibrationHandler(req, res) { const {personid, name, username} = req.session; const user = new User(username, name, personid); - const { detectors, runType, configurationName } = req.body; + const { detectors, runType, selectedConfiguration } = req.body; if (detectors?.length !== 1) { updateAndSendExpressResponseFromNativeError( @@ -146,48 +146,28 @@ class DeploymentController { } const [detector] = detectors; - if (!configurationName) { + if (!selectedConfiguration) { updateAndSendExpressResponseFromNativeError( res, new InvalidInputError('Missing Configuration Name for deployment') ); return; } - // Retrieve latest configuration version for given name - let variables = undefined; - try { - ({ variables } = await this._workflowService.retrieveWorkflowSavedConfiguration(configurationName)); - if (!variables) { - throw new InvalidInputError(`No configuration variables found for ${configurationName}`); - } - } catch (error) { - this._logger.errorMessage(`Unable to retrieve saved configuration for ${configurationName} due to ${error}`); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - - // Retrieve latest default workflow to use - let workflowTemplatePath; - try { - const {template, repository, revision} = await this._workflowService.getDefaultTemplateSource(); - workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; - } catch (error) { - this._logger.debug(`Unable to retrieve default workflow template due to ${error}`); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } + // Attempt to deploy environment try { - this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${configurationName}`, + this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${selectedConfiguration}`, {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} ); - const environment = await this._envService.newAutoEnvironment( - workflowTemplatePath, variables, detector, runType, user + const environment = await this._deploymentService.deployEnvironmentCalibration( + { + detector, runType, selectedConfiguration, user, + } ); - res.status(200).json(environment); + res.status(201).json(environment); } catch (error) { this._logger.errorMessage( - `Unable to deploy request by username(${username}) for ${configurationName} due to error`, + `Unable to deploy request by username(${username}) for ${selectedConfiguration} due to error`, {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} ); updateAndSendExpressResponseFromNativeError(res, error); diff --git a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js index 1a55ebde2..50e43bd09 100644 --- a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js +++ b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js @@ -92,7 +92,7 @@ export class CalibrationRunsModel extends Observable { this.notify(); const payload = { - detector, runType, configurationName + detectors: [...detector], runType, selectedConfiguration: configurationName }; const {result, ok} = await this._model.loader.post('/api/environment/auto', payload, true); From 8f8b30769753174401c17fd7c24e625f0a176db3 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 16:29:30 +0200 Subject: [PATCH 4/7] Fix bug in which potentially parsing fails and not the gRPC call --- Control/lib/services/Run.service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Control/lib/services/Run.service.js b/Control/lib/services/Run.service.js index f8aaea6f4..fae8d664e 100644 --- a/Control/lib/services/Run.service.js +++ b/Control/lib/services/Run.service.js @@ -136,10 +136,10 @@ class RunService { const calibrationMappings = await this._apricotService.getRuntimeEntryByComponent(COG, CALIBRATION_MAPPING); return JSON.parse(calibrationMappings); } catch (error) { - const err = grpcErrorToNativeError(error); - this._logger.errorMessage(`Unable to load calibration mapping due to: ${err}`, + const nativeError = error instanceof Error ? error : grpcErrorToNativeError(error); + this._logger.errorMessage(`Unable to load calibration mapping due to: ${nativeError.message}`, {level: LogLevel.OPERATIONS, system: 'GUI', facility: 'calibration-service'} - ) + ); } return {}; } From e455ee0f5b389487046d246b1a434f0d5f921b5a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 17:14:54 +0200 Subject: [PATCH 5/7] Remove dedicated grpc stream creation for calibration --- Control/lib/services/Environment.service.js | 140 -------------------- 1 file changed, 140 deletions(-) diff --git a/Control/lib/services/Environment.service.js b/Control/lib/services/Environment.service.js index 09129ba36..30ab633d1 100644 --- a/Control/lib/services/Environment.service.js +++ b/Control/lib/services/Environment.service.js @@ -238,146 +238,6 @@ class EnvironmentService { this._environmentCacheService.addOrUpdateEnvironment(environmentInfo, true); return environmentInfo; } - - /** - * Given the workflowTemplate and variables configuration, it will generate a unique string and send all to AliECS to create a - * new auto transitioning environment - * @param {String} workflowTemplate - name in format `repository/revision/template` - * @param {Object} vars - KV string pairs to define environment configuration - * @param {String} detector - on which the environment is deployed - * @param {String} runType - for which the environment is deployed - * @return {AutoEnvironmentDeployment} - if environment request was successfully sent - */ - async newAutoEnvironment(workflowTemplate, vars, detector, runType, user) { - const channelIdString = (Math.floor(Math.random() * (999999 - 100000) + 100000)).toString(); - const autoEnvironment = { - channelIdString, - inProgress: true, - detector, - runType, - events: [ - { - type: 'ENVIRONMENT', - payload: { - id: '-', - message: 'request was sent to AliECS', - at: Date.now(), - } - } - ], - }; - let calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - if (!calibrationRunsRequests) { - calibrationRunsRequests = {}; - } - if (!calibrationRunsRequests[detector]) { - calibrationRunsRequests[detector] = {}; - } - if (!calibrationRunsRequests[detector[runType]]) { - calibrationRunsRequests[detector][runType] = autoEnvironment; - - } - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - - const subscribeChannel = this._coreGrpc.client.Subscribe({id: channelIdString}); - subscribeChannel.on('data', (data) => this._onData(data, detector, runType)); - subscribeChannel.on('error', (error) => this._onError(error, detector, runType)); - subscribeChannel.on('end', () => this._onEnd(detector, runType)); - - - this._coreGrpc.NewAutoEnvironment({ - vars, - workflowTemplate, - id: channelIdString, - requestUser: user.toEcsFormat() - }); - - return autoEnvironment; - } - - /** - * Method to parse incoming messages from stream channel - * @param {Event} event - AliECS Event (proto) - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onData(event, detector, runType) { - const events = []; - const {taskEvent, environmentEvent, timestamp = Date.now()} = event; - if (taskEvent && (taskEvent.state === 'ERROR' || taskEvent.status === 'TASK_FAILED')) { - events.push({ - type: 'TASK', - payload: { - ...taskEvent, - at: Number(timestamp), - message: 'Please ensure environment is killed before retrying', - } - }); - } else if (environmentEvent) { - events.push({ - type: 'ENVIRONMENT', - payload: { - ...environmentEvent, - at: Number(timestamp), - } - }); - } - if (events.length > 0) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push(...events); - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } - } - - /** - * Method to be used in case of AliECS environment creation request error - * @param {Error} error - error encountered during the creation of environment - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onError(error, detector, runType) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ERROR', - payload: { - error, - at: Date.now() - } - }); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ERROR', - payload: { - error: 'Please ensure environment is killed before retrying', - at: Date.now() - } - }); - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } - - /** - * Method to be used for when environment successfully finished transitioning - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onEnd(detector, runType) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ENVIRONMENT', - payload: { - at: Date.now(), - message: 'Stream has now ended' - } - }); - calibrationRunsRequests[detector][runType].inProgress = false; - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } } module.exports = {EnvironmentService}; From e9fb76d8f6f0a9fb055010b0d7257508154b492d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 17:15:46 +0200 Subject: [PATCH 6/7] Add calibration dedicated deployment logic with Kafka --- Control/lib/api.js | 4 +- Control/lib/services/Deployment.service.js | 69 ++++++++++++++++++- .../environment/EnvironmentCache.service.js | 31 ++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/Control/lib/api.js b/Control/lib/api.js index 506db71ac..4c5894b39 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -103,7 +103,7 @@ module.exports.setup = (http, ws) => { const wsService = new WebSocketService(ws); const broadcastService = new BroadcastService(ws); const cacheService = new CacheService(broadcastService); - const environmentCacheService = new EnvironmentCacheService(broadcastService, eventEmitter); + const environmentCacheService = new EnvironmentCacheService(broadcastService, eventEmitter, cacheService); const qcConfigurationService = new QCConfigurationService(consulService); const qcConfigurationController = new QCConfigurationController(qcConfigurationService, config.consul); @@ -124,7 +124,7 @@ module.exports.setup = (http, ws) => { ctrlProxy, apricotService, cacheService, broadcastService, environmentCacheService ); const workflowService = new WorkflowTemplateService(ctrlProxy, apricotService); - const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService); + const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService, cacheService); const taskService = new TaskService(ctrlProxy); /** diff --git a/Control/lib/services/Deployment.service.js b/Control/lib/services/Deployment.service.js index 289b1cb90..81e627c3a 100644 --- a/Control/lib/services/Deployment.service.js +++ b/Control/lib/services/Deployment.service.js @@ -12,8 +12,9 @@ * or submit itself to any jurisdiction. */ -const {LogManager, LogLevel, NotFoundError} = require('@aliceo2/web-ui'); +const {LogManager, LogLevel, NotFoundError, InvalidInputError} = require('@aliceo2/web-ui'); const CoreUtils = require('./../control-core/CoreUtils.js'); +const {CacheKeys} = require('../common/cacheKeys.enum.js'); /** * **high-level service for deployment** @@ -30,10 +31,11 @@ class DeploymentService { * @param {EnvironmentService} environmentService - to use for creating new environments * @param {WorkflowService} workflowService - to use for retrieving template workflow information */ - constructor(environmentService, workflowService, environmentCacheService) { + constructor(environmentService, workflowService, environmentCacheService, cacheService) { this._environmentService = environmentService; this._workflowService = workflowService; this._environmentCacheService = environmentCacheService; + this._generalCacheService = cacheService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-service`); } @@ -66,6 +68,69 @@ class DeploymentService { return environment; } + /** + * High-level service method to gather the necessary information and request the deployment of a calibration environment for a given detector and run type. + * @param {object} deploymentConfiguration - the configuration to be used for deployment + * @param {string} deploymentConfiguration.detector - the detector for which the calibration environment should be deployed + * @param {string} deploymentConfiguration.runType - the run type to be used for deployment, needed to retrieve the hosts to ignore for deployment + * @param {string} deploymentConfiguration.selectedConfiguration - the name of the saved configuration to be used for deployment, needed to retrieve the variables for deployment + * @param {User} deploymentConfiguration.user - the user to be used for deployment + * @returns {EnvironmentInfo} - the id of the environment created + * @throws {Error} - if the deployment fails or invalid input + */ + async deployEnvironmentCalibration({ detector, runType, selectedConfiguration, user }) { + // Retrieve latest configuration version for given name + const { variables } = await this._workflowService.retrieveWorkflowSavedConfiguration(selectedConfiguration); + if (!variables) { + throw new InvalidInputError(`No configuration variables found for ${selectedConfiguration}`); + } + + // Retrieve latest default workflow to use + const { template, repository, revision } = await this._workflowService.getDefaultTemplateSource(); + const workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; + + const environment = await this._envService.newEnvironmentAsync({ + workflowTemplate: workflowTemplatePath, + userVars: variables, + user, + shouldAutoTransition: true, + detectors: [detector], + }); + + /** + * A dedicated calibration page environment object is created to follow only change of environment state events + */ + const calibrationEnvironment = { + inProgress: true, + detector, + runType, + events: [ + { + type: 'ENVIRONMENT', + payload: { + id: environment.id, + message: 'request was sent to AliECS', + at: Date.now(), + } + } + ], + }; + let calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + if (!calibrationRunsRequests) { + calibrationRunsRequests = {}; + } + if (!calibrationRunsRequests[detector]) { + calibrationRunsRequests[detector] = {}; + } + if (!calibrationRunsRequests[detector][runType]) { + calibrationRunsRequests[detector][runType] = calibrationEnvironment; + + } + this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); + return environment; + } + /** * Method to acknowledge a deployment failure for a given environment. * A failed deployment is not considered active anymore by ECS, thus it will only be present in the GUI cache diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index de35a11e3..1b5bebcdf 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -25,6 +25,7 @@ const { EnvironmentState } = require('../../common/environmentState.enum.js'); const { TaskState } = require('../../common/taskState.enum.js'); const { EnvironmentTransitionType } = require('../../common/environmentTransitionType.enum.js'); const { EcsOperationAndStepStatus } = require('../../common/ecsOperationAndStepStatus.enum.js'); +const { CacheKeys } = require('../../common/cacheKeys.enum.js'); const EPN_PATH_IN_ENVIRONMENT_INFO = 'hardware.epn.info'; /** @@ -38,13 +39,15 @@ class EnvironmentCacheService { * - optional service for broadcasting information * @param {BroadcastService} broadcastService - which is to be used for broadcasting * @param {EventEmitter} eventEmitter - which is to be used for listening to events + * @param {CacheService} cacheService - which is to be used for storing and retrieving cached data */ - constructor(broadcastService, eventEmitter) { + constructor(broadcastService, eventEmitter, cacheService) { this._environments = new Map(); this._lastUpdate = undefined; this._broadcastService = broadcastService; this._eventEmitter = eventEmitter; + this._cacheService = cacheService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/env-cache-service`); this._listenToEventsAndBroadcast(); @@ -275,6 +278,32 @@ class EnvironmentCacheService { this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); this._lastUpdate = Date.now(); + + /** + * Check if ID of environment is available in the general cache of CALIBRATION_RUNS_REQUESTS. + * If yes, find the detector and runType for this environment, push the event to the cache and broadcast it to clients. + */ + const calibrationRunsRequests = this._cacheService?.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + const { userVars } = cachedEnvironment; + const { includedDetectors = [], runType } = userVars ?? {}; + + if (includedDetectors.length === 1 && runType) { + // One detector only, it means environment may be of calibration type. + const [detector] = includedDetectors; + if (calibrationRunsRequests?.[detector]?.[runType]) { + calibrationRunsRequests[detector][runType].events.push( + { + type: 'ENVIRONMENT', + payload: { ...environmentEvent, at: environmentEvent.timestamp ?? Date.now() }, + }); + calibrationRunsRequests[detector][runType].inProgress = cachedEnvironment.isDeploying; + this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._broadcastService.broadcast( + CacheKeys.CALIBRATION_RUNS_REQUESTS, + calibrationRunsRequests[detector][runType] + ); + } + } } } From 9a4b8559a633da5f33d0a5f53e15be123c354119 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 25 May 2026 17:38:40 +0200 Subject: [PATCH 7/7] Fix small bugs from refactor --- Control/lib/api.js | 4 +- .../lib/controllers/Deployment.controller.js | 2 +- Control/lib/services/Deployment.service.js | 12 +++-- Control/lib/services/Environment.service.js | 17 ++++--- .../environment/EnvironmentCache.service.js | 48 ++++++++++--------- .../CalibrationRuns/CalibrationRuns.model.js | 4 +- 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/Control/lib/api.js b/Control/lib/api.js index 4c5894b39..b572b21bf 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -121,10 +121,10 @@ module.exports.setup = (http, ws) => { const detectorService = new DetectorService(ctrlProxy, apricotProxy); const environmentService = new EnvironmentService( - ctrlProxy, apricotService, cacheService, broadcastService, environmentCacheService + ctrlProxy, detectorService, cacheService, broadcastService, environmentCacheService ); const workflowService = new WorkflowTemplateService(ctrlProxy, apricotService); - const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService, cacheService); + const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService, cacheService, broadcastService); const taskService = new TaskService(ctrlProxy); /** diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index 182bf1f9e..a23f060b8 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -167,7 +167,7 @@ class DeploymentController { res.status(201).json(environment); } catch (error) { this._logger.errorMessage( - `Unable to deploy request by username(${username}) for ${selectedConfiguration} due to error`, + `Unable to deploy request by username(${username}) for ${selectedConfiguration} due to ${error.message}`, {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} ); updateAndSendExpressResponseFromNativeError(res, error); diff --git a/Control/lib/services/Deployment.service.js b/Control/lib/services/Deployment.service.js index 81e627c3a..af3fcabd7 100644 --- a/Control/lib/services/Deployment.service.js +++ b/Control/lib/services/Deployment.service.js @@ -30,12 +30,16 @@ class DeploymentService { * Constructor for inserting dependencies needed to retrieve environment data * @param {EnvironmentService} environmentService - to use for creating new environments * @param {WorkflowService} workflowService - to use for retrieving template workflow information + * @param {EnvironmentCacheService} environmentCacheService - to use for retrieving and updating environment data in cache + * @param {CacheService} cacheService - to use for retrieving and updating general data in cache, e.g. calibration runs requests + * @param {BroadcastService} broadcastService - to use for broadcasting updates to clients, e.g. calibration runs requests updates */ - constructor(environmentService, workflowService, environmentCacheService, cacheService) { + constructor(environmentService, workflowService, environmentCacheService, cacheService, _broadcastService) { this._environmentService = environmentService; this._workflowService = workflowService; this._environmentCacheService = environmentCacheService; this._generalCacheService = cacheService; + this._broadcastService = _broadcastService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-service`); } @@ -89,7 +93,7 @@ class DeploymentService { const { template, repository, revision } = await this._workflowService.getDefaultTemplateSource(); const workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; - const environment = await this._envService.newEnvironmentAsync({ + const environment = await this._environmentService.newEnvironmentAsync({ workflowTemplate: workflowTemplatePath, userVars: variables, user, @@ -115,7 +119,7 @@ class DeploymentService { } ], }; - let calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + let calibrationRunsRequests = this._generalCacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); if (!calibrationRunsRequests) { calibrationRunsRequests = {}; } @@ -126,7 +130,7 @@ class DeploymentService { calibrationRunsRequests[detector][runType] = calibrationEnvironment; } - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._generalCacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); return environment; } diff --git a/Control/lib/services/Environment.service.js b/Control/lib/services/Environment.service.js index 30ab633d1..9fcfca6c2 100644 --- a/Control/lib/services/Environment.service.js +++ b/Control/lib/services/Environment.service.js @@ -13,7 +13,6 @@ */ const {LogManager,grpcErrorToNativeError, NotFoundError} = require('@aliceo2/web-ui'); -const { CacheKeys } = require('./../common/cacheKeys.enum.js'); const { BroadcastKeys: { ENVIRONMENTS_OVERVIEW } } = require('./../common/broadcastKeys.enum'); const EnvironmentInfoAdapter = require('./../adapters/EnvironmentInfoAdapter.js'); const {EnvironmentTransitionResultAdapter} = require('./../adapters/EnvironmentTransitionResultAdapter.js'); @@ -25,21 +24,21 @@ class EnvironmentService { /** * Constructor for inserting dependencies needed to retrieve environment data * @param {GrpcServiceClient} coreGrpc - * @param {ApricotProxy} apricotGrpc + * @param {DetectorService} detectorService - to use for retrieving detector and host information * @param {CacheService} cacheService - to use for updating information on environments * @param {BroadcastService} broadcastService - to use for broadcasting information * @param {EnvironmentCacheService} environmentCacheService - to use for caching environments */ - constructor(coreGrpc, apricotGrpc, cacheService, broadcastService, environmentCacheService) { + constructor(coreGrpc, detectorService, cacheService, broadcastService, environmentCacheService) { /** * @type {GrpcServiceClient} */ this._coreGrpc = coreGrpc; /** - * @type {ApricotProxy} + * @type {DetectorService} */ - this._apricotGrpc = apricotGrpc; + this._detectorService = detectorService; /** * @type {CacheService} */ @@ -130,8 +129,8 @@ class EnvironmentService { if (!environment) { throw new NotFoundError(`Environment (id: ${id}) not found`); } - const detectorsAll = this._apricotGrpc.detectors ?? []; - const hostsByDetector = this._apricotGrpc.hostsByDetector ?? {}; + const detectorsAll = this._detectorService.detectors; + const hostsByDetector = this._detectorService.hostsByDetector; const environmentInfo = EnvironmentInfoAdapter.toEntity( environment, taskSource, detectorsAll, hostsByDetector ); @@ -204,8 +203,8 @@ class EnvironmentService { throw grpcErrorToNativeError(grpcError); } - const detectorsAll = this._apricotGrpc.detectors ?? []; - const hostsByDetector = this._apricotGrpc.hostsByDetector ?? {}; + const detectorsAll = this._detectorService.detectors; + const hostsByDetector = this._detectorService.hostsByDetector; /** * Transition is not yet started as per ECS, but we set the state to DEPLOYING to ensure that the UI * is updated accordingly. The state will be updated once the environment is created and the transition diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index 1b5bebcdf..ee3082f2d 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -279,30 +279,34 @@ class EnvironmentCacheService { this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); this._lastUpdate = Date.now(); - /** - * Check if ID of environment is available in the general cache of CALIBRATION_RUNS_REQUESTS. - * If yes, find the detector and runType for this environment, push the event to the cache and broadcast it to clients. - */ - const calibrationRunsRequests = this._cacheService?.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - const { userVars } = cachedEnvironment; - const { includedDetectors = [], runType } = userVars ?? {}; + try { + /** + * Check if ID of environment is available in the general cache of CALIBRATION_RUNS_REQUESTS. + * If yes, find the detector and runType for this environment, push the event to the cache and broadcast it to clients. + */ + const calibrationRunsRequests = this._cacheService?.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + const { userVars } = cachedEnvironment; + const { includedDetectors = [], runType } = userVars ?? {}; - if (includedDetectors.length === 1 && runType) { - // One detector only, it means environment may be of calibration type. - const [detector] = includedDetectors; - if (calibrationRunsRequests?.[detector]?.[runType]) { - calibrationRunsRequests[detector][runType].events.push( - { - type: 'ENVIRONMENT', - payload: { ...environmentEvent, at: environmentEvent.timestamp ?? Date.now() }, - }); - calibrationRunsRequests[detector][runType].inProgress = cachedEnvironment.isDeploying; - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast( - CacheKeys.CALIBRATION_RUNS_REQUESTS, - calibrationRunsRequests[detector][runType] - ); + if (includedDetectors.length === 1 && runType) { + // One detector only, it means environment may be of calibration type. + const [detector] = includedDetectors; + if (calibrationRunsRequests?.[detector]?.[runType]) { + calibrationRunsRequests[detector][runType].events.push( + { + type: 'ENVIRONMENT', + payload: { ...environmentEvent, at: environmentEvent.timestamp ?? Date.now() }, + }); + calibrationRunsRequests[detector][runType].inProgress = cachedEnvironment.isDeploying; + this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._broadcastService.broadcast( + CacheKeys.CALIBRATION_RUNS_REQUESTS, + calibrationRunsRequests[detector][runType] + ); + } } + } catch (error) { + console.trace(error); } } } diff --git a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js index 50e43bd09..08676a173 100644 --- a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js +++ b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js @@ -92,9 +92,9 @@ export class CalibrationRunsModel extends Observable { this.notify(); const payload = { - detectors: [...detector], runType, selectedConfiguration: configurationName + detectors: [detector], runType, selectedConfiguration: configurationName }; - const {result, ok} = await this._model.loader.post('/api/environment/auto', payload, true); + const {result, ok} = await this._model.loader.post('/api/deploy/calibration', payload, true); this._calibrationRuns.payload[detector][runType].ongoingCalibrationRun = ok ? RemoteData.success(result) : RemoteData.failure(result.message);