UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

194 lines (165 loc) 5.34 kB
/* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ /* Does the queries to update changefeeds, deduplicating across both all changefeeds and a small interval of time. */ // set to false to completely disable for debugging/testing const THROTTLE: boolean = true; // 10ms when running unit tests, still throttle, but make it quick. // Otherwise, we default to 250ms, which is enough to be massively // useful, but also not noticed by user. let THROTTLE_MS: number = process.env.SMC_TEST ? 10 : 500; // THROTTLE_MS can be overridden the POSTGRES_THROTTLE_CHANGEFEED_MS // environment variable. if (process.env.POSTGRES_THROTTLE_CHANGEFEED_MS != null) { THROTTLE_MS = parseInt(process.env.POSTGRES_THROTTLE_CHANGEFEED_MS); } import { EventEmitter } from "events"; import { callback } from "awaiting"; import { once } from "smc-util/async-utils"; import { close, copy } from "smc-util/misc"; const { one_result, all_results } = require("../postgres-base"); import { PostgreSQL, QueryWhere } from "./types"; interface QueryOpts { db: PostgreSQL; select: string[]; table: string; where: QueryWhere; one: boolean; } interface TableQuery { select: string[]; where: QueryWhere; } function key(obj: { [key: string]: any }): string { return `query-${JSON.stringify(obj)}`; } type State = "ready" | "closed"; class ThrottledTableQueue extends EventEmitter { private table: string; private queue: { [key: string]: TableQuery } = {}; private db: PostgreSQL; private process_timer: any; private interval_ms: number; private state: State = "ready"; constructor(db: PostgreSQL, table: string, interval_ms: number) { super(); this.db = db; this.table = table; this.interval_ms = interval_ms; // client listens for results of query -- if queries pile up, and // there are many tables, the default of 10 can easily be exceeded. this.setMaxListeners(100); } private dbg(f): Function { return this.db._dbg(`ThrottledTableQueue('${this.table}').${f}`); } public close(): void { if (this.state == "closed") { return; } if (this.process_timer != null) { clearTimeout(this.process_timer); } for (const k in this.queue) { this.emit(k, "closed"); } this.emit("closed"); this.removeAllListeners(); close(this); this.state = "closed"; } public enqueue(query: TableQuery): string { if (this.state == "closed") { throw Error("trying to enqueue after close"); } const k = key(query); this.queue[k] = query; if (this.process_timer == null) { this.dbg("enqueue")(`will process queue in ${this.interval_ms}ms...`); this.process_timer = setTimeout( this.process_queue.bind(this), this.interval_ms ); } return k; } private async process_queue(): Promise<void> { const dbg = this.dbg("process_queue"); delete this.process_timer; // it just fired // First time we just doing them one at a time. // FURTHER WORK: we could instead do ALL queries simultaneously as // a single query, or at least do them in parallel... // Make a copy of whatever is queued up at this moment in time, // then clear the queue. We are responsible for handling all // queries in the queue now, but nothing further. If queries are // slow, it can easily be the case that process_queue is // called several times at once. However, that's fine, since // each call is responsible for only the part of the queue that // existed when it was called. const queue = copy(this.queue); this.queue = {}; for (const k in queue) { dbg(k); const { select, where } = queue[k]; try { const result = await callback( one_query, this.db, select, this.table, where ); if (this.state == "closed") return; dbg("success", k); this.emit(k, undefined, result); } catch (err) { if (this.state == "closed") return; dbg("fail", k); this.emit(k, err, undefined); } } } } const throttled_table_queues: { [table: string]: ThrottledTableQueue } = {}; function throttled_table_queue( db: PostgreSQL, table: string, interval_ms: number ): ThrottledTableQueue { if (throttled_table_queues[table] != null) { return throttled_table_queues[table]; } return (throttled_table_queues[table] = new ThrottledTableQueue( db, table, interval_ms )); } export async function query(opts: QueryOpts): Promise<any> { if (THROTTLE && opts.one) { const Table = throttled_table_queue(opts.db, opts.table, THROTTLE_MS); const k: string = Table.enqueue({ select: opts.select, where: opts.where }); const [err, result] = await once(Table, k); if (err != null) { throw err; } return result; } return await callback( opts.one ? one_query : all_query, opts.db, opts.select, opts.table, opts.where ); } function all_query(db, select, table, where, cb): void { db._query({ select, table, where, cb: all_results(cb) }); } function one_query(db, select, table, where, cb): void { db._query({ select, table, where, cb: one_result(cb) }); }