CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/pool/cached.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Caches queries for a certain amount of time.
8
Also, if there are multiple queries coming in
9
at the same time for the same thing, only
10
one actually goes to the database.
11
12
IMPORTANT: This *only* caches a query if the query actually
13
returns at least one row. If the query returns nothing,
14
that fact is not cached. This is usually what we want, e.g.,
15
if somebody access https://cocalc.com/wstein/myproject, and
16
notices that myproject isn't the set name, then they may
17
set it and immediately try https://cocalc.com/wstein/myproject again.
18
In that case, we don't want to the cache to mean that they don't
19
see the page for a while. On the other hand, if querying for the
20
project that myproject resolves to is cached for a minute that is
21
fine, since this would only be a problem when they change the name
22
of multiple projects.
23
*/
24
25
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
26
import LRU from "lru-cache";
27
import { Pool } from "pg";
28
29
import getLogger from "@cocalc/backend/logger";
30
import getPool from "./pool";
31
32
const L = getLogger("db:pool:cached");
33
34
const MAX_AGE_S = {
35
"": 0, // no cache at all
36
short: 5, // just to avoid a very rapid fire sequence of re-requests
37
medium: 15, // usually use this.
38
long: 30,
39
minutes: 10 * 60, // a really long time -- for now, 10 minutes. example, the owner of a project.
40
infinite: 60 * 60 * 24 * 365, // effectively forever; e.g., getting path from share id is really just a reversed sha1 hash, so can't change.
41
} as const;
42
43
export type CacheTime = keyof typeof MAX_AGE_S;
44
45
const caches = new Map<CacheTime, LRU<string, any>>();
46
47
for (const cacheTime in MAX_AGE_S) {
48
if (!cacheTime) continue;
49
caches[cacheTime] = new LRU<string, any>({
50
max: 1000,
51
ttl: 1000 * MAX_AGE_S[cacheTime],
52
});
53
}
54
55
const cachedQuery = reuseInFlight(async (cacheTime: CacheTime, ...args) => {
56
const cache = caches[cacheTime];
57
if (cache == null) {
58
throw Error(`invalid cache "${cacheTime}"`);
59
}
60
const key = JSON.stringify(args);
61
if (cache.has(key)) {
62
// console.log(`YES - using cache for ${key}`);
63
return cache.get(key);
64
}
65
// console.log(`NOT using cache for ${key}`);
66
67
const pool = getPool();
68
try {
69
// @ts-ignore - no clue how to typescript this.
70
const result = await pool.query(...args);
71
if (result.rows.length > 0) {
72
// We only cache query if it returned something.
73
cache.set(key, result);
74
}
75
return result;
76
} catch (err) {
77
L.error(`cachedQuery error: ${err}`);
78
throw err;
79
}
80
});
81
82
export default function getCachedPool(cacheTime: CacheTime) {
83
if (!cacheTime) {
84
return getPool();
85
}
86
return {
87
query: async (...args) => await cachedQuery(cacheTime, ...args),
88
} as any as Pool; // obviously not really a Pool, but is enough for what we're doing.
89
}
90
91