@prairielearn/named-locks 3.0.6 → 3.0.8

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,20 @@
1
1
  # @prairielearn/named-locks
2
2
 
3
+ ## 3.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b79275: Upgrade all JavaScript dependencies
8
+ - Updated dependencies [4b79275]
9
+ - @prairielearn/postgres@2.1.4
10
+
11
+ ## 3.0.7
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [852c2e2]
16
+ - @prairielearn/postgres@2.1.3
17
+
3
18
  ## 3.0.6
4
19
 
5
20
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { PoolConfig } from 'pg';
2
- import { PostgresPool, PoolClient } from '@prairielearn/postgres';
1
+ import { type PoolConfig } from 'pg';
2
+ import { PostgresPool, type PoolClient } from '@prairielearn/postgres';
3
3
  interface NamedLocksConfig {
4
4
  /**
5
5
  * How often to renew the lock in milliseconds. Defaults to 60 seconds.
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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","sourcesContent":["import { PoolConfig } from 'pg';\n\nimport { PostgresPool, PoolClient } from '@prairielearn/postgres';\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, {});\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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { type PoolConfig } from 'pg';\n\nimport { PostgresPool, type PoolClient } from '@prairielearn/postgres';\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, {});\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,6 @@
1
1
  {
2
2
  "name": "@prairielearn/named-locks",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "repository": {
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "@prairielearn/tsconfig": "^0.0.0",
17
- "@types/node": "^20.14.13",
18
- "typescript": "^5.5.4"
17
+ "@types/node": "^20.17.5",
18
+ "typescript": "^5.6.3"
19
19
  },
20
20
  "dependencies": {
21
- "@prairielearn/postgres": "^2.1.2",
22
- "pg": "^8.12.0"
21
+ "@prairielearn/postgres": "^2.1.4",
22
+ "pg": "^8.13.1"
23
23
  }
24
24
  }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { PoolConfig } from 'pg';
1
+ import { type PoolConfig } from 'pg';
2
2
 
3
- import { PostgresPool, PoolClient } from '@prairielearn/postgres';
3
+ import { PostgresPool, type PoolClient } from '@prairielearn/postgres';
4
4
 
5
5
  interface NamedLocksConfig {
6
6
  /**