Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
bb5a9c3
[O2B-1545] Add GAQ summary models, adapters and migration
isaachilly Mar 5, 2026
4fade0f
[O2B-1545] Add GAQ summary repositories and timestamps
isaachilly Mar 16, 2026
15afdaa
[O2B-1545] Use default Sequelize createdAt/updatedAt instead of compu…
isaachilly Mar 17, 2026
363f09b
[O2B-1545] Use mcReproducibleCoverage float instead of boolean
isaachilly Mar 17, 2026
002f301
[O2B-1545] Remove mcReproducible field
isaachilly Mar 17, 2026
5bebbb5
[O2B-1563] Invalidate GAQ summaries on related changes
isaachilly Mar 17, 2026
ee55f59
Add GAQ worker and summary recalculation
isaachilly Mar 17, 2026
530216b
[O2B-1564] Create computeSummary to separate getting and computing
isaachilly Mar 17, 2026
e1f3223
[O2B-1564] Exclude reproducible coverage from badEffectiveRunCoverage
isaachilly Mar 17, 2026
16d3563
[O2B-1545] Rename GAQ summary coverage fields
isaachilly Mar 18, 2026
9bf8027
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly Mar 18, 2026
edcd9f7
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly Mar 18, 2026
87afd8c
[O2B-1564] Rename coverage fields to match table cols in GaqService o…
isaachilly Mar 18, 2026
170c1b3
[O2B-1563] Add GAQ summary invalidation trigger tests
isaachilly Mar 31, 2026
78a2181
[O2B-1563] Use dataPass.id in GAQ invalidation TYPO
isaachilly Mar 31, 2026
a41b312
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly Mar 31, 2026
abb263b
Make GAQ worker singleton and add pause/resume
isaachilly Apr 1, 2026
10fac20
[O2B-1564] Add GAQ worker tests; pause worker in DB reset
isaachilly Apr 1, 2026
6aa9617
[O2B-1564] Add GaqService tests and register suite
isaachilly Apr 1, 2026
c2ccbd4
Add calculationFailed to GAQ summaries
isaachilly Apr 17, 2026
1e78cac
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly Apr 17, 2026
f0dea1f
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly Apr 17, 2026
35112e5
[O2B-1545] Make GAQ summary columns nullable
isaachilly Apr 17, 2026
c81cdf6
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly Apr 17, 2026
5562afe
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly Apr 17, 2026
83ddf1d
[O2B-1564] Persist GAQ calculation failure
isaachilly Apr 17, 2026
00c1e2b
[O2B-1564] Assert calculationFailed in created summary
isaachilly Apr 28, 2026
5059082
[O2B-1545] Add invalidatedAt to gaq_summaries, remove invalidation table
isaachilly May 4, 2026
9893667
[O2B-1545] Rename calculationFailed to notComputable
isaachilly May 4, 2026
1588cd0
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly May 4, 2026
47449a7
[O2B-1545] Remove unused GaqSummaryInvalidation files and imports
isaachilly May 4, 2026
6ed0538
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly May 4, 2026
0b8b161
[O2B-1563] Use GaqSummaryRepository invalidation
isaachilly May 4, 2026
6037553
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly May 4, 2026
f4d2f0d
[O2B-1564] Use invalidatedAt on GaqSummary in background worker
isaachilly May 4, 2026
a0b44cc
[O2B-1564] Fix GAQ recalculation config and default env vars
isaachilly May 5, 2026
8bc1d41
[O2B-1564] Process invalid GAQ summaries concurrently in batches
isaachilly May 5, 2026
772a96c
[O2B-1545] Add index on gaq_summaries.invalidated_at
isaachilly May 5, 2026
4ecce8f
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly May 5, 2026
98ae68c
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly May 5, 2026
92e3c54
[O2B-1563] Stealth fix: We do not need to soft delete flags that are …
isaachilly May 5, 2026
509154e
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly May 5, 2026
4943091
[O2B-1564] Improve logging message for GAQWorker
isaachilly May 5, 2026
0586b3e
[O2B-1564] Add warning message when the num of invalidated summaries …
isaachilly May 5, 2026
f5f686c
[O2B-1545] Add a composite primary key on GAQSummary table
isaachilly Jun 18, 2026
3c7bce3
Merge branch 'feature/O2B-1545/Create-GAQ-Summary-Tables' into featur…
isaachilly Jun 18, 2026
9c1116b
[O2B-1563] Centralise GAQ summary invalidation and skip redundant work
isaachilly Jun 18, 2026
baa9bb0
Merge branch 'feature/O2B-1563/Create-GAQ-summary-invalidation-mechan…
isaachilly Jun 19, 2026
8992231
[O2B-1564] Overwrite summary values if couldn't be calculated
isaachilly Jun 19, 2026
7481ec1
[O2B-1564] Protect invalidatedAt from race condition
isaachilly Jun 19, 2026
fe8062c
[O2B-1564] Fixed silent error in test
isaachilly Jun 19, 2026
9851455
[O2B-1564] Modify pause behaviour to account for in-flight recalculation
isaachilly Jun 19, 2026
d7dc453
[O2B-1564] Remove extra where option
isaachilly Jun 19, 2026
35272c7
[O2B-1564] Make batchSize dynamic between a min and max
isaachilly Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ services:
JWT_SECRET: BOOKKEEPING-DEV
GRPC_INTERNAL_ORIGIN: '[::]:4001'
GRPC_AUTHENTICATED_ORIGIN: '[::]:4002'
GAQ_ENABLE_RECALCULATION: "True"
GAQ_RECALCULATION_PERIOD: 10000
GAQ_RECALCULATION_MIN_BATCH_SIZE: 5
GAQ_RECALCULATION_MAX_BATCH_SIZE: 50
ports:
- "4000:4000"
- "4001:4001"
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ services:
JWT_SECRET: BOOKKEEPING-TEST-SUITE
PAGE_ITEMS_LIMIT: 100
CCDB_SYNCHRONIZATION_PERIOD: 3153600000000 # 100y in milliseconds, to be sure all the runs are included when testing sync
GAQ_ENABLE_RECALCULATION: "True"
GAQ_RECALCULATION_PERIOD: 1000
GAQ_RECALCULATION_MIN_BATCH_SIZE: 1
GAQ_RECALCULATION_MAX_BATCH_SIZE: 1
restart: "no"

database:
Expand Down
13 changes: 12 additions & 1 deletion lib/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
const { webUiServer } = require('./server');
const { GRPCConfig, ServicesConfig } = require('./config');

const { userCertificate, monalisa: monalisaConfig, ccdb: ccdbConfig, enableHousekeeping } = ServicesConfig;
const { userCertificate, monalisa: monalisaConfig, ccdb: ccdbConfig, gaq: gaqConfig, enableHousekeeping } = ServicesConfig;
const { handleLostRunsAndEnvironments } = require('./server/services/housekeeping/handleLostRunsAndEnvironments.js');
const { isInTestMode } = require('./utilities/env-utils.js');
const { ScheduledProcessesManager } = require('./server/services/ScheduledProcessesManager.js');
Expand All @@ -27,6 +27,7 @@
const { environmentService } = require('./server/services/environment/EnvironmentService.js');
const { runService } = require('./server/services/run/RunService.js');
const { CcdbSynchronizer } = require('./server/externalServicesSynchronization/ccdb/CcdbSynchronizer.js');
const { gaqWorker } = require('./server/services/gaq/GaqWorker.js');
const { promises: fs } = require('fs');
const { MonAlisaClient } = require('./server/externalServicesSynchronization/monalisa/MonAlisaClient.js');
const https = require('https');
Expand Down Expand Up @@ -131,6 +132,16 @@
},
);
}

if (gaqConfig.enableRecalculation) {
this.scheduledProcessesManager.schedule(
() => gaqWorker.recalculateGaqSummaries(gaqConfig.minBatchSize, gaqConfig.maxBatchSize),
{
wait: 10 * 1000,
every: gaqConfig.recalculationPeriod,
},
);
}
} catch (error) {
this._logger.errorMessage(`Error while starting: ${error}`);
return this.stop();
Expand All @@ -156,7 +167,7 @@
}

/**
* Housekeeping method, it wraps @see handleLostRunsAndEnvironments and logs its results

Check warning on line 170 in lib/application.js

View workflow job for this annotation

GitHub Actions / linter

Unexpected inline JSDoc tag. Did you mean to use {@see}, \@see, or `@see`?
*
* @param {Agent} httpAgent agent to be used by the HTTP client
* @return {Promise<void>} promise
Expand Down
26 changes: 26 additions & 0 deletions lib/config/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@
CCDB_RUN_INFO_URL,
} = process.env ?? {};

/**
* Parse a positive integer env var, falling back to the default and warning if the value is invalid
*
* @param {string|undefined} raw the raw env var value
* @param {number} defaultValue value to use when raw is unset or invalid
* @param {string} name env var name (for the warning message)
* @return {number} the parsed value or the default
*/
const parsePositiveInt = (raw, defaultValue, name) => {
if (raw === undefined) return defaultValue;

Check failure on line 39 in lib/config/services.js

View workflow job for this annotation

GitHub Actions / linter

Expected { after 'if' condition
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed <= 0) {
// eslint-disable-next-line no-console
console.warn(`Invalid ${name}=${JSON.stringify(raw)}; falling back to ${defaultValue}`);
return defaultValue;
}
return parsed;
};

exports.services = {
enableHousekeeping: process.env?.ENABLE_HOUSEKEEPING?.toLowerCase() === 'true',
userCertificate: {
Expand Down Expand Up @@ -68,4 +87,11 @@
synchronizationPeriod: Number(CCDB_SYNCHRONIZATION_PERIOD) || 24 * 60 * 60 * 1000, // 1d in milliseconds
runInfoUrl: CCDB_RUN_INFO_URL,
},

gaq: {
enableRecalculation: process.env?.GAQ_ENABLE_RECALCULATION?.toLowerCase() === 'true',
recalculationPeriod: parsePositiveInt(process.env?.GAQ_RECALCULATION_PERIOD, 30 * 1000, 'GAQ_RECALCULATION_PERIOD'), // 30s default
minBatchSize: parsePositiveInt(process.env?.GAQ_RECALCULATION_MIN_BATCH_SIZE, 1, 'GAQ_RECALCULATION_MIN_BATCH_SIZE'),
maxBatchSize: parsePositiveInt(process.env?.GAQ_RECALCULATION_MAX_BATCH_SIZE, 100, 'GAQ_RECALCULATION_MAX_BATCH_SIZE'),
},
};
62 changes: 62 additions & 0 deletions lib/database/adapters/GaqSummaryAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @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.
*/

/**
* GaqSummaryAdapter
*/
class GaqSummaryAdapter {
/**
* Constructor
*/
constructor() {
this.toEntity = this.toEntity.bind(this);
}

/**
* Converts the given database object to an entity object.
*
* @param {SequelizeGaqSummary} databaseObject Object to convert.
* @returns {GaqSummary} Converted entity object.
*/
toEntity(databaseObject) {
const {
dataPassId,
runNumber,
badRunCoverage,
explicitlyNotBadRunCoverage,
mcReproducibleCoverage,
missingVerificationsCount,
undefinedQualityPeriodsCount,
notComputable,
invalidatedAt,
createdAt,
updatedAt,
} = databaseObject;

return {
dataPassId,
runNumber,
badRunCoverage,
explicitlyNotBadRunCoverage,
mcReproducibleCoverage,
missingVerificationsCount,
undefinedQualityPeriodsCount,
notComputable,
invalidatedAt,
createdAt,
updatedAt,
};
}
}

module.exports = { GaqSummaryAdapter };
3 changes: 3 additions & 0 deletions lib/database/adapters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const EorReasonAdapter = require('./EorReasonAdapter');
const FlpRoleAdapter = require('./FlpRoleAdapter');
const { HostAdapter } = require('./HostAdapter.js');
const { GaqDetectorAdapter } = require('./GaqDetectorAdapter.js');
const { GaqSummaryAdapter } = require('./GaqSummaryAdapter.js');
const { LhcFillAdapter } = require('./LhcFillAdapter.js');
const { LhcFillStatisticsAdapter } = require('./LhcFillStatisticsAdapter.js');
const LhcPeriodAdapter = require('./LhcPeriodAdapter');
Expand Down Expand Up @@ -63,6 +64,7 @@ const environmentHistoryItemAdapter = new EnvironmentHistoryItemAdapter();
const eorReasonAdapter = new EorReasonAdapter();
const flpRoleAdapter = new FlpRoleAdapter();
const gaqDetectorAdapter = new GaqDetectorAdapter();
const gaqSummaryAdapter = new GaqSummaryAdapter();
const hostAdapter = new HostAdapter();
const lhcFillAdapter = new LhcFillAdapter();
const lhcFillStatisticsAdapter = new LhcFillStatisticsAdapter();
Expand Down Expand Up @@ -159,6 +161,7 @@ module.exports = {
eorReasonAdapter,
flpRoleAdapter,
gaqDetectorAdapter,
gaqSummaryAdapter,
hostAdapter,
lhcFillAdapter,
lhcFillStatisticsAdapter,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.createTable('gaq_summaries', {
data_pass_id: {
type: Sequelize.INTEGER,
primaryKey: true,
allowNull: false,
references: {
model: 'data_passes',
key: 'id',
},
},
run_number: {
type: Sequelize.INTEGER,
primaryKey: true,
allowNull: false,
references: {
model: 'runs',
key: 'run_number',
},
},
bad_run_coverage: {
type: Sequelize.FLOAT,
},
explicitly_not_bad_run_coverage: {
type: Sequelize.FLOAT,
},
mc_reproducible_coverage: {
type: Sequelize.FLOAT,
},
missing_verifications_count: {
type: Sequelize.INTEGER,
},
undefined_quality_periods_count: {
type: Sequelize.INTEGER,
},
not_computable: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
invalidated_at: {
type: Sequelize.DATE(3),
allowNull: true,
defaultValue: null,
},
created_at: {
type: Sequelize.DATE(3),
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'),
},
updated_at: {
type: Sequelize.DATE(3),
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'),
},
}, { transaction });

await queryInterface.addIndex('gaq_summaries', {
name: 'gaq_summaries_invalidated_at_idx',
fields: ['invalidated_at'],
}, { transaction });
}),

down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.dropTable('gaq_summaries', { transaction });
}),
};
59 changes: 59 additions & 0 deletions lib/database/models/gaqSummary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @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.
*/

module.exports = (sequelize) => {
const Sequelize = require('sequelize');

const GaqSummary = sequelize.define('GaqSummary', {
dataPassId: {
type: Sequelize.INTEGER,
primaryKey: true,
},
runNumber: {
type: Sequelize.INTEGER,
primaryKey: true,
},
badRunCoverage: {
type: Sequelize.FLOAT,
},
explicitlyNotBadRunCoverage: {
type: Sequelize.FLOAT,
},
mcReproducibleCoverage: {
type: Sequelize.FLOAT,
},
missingVerificationsCount: {
type: Sequelize.INTEGER,
},
undefinedQualityPeriodsCount: {
type: Sequelize.INTEGER,
},
notComputable: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
invalidatedAt: {
type: Sequelize.DATE(3),
},
}, { tableName: 'gaq_summaries' });

GaqSummary.removeAttribute('id');

GaqSummary.associate = (models) => {
GaqSummary.belongsTo(models.Run, { foreignKey: 'runNumber', as: 'run' });
GaqSummary.belongsTo(models.DataPass, { foreignKey: 'dataPassId', as: 'dataPass' });
};

return GaqSummary;
};
2 changes: 2 additions & 0 deletions lib/database/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const EorReason = require('./eorreason');
const EpnRoleSession = require('./epnrolesession');
const FlpRole = require('./flprole');
const GaqDetector = require('./gaqDetector.js');
const GaqSummary = require('./gaqSummary.js');
const Host = require('./host.js');
const LhcFill = require('./lhcFill');
const LhcFillStatistics = require('./lhcFillStatistics.js');
Expand Down Expand Up @@ -66,6 +67,7 @@ module.exports = (sequelize) => {
EpnRoleSessionkey: EpnRoleSession(sequelize),
FlpRole: FlpRole(sequelize),
GaqDetector: GaqDetector(sequelize),
GaqSummary: GaqSummary(sequelize),
Host: Host(sequelize),
LhcFill: LhcFill(sequelize),
LhcFillStatistics: LhcFillStatistics(sequelize),
Expand Down
55 changes: 55 additions & 0 deletions lib/database/repositories/GaqSummaryRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @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.
*/

const {
models: {
GaqSummary,
},
} = require('..');
const Repository = require('./Repository');

/**
* GaqSummary repository
*/
class GaqSummaryRepository extends Repository {
/**
* Creates a new `GaqSummaryRepository` instance.
*/
constructor() {
super(GaqSummary);
}

/**
* Mark the summary for a given (dataPassId, runNumber) as invalidated, creating the row if it does not yet exist
*
* @param {number} dataPassId data pass id
* @param {number} runNumber run number
* @return {Promise<void>} resolves once the summary is invalidated
*/
async invalidate(dataPassId, runNumber) {
await this.upsert({ dataPassId, runNumber, invalidatedAt: new Date() });
}

/**
* Mark a list of summaries as invalidated in parallel
*
* @param {{ dataPassId: number, runNumber: number }[]} pairs the (dataPassId, runNumber) pairs to invalidate
* @return {Promise<void>} resolves once all summaries are invalidated
*/
async invalidateMany(pairs) {
const invalidatedAt = new Date();
await Promise.all(pairs.map(({ dataPassId, runNumber }) => this.upsert({ dataPassId, runNumber, invalidatedAt })));
}
}

module.exports = new GaqSummaryRepository();
Loading
Loading