UNPKG

@cocalc/database

Version:

CoCalc: code for working with our PostgreSQL database

264 lines (234 loc) 10.7 kB
"use strict"; /* * 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