| /** | |
| * Stores cluster runner registration and shard ownership in SQL. | |
| * | |
| * The SQL-backed `RunnerStorage` records runners, health flags, machine ids, | |
| * and shard locks so multiple processes can coordinate which runner owns each | |
| * shard. This module creates the required runner and lock tables, supports an | |
| * optional table prefix, uses advisory locks for PostgreSQL and MySQL when | |
| * enabled, and provides constructors and layers for the storage service. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.js"; | |
| import * as Duration from "../../Duration.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Scope from "../../Scope.js"; | |
| import * as SqlClient from "../sql/SqlClient.js"; | |
| import { PersistenceError } from "./ClusterError.js"; | |
| import { ResourceRef } from "./internal/resourceRef.js"; | |
| import * as RunnerStorage from "./RunnerStorage.js"; | |
| import * as ShardId from "./ShardId.js"; | |
| import * as ShardingConfig from "./ShardingConfig.js"; | |
| const withTracerDisabled = /*#__PURE__*/Effect.withTracerEnabled(false); | |
| /** | |
| * Creates a SQL-backed `RunnerStorage` implementation for registered runners and | |
| * shard locks, using the configured table prefix and advisory locks where | |
| * supported and enabled. | |
| * | |
| * **When to use** | |
| * | |
| * Use to create a SQL-backed `RunnerStorage` value directly when building | |
| * custom service or layer composition around the storage implementation. | |
| * | |
| * **Details** | |
| * | |
| * When `prefix` is omitted, `make` uses the `cluster` prefix, creating | |
| * `cluster_runners` and `cluster_locks`. PostgreSQL and MySQL use advisory | |
| * locks unless `ShardingConfig.shardLockDisableAdvisory` is enabled; other | |
| * dialects use rows in the locks table. | |
| * | |
| * **Gotchas** | |
| * | |
| * Changing `prefix` changes both generated table names, so runners using | |
| * different prefixes do not share registrations or shard locks. | |
| * | |
| * @see {@link layer} for the default SQL-backed storage layer | |
| * @see {@link layerWith} for a SQL-backed storage layer with a custom table prefix | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const make = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const config = yield* ShardingConfig.ShardingConfig; | |
| const shardGroups = ShardingConfig.shardGroupConfig(config); | |
| const availableShardGroups = Array.from(shardGroups.available); | |
| const disableAdvisoryLocks = config.shardLockDisableAdvisory; | |
| const sql = (yield* SqlClient.SqlClient).withoutTransforms(); | |
| const prefix = options?.prefix ?? "cluster"; | |
| const table = name => `${prefix}_${name}`; | |
| const acquireLockConn = sql.onDialectOrElse({ | |
| pg: () => Effect.fnUntraced(function* (scope) { | |
| const conn = yield* Effect.orDie(sql.reserve).pipe(Scope.provide(scope)); | |
| const pid = (yield* conn.executeValues("SELECT pg_backend_pid()", []))[0][0]; | |
| yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", []))); | |
| return [conn, pid]; | |
| }, Effect.orDie), | |
| mysql: () => Effect.fnUntraced(function* (scope) { | |
| const conn = yield* Effect.orDie(sql.reserve).pipe(Scope.provide(scope)); | |
| // we need to get the connection id using IS_USED_LOCK to properly | |
| // support vitess | |
| let pid = undefined; | |
| while (pid === undefined) { | |
| const address = `cluster:pid:${Math.random() * Number.MAX_SAFE_INTEGER | 0}`; | |
| const taken = (yield* conn.executeValues(`SELECT GET_LOCK('${address}', 10), IS_USED_LOCK('${address}')`, []))[0]; | |
| if (taken[0] === null) continue; | |
| pid = taken[1]; | |
| } | |
| yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", []))); | |
| return [conn, pid]; | |
| }, Effect.orDie), | |
| orElse: () => undefined | |
| }); | |
| const lockConn = acquireLockConn && (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn)); | |
| const runnersTable = table("runners"); | |
| const runnersTableSql = sql(runnersTable); | |
| yield* sql.onDialectOrElse({ | |
| mssql: () => sql` | |
| IF OBJECT_ID(N'${runnersTableSql}', N'U') IS NULL | |
| CREATE TABLE ${runnersTableSql} ( | |
| machine_id INT IDENTITY PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| runner TEXT NOT NULL, | |
| healthy BIT NOT NULL DEFAULT 1, | |
| last_heartbeat DATETIME NOT NULL DEFAULT GETDATE(), | |
| UNIQUE(address) | |
| ) | |
| `, | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( | |
| machine_id INT AUTO_INCREMENT PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| runner TEXT NOT NULL, | |
| healthy BOOLEAN NOT NULL DEFAULT TRUE, | |
| last_heartbeat DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| UNIQUE(address) | |
| ) | |
| `, | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( | |
| machine_id SERIAL PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| runner TEXT NOT NULL, | |
| healthy BOOLEAN NOT NULL DEFAULT TRUE, | |
| last_heartbeat TIMESTAMP NOT NULL DEFAULT NOW(), | |
| UNIQUE(address) | |
| ) | |
| `, | |
| orElse: () => | |
| // sqlite | |
| sql` | |
| CREATE TABLE IF NOT EXISTS ${runnersTableSql} ( | |
| machine_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| address TEXT NOT NULL, | |
| runner TEXT NOT NULL, | |
| healthy INTEGER NOT NULL DEFAULT 1, | |
| last_heartbeat DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP), | |
| UNIQUE(address) | |
| ) | |
| ` | |
| }); | |
| const locksTable = table("locks"); | |
| const locksTableSql = sql(locksTable); | |
| yield* sql.onDialectOrElse({ | |
| mssql: () => sql` | |
| IF OBJECT_ID(N'${locksTableSql}', N'U') IS NULL | |
| CREATE TABLE ${locksTableSql} ( | |
| shard_id VARCHAR(50) PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| acquired_at DATETIME NOT NULL | |
| ) | |
| `, | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${locksTableSql} ( | |
| shard_id VARCHAR(50) PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| acquired_at DATETIME NOT NULL | |
| ) | |
| `, | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${locksTableSql} ( | |
| shard_id VARCHAR(50) PRIMARY KEY, | |
| address VARCHAR(255) NOT NULL, | |
| acquired_at TIMESTAMP NOT NULL | |
| ) | |
| `, | |
| orElse: () => | |
| // sqlite | |
| sql` | |
| CREATE TABLE IF NOT EXISTS ${locksTableSql} ( | |
| shard_id TEXT PRIMARY KEY, | |
| address TEXT NOT NULL, | |
| acquired_at DATETIME NOT NULL | |
| ) | |
| ` | |
| }); | |
| const sqlNowString = sql.onDialectOrElse({ | |
| pg: () => "NOW()", | |
| mysql: () => "NOW()", | |
| mssql: () => "GETDATE()", | |
| orElse: () => "CURRENT_TIMESTAMP" | |
| }); | |
| const sqlNow = sql.literal(sqlNowString); | |
| const expiresSeconds = sql.literal(Math.ceil(Duration.toSeconds(Duration.fromInputUnsafe(config.shardLockExpiration))).toString()); | |
| const lockExpiresAt = sql.onDialectOrElse({ | |
| pg: () => sql`${sqlNow} - INTERVAL '${expiresSeconds} seconds'`, | |
| mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL ${expiresSeconds} SECOND)`, | |
| mssql: () => sql`DATEADD(SECOND, -${expiresSeconds}, ${sqlNow})`, | |
| orElse: () => sql`datetime(${sqlNow}, '-${expiresSeconds} seconds')` | |
| }); | |
| const encodeBoolean = sql.onDialectOrElse({ | |
| mssql: () => b => b ? 1 : 0, | |
| sqlite: () => b => b ? 1 : 0, | |
| orElse: () => b => b | |
| }); | |
| // Upsert runner and return machine_id | |
| const insertRunner = sql.onDialectOrElse({ | |
| mssql: () => (address, runner, healthy) => sql` | |
| MERGE ${runnersTableSql} AS target | |
| USING (SELECT ${address} AS address, ${runner} AS runner, ${sqlNow} AS last_heartbeat, ${encodeBoolean(healthy)} AS healthy) AS source | |
| ON target.address = source.address | |
| WHEN MATCHED THEN | |
| UPDATE SET runner = source.runner, last_heartbeat = source.last_heartbeat, healthy = source.healthy | |
| WHEN NOT MATCHED THEN | |
| INSERT (address, runner, last_heartbeat, healthy) | |
| VALUES (source.address, source.runner, source.last_heartbeat, source.healthy) | |
| OUTPUT INSERTED.machine_id; | |
| `.values, | |
| mysql: () => (address, runner, healthy) => sql` | |
| INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) | |
| VALUES (${address}, ${runner}, ${sqlNow}, ${healthy}) | |
| ON DUPLICATE KEY UPDATE | |
| runner = VALUES(runner), | |
| last_heartbeat = VALUES(last_heartbeat), | |
| healthy = VALUES(healthy); | |
| SELECT machine_id FROM ${runnersTableSql} WHERE address = ${address}; | |
| `.unprepared.pipe(Effect.map(results => [[results[1][0].machine_id]])), | |
| pg: () => (address, runner, healthy) => sql` | |
| INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) | |
| VALUES (${address}, ${runner}, ${sqlNow}, ${healthy}) | |
| ON CONFLICT (address) DO UPDATE | |
| SET runner = EXCLUDED.runner, | |
| last_heartbeat = EXCLUDED.last_heartbeat, | |
| healthy = EXCLUDED.healthy | |
| RETURNING machine_id | |
| `.values, | |
| orElse: () => (address, runner, healthy) => | |
| // sqlite | |
| sql` | |
| INSERT INTO ${runnersTableSql} (address, runner, last_heartbeat, healthy) | |
| VALUES (${address}, ${runner}, ${sqlNow}, ${encodeBoolean(healthy)}) | |
| ON CONFLICT(address) DO UPDATE SET | |
| runner = excluded.runner, | |
| last_heartbeat = excluded.last_heartbeat, | |
| healthy = excluded.healthy | |
| RETURNING machine_id; | |
| `.values | |
| }); | |
| const execWithLockConn = effect => { | |
| if (!lockConn) return effect; | |
| const [query, params] = effect.compile(); | |
| return lockConn.await.pipe(Effect.flatMap(([conn]) => conn.executeRaw(query, params)), Effect.onError(() => lockConn.rebuildUnsafe())); | |
| }; | |
| const execWithLockConnUnprepared = effect => { | |
| if (!lockConn) return effect.values; | |
| const [query, params] = effect.compile(); | |
| return lockConn.await.pipe(Effect.flatMap(([conn]) => conn.executeUnprepared(query, params, undefined)), Effect.onError(() => lockConn.rebuildUnsafe())); | |
| }; | |
| const execWithLockConnValues = effect => { | |
| if (!lockConn) return effect.values; | |
| const [query, params] = effect.compile(); | |
| return lockConn.await.pipe(Effect.flatMap(([conn]) => conn.executeValues(query, params)), Effect.onError(() => lockConn.rebuildUnsafe())); | |
| }; | |
| const acquireLock = sql.onDialectOrElse({ | |
| pg: () => { | |
| if (disableAdvisoryLocks) { | |
| return (address, shardIds) => { | |
| const values = shardIds.map(shardId => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`); | |
| return sql` | |
| INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} | |
| ON CONFLICT (shard_id) DO UPDATE | |
| SET address = ${address}, acquired_at = ${sqlNow} | |
| WHERE ${locksTableSql}.address = ${address} | |
| OR ${locksTableSql}.acquired_at < ${lockExpiresAt} | |
| `.pipe(Effect.andThen(acquiredLocks(address, shardIds))); | |
| }; | |
| } | |
| return Effect.fnUntraced(function* (_address, shardIds) { | |
| const [conn, pid] = yield* lockConn.await; | |
| const acquiredShardIds = []; | |
| const toAcquire = new Map(shardIds.map(shardId => [lockNumbers.get(shardId), shardId])); | |
| const takenLocks = yield* conn.executeValues(`SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = ${pid} ORDER BY objid`, []); | |
| for (let i = 0; i < takenLocks.length; i++) { | |
| const lockNum = takenLocks[i][0]; | |
| acquiredShardIds.push(lockNumbersReverse.get(lockNum)); | |
| toAcquire.delete(lockNum); | |
| } | |
| if (toAcquire.size === 0) { | |
| return acquiredShardIds; | |
| } | |
| const rows = yield* conn.executeUnprepared(`SELECT ${pgLocks(toAcquire)}`, [], undefined); | |
| const results = rows[0]; | |
| for (const shardId in results) { | |
| if (results[shardId]) { | |
| acquiredShardIds.push(shardId); | |
| } | |
| } | |
| return acquiredShardIds; | |
| }, Effect.onError(() => lockConn.rebuildUnsafe())); | |
| }, | |
| mysql: () => { | |
| if (disableAdvisoryLocks) { | |
| return (address, shardIds) => { | |
| const values = shardIds.map(shardId => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`); | |
| return sql` | |
| INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} | |
| ON DUPLICATE KEY UPDATE | |
| address = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(address), address), | |
| acquired_at = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(acquired_at), acquired_at) | |
| `.unprepared.pipe(Effect.andThen(acquiredLocks(address, shardIds))); | |
| }; | |
| } | |
| return Effect.fnUntraced(function* (_address, shardIds) { | |
| const [conn, pid] = yield* lockConn.await; | |
| const takenLocks = (yield* conn.executeValues(`SELECT ${allMySqlTakenLocks}`, []))[0]; | |
| const acquiredShardIds = []; | |
| const toAcquire = []; | |
| for (let i = 0; i < shardIds.length; i++) { | |
| const shardId = shardIds[i]; | |
| const lockTakenBy = takenLocks[shardIdsIndex.get(shardId)]; | |
| if (lockTakenBy === pid) { | |
| acquiredShardIds.push(shardId); | |
| } else if (shardIds.includes(shardId)) { | |
| toAcquire.push(shardId); | |
| } | |
| } | |
| if (toAcquire.length === 0) { | |
| return acquiredShardIds; | |
| } | |
| const results = (yield* conn.executeValues(`SELECT ${mysqlLocks(toAcquire)}`, []))[0]; | |
| for (let i = 0; i < results.length; i++) { | |
| if (results[i] === 1) { | |
| acquiredShardIds.push(toAcquire[i]); | |
| } | |
| } | |
| return acquiredShardIds; | |
| }, Effect.onError(() => lockConn.rebuildUnsafe())); | |
| }, | |
| mssql: () => (address, shardIds) => { | |
| const values = shardIds.map(shardId => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`); | |
| return sql` | |
| MERGE ${locksTableSql} WITH (HOLDLOCK) AS target | |
| USING (SELECT * FROM (VALUES ${sql.csv(values)})) AS source (shard_id, address, acquired_at) | |
| ON target.shard_id = source.shard_id | |
| WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) > ${expiresSeconds}) THEN | |
| UPDATE SET address = source.address, acquired_at = source.acquired_at | |
| WHEN NOT MATCHED THEN | |
| INSERT (shard_id, address, acquired_at) | |
| VALUES (source.shard_id, source.address, source.acquired_at); | |
| `.pipe(Effect.andThen(acquiredLocks(address, shardIds)), sql.withTransaction); | |
| }, | |
| orElse: () => (address, shardIds) => { | |
| const values = shardIds.map(shardId => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`); | |
| return sql` | |
| WITH source(shard_id, address, acquired_at) AS (VALUES ${sql.csv(values)}) | |
| INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) | |
| SELECT source.shard_id, source.address, source.acquired_at | |
| FROM source | |
| WHERE NOT EXISTS ( | |
| SELECT 1 FROM ${locksTableSql} | |
| WHERE shard_id = source.shard_id | |
| AND address != ${address} | |
| AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <= ${expiresSeconds} | |
| ) | |
| ON CONFLICT(shard_id) DO UPDATE | |
| SET address = ${address}, acquired_at = ${sqlNow} | |
| `.pipe(Effect.andThen(acquiredLocks(address, shardIds)), sql.withTransaction); | |
| } | |
| }); | |
| const lockNumbers = new Map(); | |
| const lockNumbersReverse = new Map(); | |
| for (let i = 0; i < availableShardGroups.length; i++) { | |
| const group = availableShardGroups[i]; | |
| const base = (i + 1) * 1000000; | |
| for (let shard = 1; shard <= config.shardsPerGroup; shard++) { | |
| const shardId = ShardId.make(group, shard).toString(); | |
| const lockNum = base + shard; | |
| lockNumbers.set(shardId, lockNum); | |
| lockNumbersReverse.set(lockNum, shardId); | |
| } | |
| } | |
| const shardIdsIndex = new Map(); | |
| const lockNames = new Map(); | |
| const lockNamesReverse = new Map(); | |
| { | |
| let index = 0; | |
| for (let i = 0; i < availableShardGroups.length; i++) { | |
| const group = availableShardGroups[i]; | |
| for (let shard = 1; shard <= config.shardsPerGroup; shard++) { | |
| const shardId = ShardId.make(group, shard).toString(); | |
| const lockName = `${prefix}.${shardId}`; | |
| shardIdsIndex.set(shardId, index++); | |
| lockNames.set(shardId, lockName); | |
| lockNamesReverse.set(lockName, shardId); | |
| } | |
| } | |
| } | |
| const pgLocks = shardIdsMap => Array.from(shardIdsMap.entries(), ([lockNum, shardId]) => `pg_try_advisory_lock(${lockNum}) AS "${shardId}"`).join(", "); | |
| const mysqlLocks = shardIds => shardIds.map(shardId => `GET_LOCK('${lockNames.get(shardId)}', 0) AS "${shardId}"`).join(", "); | |
| const allMySqlTakenLocks = Array.from(lockNames.entries(), ([shardId, lockName]) => `IS_USED_LOCK('${lockName}') AS "${shardId}"`).join(", "); | |
| const acquiredLocks = (address, shardIds) => sql` | |
| SELECT shard_id FROM ${sql(locksTable)} | |
| WHERE address = ${address} | |
| AND acquired_at >= ${lockExpiresAt} | |
| AND shard_id IN ${stringLiteralArr(shardIds)} | |
| `.values.pipe(Effect.map(rows => rows.map(row => row[0]))); | |
| const wrapString = sql.onDialectOrElse({ | |
| mssql: () => s => `N'${s}'`, | |
| orElse: () => s => `'${s}'` | |
| }); | |
| const stringLiteral = s => sql.literal(wrapString(s)); | |
| const stringLiteralArr = arr => sql.literal(`(${arr.map(wrapString).join(",")})`); | |
| const refreshShards = sql.onDialectOrElse({ | |
| pg: () => { | |
| if (!disableAdvisoryLocks) return acquireLock; | |
| return (address, shardIds) => sql` | |
| UPDATE ${locksTableSql} | |
| SET acquired_at = ${sqlNow} | |
| WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} | |
| RETURNING shard_id | |
| `.pipe(execWithLockConnValues, Effect.map(rows => rows.map(row => row[0]))); | |
| }, | |
| mysql: () => { | |
| if (!disableAdvisoryLocks) return acquireLock; | |
| return (address, shardIds) => { | |
| const shardIdsStr = stringLiteralArr(shardIds); | |
| return sql` | |
| UPDATE ${locksTableSql} | |
| SET acquired_at = ${sqlNow} | |
| WHERE address = ${address} AND shard_id IN ${shardIdsStr}; | |
| SELECT shard_id FROM ${locksTableSql} WHERE address = ${address} AND shard_id IN ${shardIdsStr} | |
| `.pipe(execWithLockConnUnprepared, Effect.map(rows => rows[1].map(row => row.shard_id))); | |
| }; | |
| }, | |
| mssql: () => (address, shardIds) => sql` | |
| UPDATE ${locksTableSql} | |
| SET acquired_at = ${sqlNow} | |
| OUTPUT inserted.shard_id | |
| WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} | |
| `.pipe(execWithLockConnValues, Effect.map(rows => rows.map(row => row[0]))), | |
| orElse: () => (address, shardIds) => sql` | |
| UPDATE ${locksTableSql} | |
| SET acquired_at = ${sqlNow} | |
| WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} | |
| RETURNING shard_id | |
| `.pipe(execWithLockConnValues, Effect.map(rows => rows.map(row => row[0]))) | |
| }); | |
| return RunnerStorage.makeEncoded({ | |
| getRunners: sql`SELECT runner, healthy FROM ${runnersTableSql} WHERE last_heartbeat > ${lockExpiresAt}`.values.pipe(PersistenceError.refail, Effect.map(Arr.map(([runner, healthy]) => [String(runner), Boolean(healthy)])), withTracerDisabled), | |
| register: (address, runner, healthy) => insertRunner(address, runner, healthy).pipe(Effect.map(rows => Number(rows[0][0])), PersistenceError.refail, withTracerDisabled), | |
| unregister: address => sql`DELETE FROM ${runnersTableSql} WHERE address = ${address} OR last_heartbeat < ${lockExpiresAt}`.pipe(Effect.asVoid, PersistenceError.refail, withTracerDisabled), | |
| setRunnerHealth: (address, healthy) => sql`UPDATE ${runnersTableSql} SET healthy = ${encodeBoolean(healthy)} WHERE address = ${address}`.pipe(Effect.asVoid, PersistenceError.refail, withTracerDisabled), | |
| acquire: (address, shardIds) => acquireLock(address, shardIds).pipe(PersistenceError.refail, withTracerDisabled), | |
| refresh: (address, shardIds) => sql`UPDATE ${runnersTableSql} SET last_heartbeat = ${sqlNow} WHERE address = ${address}`.pipe(execWithLockConn, shardIds.length > 0 ? Effect.andThen(refreshShards(address, shardIds)) : Effect.as([]), PersistenceError.refail, withTracerDisabled), | |
| release: sql.onDialectOrElse({ | |
| pg: () => { | |
| if (disableAdvisoryLocks) { | |
| return (address, shardId) => sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(PersistenceError.refail, withTracerDisabled); | |
| } | |
| return Effect.fnUntraced(function* (_address, shardId) { | |
| const lockNum = lockNumbers.get(shardId); | |
| for (let i = 0; i < 5; i++) { | |
| const [conn] = yield* lockConn.await; | |
| yield* conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, []); | |
| const takenLocks = yield* conn.executeValues(`SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`, []); | |
| if (takenLocks.length === 0) return; | |
| } | |
| const [conn] = yield* lockConn.await; | |
| yield* conn.executeRaw(`SELECT pg_advisory_unlock_all()`, []); | |
| }, Effect.onError(() => lockConn.rebuildUnsafe()), Effect.asVoid, PersistenceError.refail, withTracerDisabled); | |
| }, | |
| mysql: () => { | |
| if (disableAdvisoryLocks) { | |
| return (address, shardId) => sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(PersistenceError.refail, withTracerDisabled); | |
| } | |
| return Effect.fnUntraced(function* (_address, shardId) { | |
| const lockName = lockNames.get(shardId); | |
| while (true) { | |
| const [conn, pid] = yield* lockConn.await; | |
| yield* conn.executeRaw(`SELECT RELEASE_LOCK('${lockName}')`, []); | |
| const takenLocks = yield* conn.executeValues(`SELECT IS_USED_LOCK('${lockName}')`, []); | |
| if (takenLocks.length === 0 || takenLocks[0][0] !== pid) return; | |
| } | |
| }, Effect.onError(() => lockConn.rebuildUnsafe()), Effect.asVoid, PersistenceError.refail, withTracerDisabled); | |
| }, | |
| orElse: () => (address, shardId) => sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(PersistenceError.refail, withTracerDisabled) | |
| }), | |
| releaseAll: sql.onDialectOrElse({ | |
| pg: () => address => { | |
| if (disableAdvisoryLocks) { | |
| return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(PersistenceError.refail, withTracerDisabled); | |
| } | |
| return sql`SELECT pg_advisory_unlock_all()`.pipe(execWithLockConn, Effect.asVoid, PersistenceError.refail, withTracerDisabled); | |
| }, | |
| mysql: () => address => { | |
| if (disableAdvisoryLocks) { | |
| return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(PersistenceError.refail, withTracerDisabled); | |
| } | |
| return sql`SELECT RELEASE_ALL_LOCKS()`.pipe(execWithLockConn, Effect.asVoid, PersistenceError.refail, withTracerDisabled); | |
| }, | |
| orElse: () => address => sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(PersistenceError.refail, withTracerDisabled) | |
| }) | |
| }); | |
| }, withTracerDisabled); | |
| /** | |
| * Layer that provides SQL-backed `RunnerStorage` using the default table prefix. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer = /*#__PURE__*/Layer.effect(RunnerStorage.RunnerStorage)(/*#__PURE__*/make({})); | |
| /** | |
| * Layer that provides SQL-backed `RunnerStorage` using a custom table prefix. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerWith = options => Layer.effect(RunnerStorage.RunnerStorage)(make(options)); | |
| //# sourceMappingURL=SqlRunnerStorage.js.map |
Xet Storage Details
- Size:
- 23.9 kB
- Xet hash:
- 9e8ea5adc601a842d7d535242253e7b4f3b78572fe15922b57007302f258e0a8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.