@cocalc/database
Version:
CoCalc: code for working with our PostgreSQL database
264 lines (234 loc) • 10.7 kB
JavaScript
;
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.projects_that_used_license = exports.number_of_hours_projects_used_license = exports.number_of_projects_that_used_license = exports.update_site_license_usage_log = void 0;
const query_1 = require("../query");
const const_1 = require("./const");
async function update_site_license_usage_log(db) {
// don't run this in parallel – timeout_s triggers a transaction and as of now, we have only one client<->db connection
await update_site_license_usage_log_running_projects(db);
await update_site_license_usage_log_not_running_projects(db);
}
exports.update_site_license_usage_log = update_site_license_usage_log;
/*
This function ensures that for every running project P using a site license L,
there is exactly one entry (P,L,time,null) in the table site_license_usage_log.
*/
async function update_site_license_usage_log_running_projects(db) {
const dbg = db._dbg("update_site_license_usage_log_running_projects");
dbg();
/*
In the comment below I explain how I figured out the two big queries we do below...
This is a reasonably efficient way to get all pairs (project_id, license_id) where
the license is applied and the project is running (and was actually edited in the last week).
The last_edited is a cheat to make this massively faster by not requiring a scan
through all projects (or an index).
Set A:
WITH running_license_info AS (SELECT project_id, (jsonb_each_text(site_license)).* FROM projects WHERE last_edited >= NOW() - INTERVAL '1 day' AND state#>>'{state}'='running')
SELECT project_id, key AS license_id FROM running_license_info WHERE value != '{}';
This query gets all pairs (project_id, license_id) that are currently running
with that license according to the the site_license_usage_log:
Set B:
SELECT project_id, license_id, start FROM site_license_usage_log WHERE stop IS NULL;
We want to sync these two sets by:
- For each element (project_id, license_id) of set A that is not in set B,
add a new entry to the site_license_usage_log table of the
form (project_id, license_id, NOW()).
- For each element (project_id, license_id, start) of set B that is not in set A,
modify that element to be of the form
(project_id, license_id, start, NOW())
thus removing it from set B.
What can be done with SQL to accomplish this?
This query computes set A minus set B:
WITH running_license_info AS (SELECT project_id, (jsonb_each_text(site_license)).* FROM projects WHERE last_edited >= NOW() - INTERVAL '1 day' AND state#>>'{state}'='running')
SELECT running_license_info.project_id AS project_id, running_license_info.key::UUID AS license_id FROM running_license_info WHERE
running_license_info.value != '{}' AND NOT EXISTS (SELECT FROM site_license_usage_log WHERE site_license_usage_log.stop IS NULL AND site_license_usage_log.project_id=running_license_info.project_id AND site_license_usage_log.license_id=running_license_info.key::UUID);
So this query adds everything to site_license_usage_log that is missing:
WITH missing AS (WITH running_license_info AS (SELECT project_id, (jsonb_each_text(site_license)).* FROM projects WHERE last_edited >= NOW() - INTERVAL '1 day' AND state#>>'{state}'='running')
SELECT running_license_info.project_id AS project_id, running_license_info.key::UUID AS license_id FROM running_license_info WHERE
running_license_info.value != '{}' AND
NOT EXISTS (SELECT FROM site_license_usage_log WHERE site_license_usage_log.stop IS NULL AND site_license_usage_log.project_id=running_license_info.project_id AND site_license_usage_log.license_id=running_license_info.key::UUID))
INSERT INTO site_license_usage_log(project_id, license_id, start) SELECT project_id, license_id, NOW() FROM missing;
In the other direction, we need to fill out everything in set B that is missing from set A:
This query computes set B minus set A:
WITH running_license_info
AS (SELECT project_id, (jsonb_each_text(site_license)).*
FROM projects WHERE
last_edited >= NOW() - INTERVAL '1 day' AND state#>>'{state}'='running'
)
SELECT site_license_usage_log.license_id AS license_id, site_license_usage_log.project_id AS project_id, site_license_usage_log.start AS start
FROM site_license_usage_log WHERE
stop IS NULL AND
NOT EXISTS
(SELECT FROM running_license_info
WHERE running_license_info.value != '{}'
AND running_license_info.project_id=site_license_usage_log.project_id
AND site_license_usage_log.license_id=running_license_info.key::UUID)
And now modify the entries of site_license_usage_log using set B minus set A:
WITH stopped AS (
WITH running_license_info
AS (SELECT project_id, (jsonb_each_text(site_license)).*
FROM projects WHERE
last_edited >= NOW() - INTERVAL '1 day' AND state#>>'{state}'='running'
)
SELECT site_license_usage_log.license_id AS license_id, site_license_usage_log.project_id AS project_id, site_license_usage_log.start AS start
FROM site_license_usage_log WHERE
stop IS NULL AND
NOT EXISTS
(SELECT FROM running_license_info
WHERE running_license_info.value != '{}'
AND running_license_info.project_id=site_license_usage_log.project_id
AND site_license_usage_log.license_id=running_license_info.key::UUID)
)
UPDATE site_license_usage_log SET stop=NOW()
FROM stopped
WHERE site_license_usage_log.license_id=stopped.license_id AND
site_license_usage_log.project_id=stopped.project_id AND
site_license_usage_log.start = stopped.start;
*/
const q = `
WITH missing AS
(
WITH running_license_info AS
(
SELECT
project_id,
(
jsonb_each_text(site_license)
)
.*
FROM
projects
WHERE
state #>> '{state}' = 'running'
)
SELECT
running_license_info.project_id AS project_id,
running_license_info.key::UUID AS license_id
FROM
running_license_info
WHERE
running_license_info.value != '{}'
AND NOT EXISTS
(
SELECT
FROM
site_license_usage_log
WHERE
site_license_usage_log.stop IS NULL
AND site_license_usage_log.project_id = running_license_info.project_id
AND site_license_usage_log.license_id = running_license_info.key::UUID
)
)
INSERT INTO
site_license_usage_log(project_id, license_id, start)
SELECT
project_id,
license_id,
NOW()
FROM
missing;
`;
await (0, query_1.query)({ db, query: q, timeout_s: const_1.TIMEOUT_S });
}
/*
This function ensures that there are no entries of the form
(P,L,time,null) in the site_license_usage_log table with
the project P NOT running. It does this by replacing the null
value in all such cases by NOW().
*/
async function update_site_license_usage_log_not_running_projects(db) {
const dbg = db._dbg("update_site_license_usage_log_not_running_projects");
dbg();
const q = `
WITH stopped AS
(
WITH running_license_info AS
(
SELECT
project_id,
(
jsonb_each_text(site_license)
)
.*
FROM
projects
WHERE
state #>> '{state}' = 'running'
)
SELECT
site_license_usage_log.license_id AS license_id,
site_license_usage_log.project_id AS project_id,
site_license_usage_log.start AS start
FROM
site_license_usage_log
WHERE
stop IS NULL
AND NOT EXISTS
(
SELECT
FROM
running_license_info
WHERE
running_license_info.value != '{}'
AND running_license_info.project_id = site_license_usage_log.project_id
AND site_license_usage_log.license_id = running_license_info.key::UUID
)
)
UPDATE
site_license_usage_log
SET
stop = NOW()
FROM
stopped
WHERE
site_license_usage_log.license_id = stopped.license_id
AND site_license_usage_log.project_id = stopped.project_id
AND site_license_usage_log.start = stopped.start;
`;
await (0, query_1.query)({ db, query: q, timeout_s: const_1.TIMEOUT_S });
}
// Return the number of distinct projects that used the license during the given
// interval of time.
async function number_of_projects_that_used_license(db, license_id, interval) {
const dbg = db._dbg(`number_of_projects_that_used_license("${license_id}",${interval.begin},${interval.end})`);
dbg();
return -1;
}
exports.number_of_projects_that_used_license = number_of_projects_that_used_license;
// Return the total number of hours of usage of the given license by projects during
// the given interval of time.
async function number_of_hours_projects_used_license(db, license_id, interval) {
const dbg = db._dbg(`number_of_hours_projects_used_license("${license_id}",${interval.begin},${interval.end})`);
dbg();
return -1;
}
exports.number_of_hours_projects_used_license = number_of_hours_projects_used_license;
// Given a license_id and an interval of time [begin, end], returns
// all projects that used the license during an interval that overlaps with [begin, end].
// Projects are returned as a list of objects:
// {project_id, [any other fields from the projects table (e.g., title)]}
async function projects_that_used_license(db, license_id, interval, fields = ["project_id"], limit = 500 // at most this many results; results are ordered by project_id.
) {
const dbg = db._dbg(`projects_that_used_license("${license_id}",${interval.begin},${interval.end})`);
dbg([fields, limit]);
return [];
/*
After restricting to a given license, the site_license_usage_log table gives us a set of triples
(project_id, start, stop)
where stop may be null in case the project is still running.
[begin ----------------------- end]
[start ------------- stop]
[start --------------------------------------------- stop]
[start ----------- stop]
[start ----------------stop]
One of these triples overlaps with the interval from begin to end if:
- start <= begin and begin <= stop, i.e. begin is in the interval [start, stop]
- begin = start and start <= end , i.e. starts is in the interval [begin, end]
*/
}
exports.projects_that_used_license = projects_that_used_license;
//# sourceMappingURL=usage-log.js.map