@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 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
- "use strict";
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
- exports.pool = new postgres_1.PostgresPool();
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 exports.pool.initAsync(pgConfig, idleErrorHandler);
55
- await exports.pool.queryAsync('CREATE TABLE IF NOT EXISTS named_locks (id bigserial PRIMARY KEY, name text NOT NULL UNIQUE);', {});
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 exports.pool.closeAsync();
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 exports.pool.queryAsync('INSERT INTO named_locks (name) VALUES ($name) ON CONFLICT (name) DO NOTHING;', { name });
100
- const client = await exports.pool.beginTransactionAsync();
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 exports.pool.queryWithClientAsync(client, `SET LOCAL lock_timeout = ${client.escapeLiteral(options.timeout.toString())}`, {});
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 exports.pool.queryWithClientAsync(client, lock_sql, { name });
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 exports.pool.endTransactionAsync(client, err);
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 exports.pool.endTransactionAsync(client, null);
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 exports.pool.endTransactionAsync(lock.client, null);
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": "2.0.2",
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.11.30",
17
+ "@types/node": "^20.12.2",
17
18
  "typescript": "^5.4.3"
18
19
  },
19
20
  "dependencies": {
20
- "@prairielearn/postgres": "^1.9.3",
21
- "pg": "^8.11.3"
21
+ "@prairielearn/postgres": "^2.0.0",
22
+ "pg": "^8.11.4"
22
23
  }
23
24
  }