Path: blob/master/src/packages/database/pool/pool.ts
1496 views
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Client, Pool, PoolClient } from "pg";6import { syncSchema } from "@cocalc/database/postgres/schema";7import {8pgdatabase as database,9pghost as host,10pguser as user,11pgssl as ssl,12} from "@cocalc/backend/data";13import { getLogger } from "@cocalc/backend/logger";14import { STATEMENT_TIMEOUT_MS } from "../consts";15import getCachedPool, { CacheTime } from "./cached";16import dbPassword from "./password";1718const L = getLogger("db:pool");1920export * from "./util";2122let pool: Pool | undefined = undefined;2324export default function getPool(cacheTime?: CacheTime): Pool {25if (cacheTime != null) {26return getCachedPool(cacheTime);27}28if (pool == null) {29L.debug(30`creating a new Pool(host:${host}, database:${database}, user:${user}, ssl:${JSON.stringify(ssl)} statement_timeout:${STATEMENT_TIMEOUT_MS}ms)`,31);32pool = new Pool({33password: dbPassword(),34user,35host,36database,37statement_timeout: STATEMENT_TIMEOUT_MS, // fixes https://github.com/sagemathinc/cocalc/issues/601438// the test suite assumes small pool, or there will be random failures sometimes (?)39max: process.env.PGDATABASE == TEST ? 2 : undefined,40ssl,41});4243pool.on("error", (err: Error) => {44L.debug("WARNING: Unexpected error on idle client in PG pool", {45err: err.message,46stack: err.stack,47});48});49const end = pool.end.bind(pool);50pool.end = async () => {51pool = undefined;52end();53};54}55return pool;56}5758// CRITICAL -- the caller *must* call client.release on the client59// that is returned from getTransactionClient()! E.g., for unit testing60// if you don't do this you exhaust the limit of 2 on the pool size,61// (see above) and everything hangs!62export async function getTransactionClient(): Promise<PoolClient> {63const client = await getPoolClient();64try {65await client.query("BEGIN");66} catch (err) {67await client.query("ROLLBACK");68client.release();69throw err;70}71return client;72}7374export async function getPoolClient(): Promise<PoolClient> {75const pool = await getPool();76return await pool.connect();77}7879export function getClient(): Client {80return new Client({ password: dbPassword(), user, host, database, ssl });81}8283// This is used for testing. It ensures the schema is loaded and84// test database is defined.8586// Call this with {reset:true} to reset the ephemeral87// database to a clean state with the schema loaded.88// You *can't* just initEphemeralDatabase({reset:true}) in the pre-amble89// of jest tests though, since all the tests are running in parallel, and90// they would mess up each other's state...91const TEST = "smc_ephemeral_testing_database";92export async function initEphemeralDatabase({93reset,94}: { reset?: boolean } = {}) {95if (database != TEST) {96throw Error(97`You can't use initEphemeralDatabase() and test using the database if the env variabe PGDATABASE is not set to ${TEST}!`,98);99}100const db = new Pool({101password: dbPassword(),102user,103host,104database: "smc",105statement_timeout: STATEMENT_TIMEOUT_MS,106ssl,107});108db.on("error", (err: Error) => {109L.debug("WARNING: Unexpected error on idle client in PG pool", {110err: err.message,111stack: err.stack,112});113});114const { rows } = await db.query(115"SELECT COUNT(*) AS count FROM pg_catalog.pg_database WHERE datname = $1",116[TEST],117);118//await db.query(`DROP DATABASE IF EXISTS ${TEST}`);119const databaseExists = rows[0].count > 0;120if (!databaseExists) {121await db.query(`CREATE DATABASE ${TEST}`);122}123await db.end();124// sync the schema125await syncSchema();126if (databaseExists && reset) {127// Drop all data from all tables for a clean slate.128// Unfortunately, this can take a little while.129await dropAllData();130}131}132133async function dropAllData() {134const pool = getPool();135pool.on("error", (err: Error) => {136L.debug("WARNING: Unexpected error on idle client in PG pool", {137err: err.message,138stack: err.stack,139});140});141if (pool?.["options"]?.database != TEST) {142// safety check!143throw Error(144`You can't use dropAllData() if the env variabe PGDATABASE is not set to ${TEST}!`,145);146}147const client = await pool.connect();148149try {150// Get all table names151const result = await client.query(152"SELECT tablename FROM pg_tables WHERE schemaname='public'",153);154const tableNames = result.rows.map((row) => row.tablename);155await client.query(`TRUNCATE ${tableNames.join(",")}`);156} catch (err) {157throw err;158} finally {159client.release();160}161}162163164