@prairielearn/named-locks 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/index.js +15 -21
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @prairielearn/named-locks
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 4f30b7e: Publish as native ESM
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [4f30b7e]
|
|
12
|
+
- @prairielearn/postgres@2.0.0
|
|
13
|
+
|
|
14
|
+
## 2.0.3
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- c7e6553: Upgrade all JavaScript dependencies
|
|
19
|
+
- Updated dependencies [c7e6553]
|
|
20
|
+
- @prairielearn/postgres@1.9.4
|
|
21
|
+
|
|
3
22
|
## 2.0.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.doWithLock = exports.close = exports.init = exports.pool = void 0;
|
|
4
|
-
const postgres_1 = require("@prairielearn/postgres");
|
|
1
|
+
import { PostgresPool } from '@prairielearn/postgres';
|
|
5
2
|
/*
|
|
6
3
|
* The functions here all identify locks by "name", which is a plain
|
|
7
4
|
* string. The locks use the named_locks DB table. Each lock name
|
|
@@ -44,24 +41,22 @@ const postgres_1 = require("@prairielearn/postgres");
|
|
|
44
41
|
* lock already held. Since there are a finite pool of locks available, this
|
|
45
42
|
* can lead to deadlocks.
|
|
46
43
|
*/
|
|
47
|
-
|
|
44
|
+
export const pool = new PostgresPool();
|
|
48
45
|
let renewIntervalMs = 60_000;
|
|
49
46
|
/**
|
|
50
47
|
* Initializes a new {@link PostgresPool} that will be used to acquire named locks.
|
|
51
48
|
*/
|
|
52
|
-
async function init(pgConfig, idleErrorHandler, namedLocksConfig = {}) {
|
|
49
|
+
export async function init(pgConfig, idleErrorHandler, namedLocksConfig = {}) {
|
|
53
50
|
renewIntervalMs = namedLocksConfig.renewIntervalMs ?? renewIntervalMs;
|
|
54
|
-
await
|
|
55
|
-
await
|
|
51
|
+
await pool.initAsync(pgConfig, idleErrorHandler);
|
|
52
|
+
await pool.queryAsync('CREATE TABLE IF NOT EXISTS named_locks (id bigserial PRIMARY KEY, name text NOT NULL UNIQUE);', {});
|
|
56
53
|
}
|
|
57
|
-
exports.init = init;
|
|
58
54
|
/**
|
|
59
55
|
* Shuts down the database connection pool that was used to acquire locks.
|
|
60
56
|
*/
|
|
61
|
-
async function close() {
|
|
62
|
-
await
|
|
57
|
+
export async function close() {
|
|
58
|
+
await pool.closeAsync();
|
|
63
59
|
}
|
|
64
|
-
exports.close = close;
|
|
65
60
|
/**
|
|
66
61
|
* Acquires the given lock, executes the provided function with the lock held,
|
|
67
62
|
* and releases the lock once the function has executed.
|
|
@@ -70,7 +65,7 @@ exports.close = close;
|
|
|
70
65
|
* function was provided, this function is called and its return value is returned.
|
|
71
66
|
* Otherwise, an error is thrown to indicate that the lock could not be acquired.
|
|
72
67
|
*/
|
|
73
|
-
async function doWithLock(name, options, func) {
|
|
68
|
+
export async function doWithLock(name, options, func) {
|
|
74
69
|
const lock = await getLock(name, { timeout: 0, ...options });
|
|
75
70
|
if (!lock) {
|
|
76
71
|
if (options.onNotAcquired) {
|
|
@@ -87,7 +82,6 @@ async function doWithLock(name, options, func) {
|
|
|
87
82
|
await releaseLock(lock);
|
|
88
83
|
}
|
|
89
84
|
}
|
|
90
|
-
exports.doWithLock = doWithLock;
|
|
91
85
|
/**
|
|
92
86
|
* Internal helper function to get a lock with optional waiting.
|
|
93
87
|
* Do not call directly; use `doWithLock()` instead.
|
|
@@ -96,15 +90,15 @@ exports.doWithLock = doWithLock;
|
|
|
96
90
|
* @param options Optional parameters.
|
|
97
91
|
*/
|
|
98
92
|
async function getLock(name, options) {
|
|
99
|
-
await
|
|
100
|
-
const client = await
|
|
93
|
+
await pool.queryAsync('INSERT INTO named_locks (name) VALUES ($name) ON CONFLICT (name) DO NOTHING;', { name });
|
|
94
|
+
const client = await pool.beginTransactionAsync();
|
|
101
95
|
let acquiredLock = false;
|
|
102
96
|
try {
|
|
103
97
|
if (options.timeout) {
|
|
104
98
|
// SQL doesn't like us trying to use a parameterized query with
|
|
105
99
|
// `SET LOCAL ...`. So, in this very specific case, we do the
|
|
106
100
|
// parameterization ourselves using `escapeLiteral`.
|
|
107
|
-
await
|
|
101
|
+
await pool.queryWithClientAsync(client, `SET LOCAL lock_timeout = ${client.escapeLiteral(options.timeout.toString())}`, {});
|
|
108
102
|
}
|
|
109
103
|
// A stuck lock is a critical issue. To make them easier to debug, we'll
|
|
110
104
|
// include the literal lock name in the query instead of using a
|
|
@@ -115,19 +109,19 @@ async function getLock(name, options) {
|
|
|
115
109
|
const lock_sql = options.timeout
|
|
116
110
|
? `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE;`
|
|
117
111
|
: `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE SKIP LOCKED;`;
|
|
118
|
-
const result = await
|
|
112
|
+
const result = await pool.queryWithClientAsync(client, lock_sql, { name });
|
|
119
113
|
acquiredLock = result.rowCount === 1;
|
|
120
114
|
}
|
|
121
115
|
catch (err) {
|
|
122
116
|
// Something went wrong, so we end the transaction and re-throw the
|
|
123
117
|
// error.
|
|
124
|
-
await
|
|
118
|
+
await pool.endTransactionAsync(client, err);
|
|
125
119
|
throw err;
|
|
126
120
|
}
|
|
127
121
|
if (!acquiredLock) {
|
|
128
122
|
// We didn't acquire the lock so our parent caller will never
|
|
129
123
|
// release it, so we have to end the transaction now.
|
|
130
|
-
await
|
|
124
|
+
await pool.endTransactionAsync(client, null);
|
|
131
125
|
return null;
|
|
132
126
|
}
|
|
133
127
|
let intervalId = null;
|
|
@@ -151,6 +145,6 @@ async function releaseLock(lock) {
|
|
|
151
145
|
if (lock == null)
|
|
152
146
|
throw new Error('lock is null');
|
|
153
147
|
clearInterval(lock.intervalId ?? undefined);
|
|
154
|
-
await
|
|
148
|
+
await pool.endTransactionAsync(lock.client, null);
|
|
155
149
|
}
|
|
156
150
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { PostgresPool, PoolClient } from '@prairielearn/postgres';\nimport { PoolConfig } from 'pg';\n\ninterface NamedLocksConfig {\n /**\n * How often to renew the lock in milliseconds. Defaults to 60 seconds.\n * Auto-renewal must be explicitly enabled on each lock where it is desired.\n *\n */\n renewIntervalMs?: number;\n}\n\ninterface Lock {\n client: PoolClient;\n intervalId: NodeJS.Timeout | null;\n}\n\ninterface LockOptions {\n /** How many milliseconds to wait (anything other than a positive number means forever) */\n timeout?: number;\n\n /**\n * Whether or not this lock should automatically renew itself periodically.\n * By default, locks will not renew themselves.\n *\n * This is mostly useful for locks that may be held for longer than the idle\n * session timeout that's configured for the Postgres database. The lock is\n * \"renewed\" by making a no-op query.\n */\n autoRenew?: boolean;\n}\n\ninterface WithLockOptions<T> extends LockOptions {\n onNotAcquired?: () => Promise<T> | T;\n}\n\n/*\n * The functions here all identify locks by \"name\", which is a plain\n * string. The locks use the named_locks DB table. Each lock name\n * corresponds to a unique table row. To take a lock, we:\n * 1. make sure a row exists for the lock name\n * 2. start a transaction\n * 3. acquire a \"FOR UPDATE\" row lock on the DB row for the named\n * lock (this blocks all other locks on the same row). See\n * https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS\n * 4. return to the caller with the transaction held open\n *\n * The caller then does some work and finally calls releaseLock(),\n * which ends the transaction, thereby releasing the row lock.\n *\n * The flow above will wait indefinitely for the lock to become\n * available. To implement optional timeouts, we set the DB variable\n * `lock_timeout`:\n * https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT\n * If we timeout then we will return an error to the caller.\n *\n * To implement a no-waiting tryLock() we use the PostgreSQL \"SKIP\n * LOCKED\" feature:\n * https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE\n * If we fail to acquire the lock then we immediately release the\n * transaction and return to the caller with `lock = null`. In this\n * case the caller should not call releaseLock().\n *\n * The lock object returned by functions in this module are of the\n * form `{client, done}`, where `client` and `done` are sqldb\n * transaction objects. These are simply the objects we need to end\n * the transaction, which will release the lock.\n *\n * Importantly, we use a separate pool of database connections for acquiring\n * and holding locks. This ensures that we don't end up with a deadlock.\n * For instance, if we have a pool of 10 clients and there are 10 locks held at\n * once, if any code inside of a lock tries to acquire another database client,\n * we'd deadlock if we weren't separating the two pools of connections.\n *\n * You should NEVER try to acquire a lock in code that is executing with another\n * lock already held. Since there are a finite pool of locks available, this\n * can lead to deadlocks.\n */\n\nexport const pool = new PostgresPool();\nlet renewIntervalMs = 60_000;\n\n/**\n * Initializes a new {@link PostgresPool} that will be used to acquire named locks.\n */\nexport async function init(\n pgConfig: PoolConfig,\n idleErrorHandler: (error: Error, client: PoolClient) => void,\n namedLocksConfig: NamedLocksConfig = {},\n) {\n renewIntervalMs = namedLocksConfig.renewIntervalMs ?? renewIntervalMs;\n await pool.initAsync(pgConfig, idleErrorHandler);\n await pool.queryAsync(\n 'CREATE TABLE IF NOT EXISTS named_locks (id bigserial PRIMARY KEY, name text NOT NULL UNIQUE);',\n {},\n );\n}\n\n/**\n * Shuts down the database connection pool that was used to acquire locks.\n */\nexport async function close() {\n await pool.closeAsync();\n}\n\n/**\n * Acquires the given lock, executes the provided function with the lock held,\n * and releases the lock once the function has executed.\n *\n * If the lock cannot be acquired, the function is not executed. If an `onNotAcquired`\n * function was provided, this function is called and its return value is returned.\n * Otherwise, an error is thrown to indicate that the lock could not be acquired.\n */\nexport async function doWithLock<T, U = never>(\n name: string,\n options: WithLockOptions<U>,\n func: () => Promise<T>,\n): Promise<T | U> {\n const lock = await getLock(name, { timeout: 0, ...options });\n\n if (!lock) {\n if (options.onNotAcquired) {\n return await options.onNotAcquired();\n } else {\n throw new Error(`failed to acquire lock: ${name}`);\n }\n }\n\n try {\n return await func();\n } finally {\n await releaseLock(lock);\n }\n}\n\n/**\n * Internal helper function to get a lock with optional waiting.\n * Do not call directly; use `doWithLock()` instead.\n *\n * @param name The name of the lock to acquire.\n * @param options Optional parameters.\n */\nasync function getLock(name: string, options: LockOptions) {\n await pool.queryAsync(\n 'INSERT INTO named_locks (name) VALUES ($name) ON CONFLICT (name) DO NOTHING;',\n { name },\n );\n\n const client = await pool.beginTransactionAsync();\n\n let acquiredLock = false;\n try {\n if (options.timeout) {\n // SQL doesn't like us trying to use a parameterized query with\n // `SET LOCAL ...`. So, in this very specific case, we do the\n // parameterization ourselves using `escapeLiteral`.\n await pool.queryWithClientAsync(\n client,\n `SET LOCAL lock_timeout = ${client.escapeLiteral(options.timeout.toString())}`,\n {},\n );\n }\n\n // A stuck lock is a critical issue. To make them easier to debug, we'll\n // include the literal lock name in the query instead of using a\n // parameterized query. The lock name should never include PII, so it's\n // safe if it shows up in plaintext in logs, telemetry, error messages,\n // etc.\n const lockNameLiteral = client.escapeLiteral(name);\n const lock_sql = options.timeout\n ? `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE;`\n : `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE SKIP LOCKED;`;\n const result = await pool.queryWithClientAsync(client, lock_sql, { name });\n acquiredLock = result.rowCount === 1;\n } catch (err) {\n // Something went wrong, so we end the transaction and re-throw the\n // error.\n await pool.endTransactionAsync(client, err as Error);\n throw err;\n }\n\n if (!acquiredLock) {\n // We didn't acquire the lock so our parent caller will never\n // release it, so we have to end the transaction now.\n await pool.endTransactionAsync(client, null);\n return null;\n }\n\n let intervalId = null;\n if (options.autoRenew) {\n // Periodically \"renew\" the lock by making a query.\n intervalId = setInterval(() => {\n client.query('SELECT 1;').catch(() => {});\n }, renewIntervalMs);\n }\n\n // We successfully acquired the lock, so we return with the transaction\n // help open. The caller will be responsible for releasing the lock and\n // ending the transaction.\n return { client, intervalId };\n}\n\n/**\n * Release a lock.\n *\n * @param lock A previously-acquired lock.\n */\nasync function releaseLock(lock: Lock) {\n if (lock == null) throw new Error('lock is null');\n clearInterval(lock.intervalId ?? undefined);\n await pool.endTransactionAsync(lock.client, null);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/named-locks",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"main": "./dist/index.js",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
@@ -13,11 +14,11 @@
|
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
16
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
16
|
-
"@types/node": "^20.
|
|
17
|
+
"@types/node": "^20.12.2",
|
|
17
18
|
"typescript": "^5.4.3"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@prairielearn/postgres": "^
|
|
21
|
-
"pg": "^8.11.
|
|
21
|
+
"@prairielearn/postgres": "^2.0.0",
|
|
22
|
+
"pg": "^8.11.4"
|
|
22
23
|
}
|
|
23
24
|
}
|