@prairielearn/named-locks 1.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.
File without changes
@@ -0,0 +1,49 @@
1
+ /// <reference types="node" />
2
+ import { PoolClient } from '@prairielearn/postgres';
3
+ import { PoolConfig } from 'pg';
4
+ interface Lock {
5
+ client: PoolClient;
6
+ }
7
+ interface LockOptions {
8
+ /** How many milliseconds to wait (anything other than a positive number means forever) */
9
+ timeout?: number;
10
+ }
11
+ /**
12
+ * Initializes a new {@link PostgresPool} that will be used to acquire named locks.
13
+ */
14
+ export declare function init(pgConfig: PoolConfig, idleErrorHandler: (error: Error, client: PoolClient) => void): Promise<void>;
15
+ /**
16
+ * Shuts down the database connection pool that was used to acquire locks.
17
+ */
18
+ export declare function close(): Promise<void>;
19
+ /**
20
+ * Try to acquire a lock and either succeed if it's available or
21
+ * return immediately if not. If a lock is acquired then it must
22
+ * must later be released by releaseLock(). If a lock is not acquired
23
+ * (it is null) then releaseLock() should not be called.
24
+ *
25
+ * @param name The name of the lock to acquire.
26
+ */
27
+ export declare function tryLockAsync(name: string): Promise<Lock | null>;
28
+ export declare const tryLock: (arg1: string, callback: (err: NodeJS.ErrnoException, result: Lock | null) => void) => void;
29
+ /**
30
+ * Wait until a lock can be successfully acquired.
31
+ *
32
+ * @param name The name of the lock to acquire.
33
+ * @param options
34
+ */
35
+ export declare function waitLockAsync(name: string, options: LockOptions): Promise<Lock>;
36
+ export declare const waitLock: (arg1: string, arg2: LockOptions, callback: (err: NodeJS.ErrnoException | null, result: Lock) => void) => void;
37
+ /**
38
+ * Release a lock.
39
+ *
40
+ * @param lock A previously-acquired lock.
41
+ */
42
+ export declare function releaseLockAsync(lock: Lock): Promise<void>;
43
+ export declare const releaseLock: (arg1: Lock, callback: (err: NodeJS.ErrnoException) => void) => void;
44
+ /**
45
+ * Acquires the given lock, executes the provided function with the lock held,
46
+ * and releases the lock once the function has executed.
47
+ */
48
+ export declare function doWithLock<T>(name: string, options: LockOptions, func: () => Promise<T>): Promise<T>;
49
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.doWithLock = exports.releaseLock = exports.releaseLockAsync = exports.waitLock = exports.waitLockAsync = exports.tryLock = exports.tryLockAsync = exports.close = exports.init = void 0;
7
+ const util_1 = __importDefault(require("util"));
8
+ const postgres_1 = require("@prairielearn/postgres");
9
+ /*
10
+ * The functions here all identify locks by "name", which is a plain
11
+ * string. The locks use the named_locks DB table. Each lock name
12
+ * corresponds to a unique table row. To take a lock, we:
13
+ * 1. make sure a row exists for the lock name
14
+ * 2. start a transaction
15
+ * 3. acquire a "FOR UPDATE" row lock on the DB row for the named
16
+ * lock (this blocks all other locks on the same row). See
17
+ * https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS
18
+ * 4. return to the caller with the transaction held open
19
+ *
20
+ * The caller then does some work and finally calls releaseLock(),
21
+ * which ends the transaction, thereby releasing the row lock.
22
+ *
23
+ * The flow above will wait indefinitely for the lock to become
24
+ * available. To implement optional timeouts, we set the DB variable
25
+ * `lock_timeout`:
26
+ * https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT
27
+ * If we timeout then we will return an error to the caller.
28
+ *
29
+ * To implement a no-waiting tryLock() we use the PostgreSQL "SKIP
30
+ * LOCKED" feature:
31
+ * https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE
32
+ * If we fail to acquire the lock then we immediately release the
33
+ * transaction and return to the caller with `lock = null`. In this
34
+ * case the caller should not call releaseLock().
35
+ *
36
+ * The lock object returned by functions in this module are of the
37
+ * form `{client, done}`, where `client` and `done` are sqldb
38
+ * transaction objects. These are simply the objects we need to end
39
+ * the transaction, which will release the lock.
40
+ *
41
+ * Importantly, we use a separate pool of database connections for acquiring
42
+ * and holding locks. This ensures that we don't end up with a deadlock.
43
+ * For instance, if we have a pool of 10 clients and there are 10 locks held at
44
+ * once, if any code inside of a lock tries to acquire another database client,
45
+ * we'd deadlock if we weren't separating the two pools of connections.
46
+ *
47
+ * You should NEVER try to acquire a lock in code that is executing with another
48
+ * lock already held. Since there are a finite pool of locks available, this
49
+ * can lead to deadlocks.
50
+ */
51
+ const pool = new postgres_1.PostgresPool();
52
+ /**
53
+ * Initializes a new {@link PostgresPool} that will be used to acquire named locks.
54
+ */
55
+ async function init(pgConfig, idleErrorHandler) {
56
+ await pool.initAsync(pgConfig, idleErrorHandler);
57
+ await pool.queryAsync('CREATE TABLE IF NOT EXISTS named_locks (id bigserial PRIMARY KEY, name text NOT NULL UNIQUE);', {});
58
+ }
59
+ exports.init = init;
60
+ /**
61
+ * Shuts down the database connection pool that was used to acquire locks.
62
+ */
63
+ async function close() {
64
+ await pool.closeAsync();
65
+ }
66
+ exports.close = close;
67
+ /**
68
+ * Try to acquire a lock and either succeed if it's available or
69
+ * return immediately if not. If a lock is acquired then it must
70
+ * must later be released by releaseLock(). If a lock is not acquired
71
+ * (it is null) then releaseLock() should not be called.
72
+ *
73
+ * @param name The name of the lock to acquire.
74
+ */
75
+ async function tryLockAsync(name) {
76
+ return getLock(name, { wait: false });
77
+ }
78
+ exports.tryLockAsync = tryLockAsync;
79
+ exports.tryLock = util_1.default.callbackify(tryLockAsync);
80
+ /**
81
+ * Wait until a lock can be successfully acquired.
82
+ *
83
+ * @param name The name of the lock to acquire.
84
+ * @param options
85
+ */
86
+ async function waitLockAsync(name, options) {
87
+ const internalOptions = {
88
+ wait: true,
89
+ timeout: options.timeout || 0,
90
+ };
91
+ const lock = await getLock(name, internalOptions);
92
+ if (lock == null)
93
+ throw new Error(`failed to acquire lock: ${name}`);
94
+ return lock;
95
+ }
96
+ exports.waitLockAsync = waitLockAsync;
97
+ exports.waitLock = util_1.default.callbackify(waitLockAsync);
98
+ /**
99
+ * Release a lock.
100
+ *
101
+ * @param lock A previously-acquired lock.
102
+ */
103
+ async function releaseLockAsync(lock) {
104
+ if (lock == null)
105
+ throw new Error('lock is null');
106
+ await pool.endTransactionAsync(lock.client, null);
107
+ }
108
+ exports.releaseLockAsync = releaseLockAsync;
109
+ exports.releaseLock = util_1.default.callbackify(releaseLockAsync);
110
+ /**
111
+ * Acquires the given lock, executes the provided function with the lock held,
112
+ * and releases the lock once the function has executed.
113
+ */
114
+ async function doWithLock(name, options, func) {
115
+ const lock = await waitLockAsync(name, options);
116
+ try {
117
+ return await func();
118
+ }
119
+ finally {
120
+ await releaseLockAsync(lock);
121
+ }
122
+ }
123
+ exports.doWithLock = doWithLock;
124
+ /**
125
+ * Internal helper function to get a lock with optional
126
+ * waiting. Do not call directly, but use tryLock() or waitLock()
127
+ * instead.
128
+ *
129
+ * @param name The name of the lock to acquire.
130
+ * @param options Optional parameters.
131
+ */
132
+ async function getLock(name, options) {
133
+ await pool.queryAsync('INSERT INTO named_locks (name) VALUES ($name) ON CONFLICT (name) DO NOTHING;', { name });
134
+ const client = await pool.beginTransactionAsync();
135
+ let acquiredLock = false;
136
+ try {
137
+ if (options.wait && options.timeout > 0) {
138
+ // SQL doesn't like us trying to use a parameterized query with
139
+ // `SET LOCAL ...`. So, in this very specific case, we do the
140
+ // parameterization ourselves using `escapeLiteral`.
141
+ await pool.queryWithClientAsync(client, `SET LOCAL lock_timeout = ${client.escapeLiteral(options.timeout.toString())}`, {});
142
+ }
143
+ // A stuck lock is a critical issue. To make them easier to debug, we'll
144
+ // include the literal lock name in the query instead of using a
145
+ // parameterized query. The lock name should never include PII, so it's
146
+ // safe if it shows up in plaintext in logs, telemetry, error messages,
147
+ // etc.
148
+ const lockNameLiteral = client.escapeLiteral(name);
149
+ const lock_sql = options.wait
150
+ ? `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE;`
151
+ : `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE SKIP LOCKED;`;
152
+ const result = await pool.queryWithClientAsync(client, lock_sql, { name });
153
+ acquiredLock = result.rowCount === 1;
154
+ }
155
+ catch (err) {
156
+ // Something went wrong, so we end the transaction and re-throw the
157
+ // error.
158
+ await pool.endTransactionAsync(client, err);
159
+ throw err;
160
+ }
161
+ if (!acquiredLock) {
162
+ // We didn't acquire the lock so our parent caller will never
163
+ // release it, so we have to end the transaction now.
164
+ await pool.endTransactionAsync(client, null);
165
+ return null;
166
+ }
167
+ // We successfully acquired the lock, so we return with the transaction
168
+ // help open. The caller will be responsible for releasing the lock and
169
+ // ending the transaction.
170
+ return { client };
171
+ }
172
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@prairielearn/named-locks",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "dev": "tsc --watch --preserveWatchOutput"
8
+ },
9
+ "devDependencies": {
10
+ "@prairielearn/tsconfig": "*",
11
+ "@types/node": "^18.14.2",
12
+ "typescript": "^4.9.4"
13
+ },
14
+ "dependencies": {
15
+ "@prairielearn/postgres": "^1.2.0"
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,209 @@
1
+ import util from 'util';
2
+ import { PostgresPool, PoolClient } from '@prairielearn/postgres';
3
+ import { PoolConfig } from 'pg';
4
+
5
+ interface Lock {
6
+ client: PoolClient;
7
+ }
8
+
9
+ interface LockOptions {
10
+ /** How many milliseconds to wait (anything other than a positive number means forever) */
11
+ timeout?: number;
12
+ }
13
+
14
+ type InternalLockOptions =
15
+ | {
16
+ wait: false;
17
+ }
18
+ | {
19
+ wait: true;
20
+ timeout: number;
21
+ };
22
+
23
+ /*
24
+ * The functions here all identify locks by "name", which is a plain
25
+ * string. The locks use the named_locks DB table. Each lock name
26
+ * corresponds to a unique table row. To take a lock, we:
27
+ * 1. make sure a row exists for the lock name
28
+ * 2. start a transaction
29
+ * 3. acquire a "FOR UPDATE" row lock on the DB row for the named
30
+ * lock (this blocks all other locks on the same row). See
31
+ * https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS
32
+ * 4. return to the caller with the transaction held open
33
+ *
34
+ * The caller then does some work and finally calls releaseLock(),
35
+ * which ends the transaction, thereby releasing the row lock.
36
+ *
37
+ * The flow above will wait indefinitely for the lock to become
38
+ * available. To implement optional timeouts, we set the DB variable
39
+ * `lock_timeout`:
40
+ * https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT
41
+ * If we timeout then we will return an error to the caller.
42
+ *
43
+ * To implement a no-waiting tryLock() we use the PostgreSQL "SKIP
44
+ * LOCKED" feature:
45
+ * https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE
46
+ * If we fail to acquire the lock then we immediately release the
47
+ * transaction and return to the caller with `lock = null`. In this
48
+ * case the caller should not call releaseLock().
49
+ *
50
+ * The lock object returned by functions in this module are of the
51
+ * form `{client, done}`, where `client` and `done` are sqldb
52
+ * transaction objects. These are simply the objects we need to end
53
+ * the transaction, which will release the lock.
54
+ *
55
+ * Importantly, we use a separate pool of database connections for acquiring
56
+ * and holding locks. This ensures that we don't end up with a deadlock.
57
+ * For instance, if we have a pool of 10 clients and there are 10 locks held at
58
+ * once, if any code inside of a lock tries to acquire another database client,
59
+ * we'd deadlock if we weren't separating the two pools of connections.
60
+ *
61
+ * You should NEVER try to acquire a lock in code that is executing with another
62
+ * lock already held. Since there are a finite pool of locks available, this
63
+ * can lead to deadlocks.
64
+ */
65
+
66
+ const pool = new PostgresPool();
67
+
68
+ /**
69
+ * Initializes a new {@link PostgresPool} that will be used to acquire named locks.
70
+ */
71
+ export async function init(
72
+ pgConfig: PoolConfig,
73
+ idleErrorHandler: (error: Error, client: PoolClient) => void
74
+ ) {
75
+ await pool.initAsync(pgConfig, idleErrorHandler);
76
+ await pool.queryAsync(
77
+ 'CREATE TABLE IF NOT EXISTS named_locks (id bigserial PRIMARY KEY, name text NOT NULL UNIQUE);',
78
+ {}
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Shuts down the database connection pool that was used to acquire locks.
84
+ */
85
+ export async function close() {
86
+ await pool.closeAsync();
87
+ }
88
+
89
+ /**
90
+ * Try to acquire a lock and either succeed if it's available or
91
+ * return immediately if not. If a lock is acquired then it must
92
+ * must later be released by releaseLock(). If a lock is not acquired
93
+ * (it is null) then releaseLock() should not be called.
94
+ *
95
+ * @param name The name of the lock to acquire.
96
+ */
97
+ export async function tryLockAsync(name: string): Promise<Lock | null> {
98
+ return getLock(name, { wait: false });
99
+ }
100
+
101
+ export const tryLock = util.callbackify(tryLockAsync);
102
+
103
+ /**
104
+ * Wait until a lock can be successfully acquired.
105
+ *
106
+ * @param name The name of the lock to acquire.
107
+ * @param options
108
+ */
109
+ export async function waitLockAsync(name: string, options: LockOptions): Promise<Lock> {
110
+ const internalOptions = {
111
+ wait: true,
112
+ timeout: options.timeout || 0,
113
+ };
114
+
115
+ const lock = await getLock(name, internalOptions);
116
+ if (lock == null) throw new Error(`failed to acquire lock: ${name}`);
117
+ return lock;
118
+ }
119
+
120
+ export const waitLock = util.callbackify(waitLockAsync);
121
+
122
+ /**
123
+ * Release a lock.
124
+ *
125
+ * @param lock A previously-acquired lock.
126
+ */
127
+ export async function releaseLockAsync(lock: Lock) {
128
+ if (lock == null) throw new Error('lock is null');
129
+ await pool.endTransactionAsync(lock.client, null);
130
+ }
131
+
132
+ export const releaseLock = util.callbackify(releaseLockAsync);
133
+
134
+ /**
135
+ * Acquires the given lock, executes the provided function with the lock held,
136
+ * and releases the lock once the function has executed.
137
+ */
138
+ export async function doWithLock<T>(
139
+ name: string,
140
+ options: LockOptions,
141
+ func: () => Promise<T>
142
+ ): Promise<T> {
143
+ const lock = await waitLockAsync(name, options);
144
+ try {
145
+ return await func();
146
+ } finally {
147
+ await releaseLockAsync(lock);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Internal helper function to get a lock with optional
153
+ * waiting. Do not call directly, but use tryLock() or waitLock()
154
+ * instead.
155
+ *
156
+ * @param name The name of the lock to acquire.
157
+ * @param options Optional parameters.
158
+ */
159
+ async function getLock(name: string, options: InternalLockOptions) {
160
+ await pool.queryAsync(
161
+ 'INSERT INTO named_locks (name) VALUES ($name) ON CONFLICT (name) DO NOTHING;',
162
+ { name }
163
+ );
164
+
165
+ const client = await pool.beginTransactionAsync();
166
+
167
+ let acquiredLock = false;
168
+ try {
169
+ if (options.wait && options.timeout > 0) {
170
+ // SQL doesn't like us trying to use a parameterized query with
171
+ // `SET LOCAL ...`. So, in this very specific case, we do the
172
+ // parameterization ourselves using `escapeLiteral`.
173
+ await pool.queryWithClientAsync(
174
+ client,
175
+ `SET LOCAL lock_timeout = ${client.escapeLiteral(options.timeout.toString())}`,
176
+ {}
177
+ );
178
+ }
179
+
180
+ // A stuck lock is a critical issue. To make them easier to debug, we'll
181
+ // include the literal lock name in the query instead of using a
182
+ // parameterized query. The lock name should never include PII, so it's
183
+ // safe if it shows up in plaintext in logs, telemetry, error messages,
184
+ // etc.
185
+ const lockNameLiteral = client.escapeLiteral(name);
186
+ const lock_sql = options.wait
187
+ ? `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE;`
188
+ : `SELECT * FROM named_locks WHERE name = ${lockNameLiteral} FOR UPDATE SKIP LOCKED;`;
189
+ const result = await pool.queryWithClientAsync(client, lock_sql, { name });
190
+ acquiredLock = result.rowCount === 1;
191
+ } catch (err) {
192
+ // Something went wrong, so we end the transaction and re-throw the
193
+ // error.
194
+ await pool.endTransactionAsync(client, err as Error);
195
+ throw err;
196
+ }
197
+
198
+ if (!acquiredLock) {
199
+ // We didn't acquire the lock so our parent caller will never
200
+ // release it, so we have to end the transaction now.
201
+ await pool.endTransactionAsync(client, null);
202
+ return null;
203
+ }
204
+
205
+ // We successfully acquired the lock, so we return with the transaction
206
+ // help open. The caller will be responsible for releasing the lock and
207
+ // ending the transaction.
208
+ return { client };
209
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@prairielearn/tsconfig",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"],
7
+ }
8
+ }