Skip to content

Commit

Permalink
[OGUI-1409] Add bkp service for past runs retrieval (#2117)
Browse files Browse the repository at this point in the history
* adds a new HTTP based service to retrieve per run information from Bookkeeping based on provided definition, type and detector
* adds a new HTTP based service to retrieve the run type maps with name associated to ID from Bookkeeping
  • Loading branch information
graduta authored Oct 10, 2023
1 parent 97436f9 commit 78e2c9e
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 2 deletions.
10 changes: 8 additions & 2 deletions Control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [O2Control gRPC](#o2control-grpc)
- [Apricot gRPC](#apricot-grpc)
- [Grafana](#grafana)
- [Bookkeeping](#bookkeeping)
- [Consul](#consul)
- [Notification service](#notification-service)
- [InfoLogger GUI](#infologger-gui)
Expand Down Expand Up @@ -66,8 +67,13 @@ It communicates with [Control agent](https://github.com/AliceO2Group/Control) ov
* `package` - name of the gRPC package

### Grafana
* `hostname` - Grafana instance hostname
* `port` - Grafana instance port
* `url` - built URL which points to grafana instance: `<protocol>://<instance>:<port>`

### Bookkeeping
* `url` - URL which points to Bookkeeping API: `<protocol>://<instance>:<port>`, `<protocol>://<domain_name>`
* `token` - token needed for permissions to retrieve data from Bookkeeping

Bookkeeping is going to be used as the source of latest `CALIBRATION` runs as per the [definition](https://github.com/AliceO2Group/Bookkeeping/blob/main/docs/RUN_DEFINITIONS.md). Detectors may need these run before stable beams, with some needing _none_, some only _one_ run and others _multiple_ ones defined by the `RUN TYPE` attribute. As this can vary depending on the period, the types corresponding to a detector will be defined and retrieved from the KV store of [O2Apricot](https://github.com/AliceO2Group/Control/tree/master/apricot) (key and value TBD).

### Consul
Use of a Consul instance is optional
Expand Down
4 changes: 4 additions & 0 deletions Control/config-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ module.exports = {
grafana: {
url: 'http://localhost:3000'
},
bookkeeping: {
url: 'http://localhost:4000',
token: 'some-token'
},
consul: {
ui: 'localhost:8500',
hostname: 'localhost',
Expand Down
54 changes: 54 additions & 0 deletions Control/lib/adapters/RunSummaryAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

/**
* RunSummaryAdapter - Given an object with RUN information as per Bookkeeping Database (https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js),
* return a minified version of it with only the summary
*/
class RunSummaryAdapter {
/**
* RunSummaryAdapter
*/
constructor() {}

/**
* Converts the given object to an entity object.
*
* @param {Object} run - Run Entity as per Bookkeeping https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js
* @returns {RunSummary} entity of a task with needed information
*/
static toEntity(run) {
const {
runNumber,
environmentId,
definition,
calibrationStatus,
runType,
startTime,
endTime,
detectors = [],
} = run;
return {
runNumber,
environmentId,
definition,
calibrationStatus,
runType: runType?.name,
startTime,
detectors: detectors.sort(),
endTime,
};
}
}

module.exports = RunSummaryAdapter;
108 changes: 108 additions & 0 deletions Control/lib/services/Bookkeeping.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

const {Log} = require('@aliceo2/web-ui');
const {httpGetJson} = require('./../utils.js');
const RunSummaryAdapter = require('./../adapters/RunSummaryAdapter.js');

/**
* BookkeepingService class to be used to retrieve data from Bookkeeping
*/
class BookkeepingService {
/**
* Constructor for configuring the service to retrieve data via Bookkeeping HTTP API
* @param {Object} config = {url: string, token: string} - configuration for using BKP service
*/
constructor({url = '', token = ''}) {
this._url = url;
const {protocol, hostname, port} = new URL(this._url);
this._hostname = hostname;
this._port = port;
this._protocol = protocol;

this._token = token;

this._runTypes = {}; // in-memory object which is filled with runTypes on server start
this._logger = new Log(`${process.env.npm_config_log_label ?? 'cog'}/bkp-service`);
}

/**
* Method to initialize the run service with static data such as runTypes
* @return {void}
*/
async init() {
this._runTypes = await this._getRunTypes();
}

/**
* Given a definition, a type of a run and a detector, fetch from Bookkeeping the last RUN matching the parameters
* @param {String} definition - definition of the run to query
* @param {String} type - type of the run to query
* @param {String} detector - detector which contained the run
* @return {RunSummary|{}} - run object from Bookkeeping
*/
async getRun(definition, type, detector) {
if (this._runTypes[type]) {
let filter = `filter[definitions]=${definition}&filter[runTypes]=${this._runTypes[type]}&page[limit]=1&`;
filter += `filter[detectors][operator]=and&filter[detectors][values]=${detector}`
try {
const {data} = await httpGetJson(this._hostname, this._port, `/api/runs?${filter}&token=${this._token}`, {
protocol: this._protocol,
rejectUnauthorized: false,
});
if (data?.length > 0) {
return RunSummaryAdapter.toEntity(data[0]);
}
} catch (error) {
this._logger.debug(error);
}
}
return {};
}

/**
* Method to fetch run types from Bookkeeping and build a map of types to IDs as needed for filtering in RUNs API
* @returns {Object<String, Number>} - map of runtypes to their ID
*/
async _getRunTypes() {
try {
const runTypesMap = {};
const {data} = await httpGetJson(this._hostname, this._port, `/api/runTypes?token=${this._token}`, {
protocol: this._protocol,
rejectUnauthorized: false,
});
for (const type of data) {
runTypesMap[type.name] = type.id;
}
return runTypesMap;
} catch (error) {
this._logger.debug(error);
}
return {};
}

/**
* Getters/Setters
*/

/**
* Return the object storing run types by their name with ID
* @return {Object<String, Number>}
*/
get runTypes() {
return this._runTypes;
}
}

module.exports = {BookkeepingService};
27 changes: 27 additions & 0 deletions Control/lib/typedefs/RunInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

/**
* @typedef RunSummary
*
* RunSummary is an object which contains just a summary of an entire run entity: https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js
*
* @property {Number} runNumber
* @property {String} environmentId
* @property {String} definition
* @property {String} calibrationStatus
* @property {String} runType
* @property {Number} startTime
* @property {Number} endTime
* @property {Array<String>} detectors
*/
155 changes: 155 additions & 0 deletions Control/test/lib/services/mocha-bookkeeping.service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
/* eslint-disable max-len */

const assert = require('assert');
const nock = require('nock');

const {BookkeepingService} = require('../../../lib/services/Bookkeeping.service');

describe('BookkeepingService test suite', () => {
const url = 'http://bkp-test.cern.ch:8888';
let bkp = new BookkeepingService({url, token: ''});

describe(`'init' test suite`, async () => {
before(() => {
bkp = new BookkeepingService({url, token: ''});
nock(url)
.get('/api/runTypes?token=')
.reply(200, {
data: [
{name: 'NOISE', id: 1}, {name: 'PHYSICS', id: 2}, {name: 'SYNTHETIC', id: 3}
]
});
nock(url)
.get('/api/runTypes?token=no-data')
.reply(200, {data: []});
nock(url)
.get('/api/runTypes?token=error')
.replyWithError('Unable to connect');
});
after(() => nock.cleanAll());

it('should successfully load runTypes from bookkeeping', async () => {
await bkp.init();
assert.deepStrictEqual(bkp.runTypes, {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3});
});

it('should successfully load an empty object if no runTypes are provided', async () => {
bkp._token = 'no-data';
await bkp.init();
assert.deepStrictEqual(bkp.runTypes, {});
});

it('should successfully load an empty object even if bookkeeping returned an error', async () => {
bkp._token = 'error';
await bkp.init();
assert.deepStrictEqual(bkp.runTypes, {});
});
});

describe(`'_getRunTypes' test suite`, async () => {
before(() => {
bkp = new BookkeepingService({url, token: ''});
nock(url)
.get('/api/runTypes?token=')
.reply(200, {
data: [
{name: 'NOISE', id: 1}, {name: 'PHYSICS', id: 2}, {name: 'SYNTHETIC', id: 3}
]
});
nock(url)
.get('/api/runTypes?token=no-data')
.reply(200, {data: []});
nock(url)
.get('/api/runTypes?token=error')
.replyWithError('Unable to connect');
});
after(() => nock.cleanAll());

it('should successfully return runTypes as object from bookkeeping', async () => {
const runTypes = await bkp._getRunTypes();
assert.deepStrictEqual(runTypes, {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3});
});

it('should successfully load an empty object if no runTypes are provided', async () => {
bkp._token = 'no-data';
const runTypes = await bkp._getRunTypes();
assert.deepStrictEqual(runTypes, {});
});

it('should successfully load an empty object even if bookkeeping returned an error', async () => {
bkp._token = 'error';
const runTypes = await bkp._getRunTypes();
assert.deepStrictEqual(runTypes, {});
});
});

describe(`'getRun' test suite`, async () => {
let runToReturn = {
runNumber: 123,
environmentId: 'abc',
definition: 'CALIBRATION',
calibrationStatus: 'good',
runType: {name: 'NOISE', id: 1},
detectors: ['TPC'],
startTime: Date.now() - 100,
endTime: Date.now(),
extraField: '',
someOther: 1234
};
before(() => {
bkp = new BookkeepingService({url, token: ''});
bkp._runTypes = {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3};
nock(url)
.get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=1&page[limit]=1&'
+ 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=')
.reply(200, {
data: [runToReturn]
});

nock(url)
.get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=2&page[limit]=1&'
+ 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=')
.reply(200, {
data: []
});

nock(url)
.get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=3&page[limit]=1&'
+ 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=')
.replyWithError('Unable');
});
after(() => nock.cleanAll());

it('should successfully return a run based on existing runType and provided def, type and detector', async () => {
const run = await bkp.getRun('CALIBRATION', 'NOISE', 'TPC');
const runInfo = JSON.parse(JSON.stringify(runToReturn));
runInfo.runType = runInfo.runType.name;
delete runInfo.extraField;
delete runInfo.someOther;
assert.deepStrictEqual(run, runInfo);
});

it('should successfully return an empty run if none was found', async () => {
const run = await bkp.getRun('CALIBRATION', 'PHYSICS', 'TPC');
assert.deepStrictEqual(run, {});
});

it('should successfully return an empty run even if bkp service throws error', async () => {
const run = await bkp.getRun('CALIBRATION', 'SYNTHETIC', 'TPC');
assert.deepStrictEqual(run, {});
});
});
});

0 comments on commit 78e2c9e

Please sign in to comment.