(core) Add API endpoint to get site usage summary

Summary:
The summary includes a count of documents that are approaching
limits, in grace period, or delete-only. The endpoint is only accessible
to site owners, and is currently unused. A follow-up diff will add usage
banners to the site home page, which will use the response from the
endpoint to communicate usage information to owners.

Test Plan: Browser and server tests.

Reviewers: alexmojaki

Reviewed By: alexmojaki

Differential Revision: https://phab.getgrist.com/D3420
This commit is contained in:
George Gevoian
2022-05-16 10:41:12 -07:00
parent cbdbe3f605
commit f48d579f64
23 changed files with 559 additions and 185 deletions

View File

@@ -1,14 +1,14 @@
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {DocumentMetadata, HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import * as log from 'app/server/lib/log';
/**
* HostedMetadataManager handles pushing document metadata changes to the Home database when
* a doc is updated. Currently only updates doc updatedAt time.
* a doc is updated. Currently updates doc updatedAt time and usage.
*/
export class HostedMetadataManager {
// updatedAt times as UTC ISO strings mapped by docId.
private _updatedAt: {[docId: string]: string} = {};
// Document metadata mapped by docId.
private _metadata: {[docId: string]: DocumentMetadata} = {};
// Set if the class holder is closing and no further pushes should be scheduled.
private _closing: boolean = false;
@@ -22,60 +22,78 @@ export class HostedMetadataManager {
// Maintains the update Promise to wait on it if the class is closing.
private _push: Promise<any>|null;
// The default delay in milliseconds between metadata pushes to the database.
private readonly _minPushDelayMs: number;
/**
* Create an instance of HostedMetadataManager.
* The minPushDelay is the delay in seconds between metadata pushes to the database.
* The minPushDelay is the default delay in seconds between metadata pushes to the database.
*/
constructor(private _dbManager: HomeDBManager, private _minPushDelay: number = 60) {}
constructor(private _dbManager: HomeDBManager, minPushDelay: number = 60) {
this._minPushDelayMs = minPushDelay * 1000;
}
/**
* Close the manager. Send out any pending updates and prevent more from being scheduled.
*/
public async close(): Promise<void> {
// Finish up everything outgoing
this._closing = true; // Pushes will no longer be scheduled.
// Pushes will no longer be scheduled.
this._closing = true;
// Wait for outgoing pushes to finish before proceeding.
if (this._push) { await this._push; }
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
// Since an update was scheduled, perform one final update now.
this._update();
if (this._push) { await this._push; }
}
if (this._push) { await this._push; }
}
/**
* Schedule a call to _update some time from now. When the update is made, it will
* store the given timestamp in the updated_at column of the docs table for the
* specified document. Timestamp should be an ISO 8601 format time, in UTC, e.g.
* the output of new Date().toISOString()
* store the given metadata in the updated_at and usage columns of the docs table for
* the specified document.
*
* If `minimizeDelay` is true, the push will be scheduled with minimum delay (0ms) and
* will cancel/overwrite an already scheduled push (if present).
*/
public scheduleUpdate(docId: string, timestamp: string): void {
// Update updatedAt even if an update is already scheduled - if the update has not yet occurred,
// the more recent updatedAt time will be used.
this._updatedAt[docId] = timestamp;
if (this._timeout || this._closing) { return; }
const minDelay = this._minPushDelay * 1000;
// Set the push to occur at least the minDelay after the last push time.
const delay = Math.round(minDelay - (Date.now() - this._lastPushTime));
this._timeout = setTimeout(() => this._update(), delay < 0 ? 0 : delay);
public scheduleUpdate(docId: string, metadata: DocumentMetadata, minimizeDelay = false): void {
if (this._closing) { return; }
// Update metadata even if an update is already scheduled - if the update has not yet occurred,
// the more recent metadata will be used.
this._setOrUpdateMetadata(docId, metadata);
if (this._timeout && !minimizeDelay) { return; }
this._schedulePush(minimizeDelay ? 0 : undefined);
}
public setDocsUpdatedAt(docUpdateMap: {[docId: string]: string}): Promise<any> {
return this._dbManager.setDocsUpdatedAt(docUpdateMap);
public setDocsMetadata(docUpdateMap: {[docId: string]: DocumentMetadata}): Promise<any> {
return this._dbManager.setDocsMetadata(docUpdateMap);
}
/**
* Push all metadata updates to the database.
*/
private _update(): void {
if (this._push) { return; }
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (this._push) { return; }
this._push = this._performUpdate()
.catch(err => { log.error("HostedMetadataManager error performing update: ", err); })
.then(() => { this._push = null; });
.catch(err => {
log.error("HostedMetadataManager error performing update: ", err);
})
.then(() => {
this._push = null;
if (!this._closing && !this._timeout && Object.keys(this._metadata).length !== 0) {
// If we have metadata that hasn't been pushed up yet, but no push scheduled,
// go ahead and schedule an immediate push. This can happen if `scheduleUpdate`
// is called frequently with minimizeDelay set to true, particularly when
// _performUpdate is taking a bit longer than normal to complete.
this._schedulePush(0);
}
});
}
/**
@@ -84,9 +102,40 @@ export class HostedMetadataManager {
*/
private async _performUpdate(): Promise<void> {
// Await the database if it is not yet connected.
const docUpdates = this._updatedAt;
this._updatedAt = {};
const docUpdates = this._metadata;
this._metadata = {};
this._lastPushTime = Date.now();
await this.setDocsUpdatedAt(docUpdates);
await this.setDocsMetadata(docUpdates);
}
/**
* Schedule a metadata push.
*
* If `delayMs` is specified, the push will be scheduled to occur at least that
* number of milliseconds in the future. If `delayMs` is unspecified, the push
* will be scheduled to occur at least `_minPushDelayMs` after the last push time.
*
* If called while a push is already scheduled, that push will be cancelled and
* replaced with this one.
*/
private _schedulePush(delayMs?: number): void {
if (delayMs === undefined) {
delayMs = Math.round(this._minPushDelayMs - (Date.now() - this._lastPushTime));
}
if (this._timeout) { clearTimeout(this._timeout); }
this._timeout = setTimeout(() => this._update(), delayMs < 0 ? 0 : delayMs);
}
/**
* Adds `docId` and its `metadata` to the list of queued updates, merging any existing values.
*/
private _setOrUpdateMetadata(docId: string, metadata: DocumentMetadata): void {
if (!this._metadata[docId]) {
this._metadata[docId] = metadata;
} else {
const {updatedAt, usage} = metadata;
if (updatedAt) { this._metadata[docId].updatedAt = updatedAt; }
if (usage !== undefined) { this._metadata[docId].usage = usage; }
}
}
}