EdgeAIG's picture
download
raw
29.5 kB
import * as Cause from "../../Cause.js";
import * as Context from "../../Context.js";
import * as Data from "../../Data.js";
import * as Duration from "../../Duration.js";
import * as Effect from "../../Effect.js";
import * as Exit from "../../Exit.js";
import { flow } from "../../Function.js";
import * as Iterable from "../../Iterable.js";
import * as Latch from "../../Latch.js";
import * as Layer from "../../Layer.js";
import * as MutableRef from "../../MutableRef.js";
import * as Queue from "../../Queue.js";
import * as RcMap from "../../RcMap.js";
import * as Schedule from "../../Schedule.js";
import * as Schema from "../../Schema.js";
import * as Scope from "../../Scope.js";
import * as SqlClient from "../sql/SqlClient.js";
import * as Redis from "./Redis.js";
/**
* Runtime type identifier for `PersistedQueue` values.
*
* @category type IDs
* @since 4.0.0
*/
export const TypeId = "~effect/persistence/PersistedQueue";
/**
* Service for constructing named `PersistedQueue` instances from schemas.
*
* @category services
* @since 4.0.0
*/
export class PersistedQueueFactory extends /*#__PURE__*/Context.Service()("effect/persistence/PersistedQueue/PersistedQueueFactory") {}
/**
* Accesses `PersistedQueueFactory` to create a named persisted queue for a
* schema.
*
* @category accessors
* @since 4.0.0
*/
export const make = options => PersistedQueueFactory.use(factory => factory.make(options));
/**
* Creates a `PersistedQueueFactory` from the current `PersistedQueueStore`.
*
* **Details**
*
* Values are encoded and decoded with the supplied schema, automatically
* assigned an id when needed, and acknowledged or retried according to the
* `take` handler's exit.
*
* @category constructors
* @since 4.0.0
*/
export const makeFactory = /*#__PURE__*/Effect.gen(function* () {
const store = yield* PersistedQueueStore;
return PersistedQueueFactory.of({
make(options) {
const jsonSchema = Schema.toCodecJson(options.schema);
const encodeUnknown = Schema.encodeUnknownEffect(jsonSchema);
const decodeUnknown = Schema.decodeUnknownEffect(jsonSchema);
return Effect.succeed({
[TypeId]: TypeId,
offer: (value, opts) => Effect.flatMap(encodeUnknown(value), element => {
const id = opts?.id ?? crypto.randomUUID();
return Effect.as(store.offer({
name: options.name,
id,
element,
isCustomId: opts?.id !== undefined
}), id);
}),
take: (f, opts) => Effect.uninterruptibleMask(Effect.fnUntraced(function* (restore) {
const scope = yield* Scope.make();
const item = yield* store.take({
name: options.name,
maxAttempts: opts?.maxAttempts ?? 10
}).pipe(Scope.provide(scope), restore);
const decoded = yield* decodeUnknown(item.element);
const exit = yield* Effect.exit(restore(f(decoded, {
id: item.id,
attempts: item.attempts
})));
yield* Scope.close(scope, exit);
return yield* exit;
}))
});
}
});
});
/**
* Provides `PersistedQueueFactory` using the current `PersistedQueueStore`.
*
* @category layers
* @since 4.0.0
*/
export const layer = /*#__PURE__*/Layer.effect(PersistedQueueFactory, makeFactory);
/**
* Runtime type identifier for `PersistedQueueError`.
*
* @category type IDs
* @since 4.0.0
*/
export const ErrorTypeId = "~@effect/experimental/PersistedQueue/PersistedQueueError";
/**
* Error raised by persisted queue store operations.
*
* @category errors
* @since 4.0.0
*/
export class PersistedQueueError extends /*#__PURE__*/Schema.ErrorClass("effect/persistence/PersistedQueue/PersistedQueueError")({
_tag: /*#__PURE__*/Schema.tag("PersistedQueueError"),
message: Schema.String,
cause: /*#__PURE__*/Schema.optional(/*#__PURE__*/Schema.Defect())
}) {
/**
* Marks this value as a persisted queue error for runtime guards.
*
* @since 4.0.0
*/
[ErrorTypeId] = ErrorTypeId;
}
/**
* Defines the low-level backing store service used by `PersistedQueue`.
*
* **When to use**
*
* Use to provide the persistence backend that stores queued elements, scoped
* takes, retry attempts, and acknowledgements.
*
* **Details**
*
* The store persists offered elements and returns taken elements in a scope so
* the finalizer can complete or retry them based on the processing exit.
*
* @category store
* @since 4.0.0
*/
export class PersistedQueueStore extends /*#__PURE__*/Context.Service()("effect/persistence/PersistedQueue/PersistedQueueStore") {}
/**
* Provides an in-memory `PersistedQueueStore`.
*
* **Details**
*
* The store is process-local and volatile; failed takes are requeued until the
* configured maximum attempts is reached.
*
* @category store
* @since 4.0.0
*/
export const layerStoreMemory = /*#__PURE__*/Layer.sync(PersistedQueueStore, () => {
const ids = new Set();
const queues = new Map();
const getOrCreateQueue = name => {
let queue = queues.get(name);
if (!queue) {
queue = {
latch: Latch.makeUnsafe(false),
items: new Set()
};
queues.set(name, queue);
}
return queue;
};
return PersistedQueueStore.of({
offer: options => Effect.sync(() => {
if (ids.has(options.id)) return;
ids.add(options.id);
const queue = getOrCreateQueue(options.name);
queue.items.add({
id: options.id,
attempts: 0,
element: options.element
});
queue.latch.openUnsafe();
}),
take: Effect.fnUntraced(function* (options) {
const queue = getOrCreateQueue(options.name);
while (true) {
yield* queue.latch.await;
const item = Iterable.headUnsafe(queue.items);
queue.items.delete(item);
if (queue.items.size === 0) {
queue.latch.closeUnsafe();
}
yield* Effect.addFinalizer(exit => {
if (exit._tag === "Success") {
return Effect.void;
} else if (!Exit.hasInterrupts(exit)) {
item.attempts += 1;
}
if (item.attempts >= options.maxAttempts) {
return Effect.void;
}
queue.items.add(item);
queue.latch.openUnsafe();
return Effect.void;
});
return item;
}
})
});
});
/**
* Creates a Redis-backed `PersistedQueueStore`.
*
* **Details**
*
* The store uses Redis lists and hashes with worker locks, periodically
* refreshes locks while items are being processed, and moves exhausted items
* to a failed queue.
*
* @category store
* @since 4.0.0
*/
export const makeStoreRedis = /*#__PURE__*/Effect.fnUntraced(function* (options) {
const redis = yield* Redis.Redis;
const pollInterval = Duration.max(options?.pollInterval ? Duration.fromInputUnsafe(options.pollInterval) : Duration.seconds(1), Duration.millis(1));
const lockRefreshMillis = Math.max(options?.lockRefreshInterval ? Duration.toMillis(Duration.fromInputUnsafe(options.lockRefreshInterval)) : 30_000, 1);
const lockExpirationMillis = Math.max(options?.lockExpiration ? Duration.toMillis(Duration.fromInputUnsafe(options.lockExpiration)) : 90_000, 1);
const prefix = options?.prefix ?? "effectq:";
const keyQueue = name => `${prefix}${name}`;
const keyLock = id => `${prefix}${id}:lock`;
const keyPending = name => `${prefix}${name}:pending`;
const keyFailed = name => `${prefix}${name}:failed`;
const workerId = crypto.randomUUID();
const requeue = redis.eval(requeueRedis);
const complete = redis.eval(completeRedis);
const failed = redis.eval(failedRedis);
const resetQueue = redis.eval(resetQueueRedis);
const offer = redis.eval(offerRedis);
const take = redis.eval(takeRedis);
const expireAll = redis.eval(expireAllRedis);
const queues = yield* RcMap.make({
lookup: Effect.fnUntraced(function* (name) {
const queueKey = keyQueue(name);
const pendingKey = keyPending(name);
const queue = yield* Queue.make();
const takers = MutableRef.make(0);
const pollLatch = Latch.makeUnsafe();
const takenLatch = Latch.makeUnsafe();
yield* Effect.addFinalizer(() => Effect.orDie(Effect.flatMap(Queue.clear(queue), elements => Effect.forEach(elements, element => requeue(queueKey, pendingKey, keyLock(element.id), element.id, JSON.stringify(element)), {
concurrency: "unbounded",
discard: true
}))));
yield* resetQueue(queueKey, pendingKey, prefix).pipe(Effect.andThen(Effect.sleep(lockRefreshMillis)), Effect.forever, Effect.forkScoped);
const poll = size => take(queueKey, pendingKey, prefix, workerId, size, lockExpirationMillis);
yield* Effect.gen(function* () {
while (true) {
yield* pollLatch.await;
yield* Effect.yieldNow;
const results = takers.current === 0 ? null : yield* poll(takers.current);
if (results === null) {
yield* Effect.sleep(pollInterval);
continue;
}
takenLatch.closeUnsafe();
yield* Queue.offerAll(queue, results.map(json => JSON.parse(json)));
yield* takenLatch.await;
yield* Effect.yieldNow;
}
}).pipe(Effect.sandbox, Effect.retry(Schedule.spaced(500)), Effect.forkScoped, Effect.interruptible);
return {
queue,
takers,
pollLatch,
takenLatch
};
}),
idleTimeToLive: Duration.seconds(30)
});
const activeLockKeys = new Set();
yield* Effect.gen(function* () {
while (true) {
yield* Effect.sleep(lockRefreshMillis);
yield* Effect.ignore(expireAll(Array.from(activeLockKeys), lockExpirationMillis));
}
}).pipe(Effect.forkScoped, Effect.interruptible, Effect.annotateLogs({
module: "effect/persistence/PersistedQueue",
fiber: "refreshLocks"
}));
return PersistedQueueStore.of({
offer: ({
element,
id,
isCustomId,
name
}) => Effect.mapError(isCustomId ? offer(`${prefix}${name}`, `${prefix}${name}:ids`, id, JSON.stringify({
id,
element,
attempts: 0
})) : redis.send("LPUSH", `${prefix}${name}`, JSON.stringify({
id,
element,
attempts: 0
})), ({
cause
}) => new PersistedQueueError({
message: "Failed to offer element to persisted queue",
cause
})),
take: options => Effect.uninterruptibleMask(restore => RcMap.get(queues, options.name).pipe(Effect.flatMap(({
pollLatch,
queue,
takenLatch,
takers
}) => {
takers.current++;
if (takers.current === 1) {
pollLatch.openUnsafe();
}
return Effect.tap(restore(Queue.take(queue)), () => Effect.sync(() => {
takers.current--;
if (takers.current === 0) {
pollLatch.closeUnsafe();
takenLatch.openUnsafe();
} else if (Queue.sizeUnsafe(queue) === 0) {
takenLatch.openUnsafe();
}
}));
}), Effect.scoped, Effect.tap(element => {
const lock = keyLock(element.id);
activeLockKeys.add(lock);
return Effect.addFinalizer(Exit.match({
onFailure: cause => {
activeLockKeys.delete(lock);
const nextAttempts = element.attempts + 1;
if (nextAttempts >= options.maxAttempts) {
return Effect.orDie(failed(keyPending(options.name), lock, keyFailed(options.name), element.id, JSON.stringify({
...element,
lastFailure: Cause.pretty(cause),
attempts: nextAttempts
})));
}
return Effect.orDie(requeue(keyQueue(options.name), keyPending(options.name), lock, element.id, JSON.stringify(Cause.hasInterruptsOnly(cause) ? element : {
...element,
lastFailure: Cause.pretty(cause),
attempts: nextAttempts
})));
},
onSuccess: () => {
activeLockKeys.delete(lock);
return Effect.orDie(complete(keyPending(options.name), lock, element.id));
}
}));
})))
});
});
const offerRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_queue = KEYS[1]
local key_ids = KEYS[2]
local id = ARGV[1]
local payload = ARGV[2]
local result = redis.call("SADD", key_ids, id)
if result == 1 then
redis.call("RPUSH", key_queue, payload)
end
`,
numberOfKeys: 2
});
const resetQueueRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_queue = KEYS[1]
local key_pending = KEYS[2]
local prefix = ARGV[1]
local entries = redis.call("HGETALL", key_pending)
for id, payload in pairs(entries) do
local lock_key = prefix .. id .. ":lock"
local exists = redis.call("EXISTS", lock_key)
if exists == 0 then
redis.call("RPUSH", key_queue, payload)
redis.call("HDEL", key_pending, id)
end
end
`,
numberOfKeys: 2
});
const requeueRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_queue = KEYS[1]
local key_pending = KEYS[2]
local key_lock = KEYS[3]
local id = ARGV[1]
local payload = ARGV[2]
redis.call("DEL", key_lock)
redis.call("HDEL", key_pending, id)
redis.call("RPUSH", key_queue, payload)
`,
numberOfKeys: 3
});
const completeRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_pending = KEYS[1]
local key_lock = KEYS[2]
local id = ARGV[1]
redis.call("DEL", key_lock)
redis.call("HDEL", key_pending, id)
`,
numberOfKeys: 2
});
const failedRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_pending = KEYS[1]
local key_lock = KEYS[2]
local key_failed = KEYS[3]
local id = ARGV[1]
local payload = ARGV[2]
redis.call("DEL", key_lock)
redis.call("HDEL", key_pending, id)
redis.call("RPUSH", key_failed, payload)
`,
numberOfKeys: 2
});
const takeRedis = /*#__PURE__*/Redis.script((...args) => args, {
lua: `
local key_queue = KEYS[1]
local key_pending = KEYS[2]
local prefix = ARGV[1]
local worker_id = ARGV[2]
local batch_size = tonumber(ARGV[3])
local pttl = ARGV[4]
local payloads = redis.call("LPOP", key_queue, batch_size)
if not payloads then
return nil
end
for i, payload in ipairs(payloads) do
local id = cjson.decode(payload).id
local key_lock = prefix .. id .. ":lock"
redis.call("SET", key_lock, worker_id, "PX", pttl)
redis.call("HSET", key_pending, id, payload)
end
return payloads
`,
numberOfKeys: 2
}).withReturnType();
const expireAllRedis = /*#__PURE__*/Redis.script((keys, ttl) => [...keys, ttl], {
numberOfKeys: keys => keys.length,
lua: `
local ttl = ARGV[1]
for i, key in ipairs(KEYS) do
redis.call("PEXPIRE", key, ttl)
end
`
});
/**
* Provides a Redis-backed `PersistedQueueStore` using `makeStoreRedis`.
*
* @category store
* @since 4.0.0
*/
export const layerStoreRedis = /*#__PURE__*/flow(makeStoreRedis, /*#__PURE__*/Layer.effect(PersistedQueueStore));
/**
* Creates a SQL-backed `PersistedQueueStore`.
*
* **Details**
*
* The store creates the queue table and indexes, acquires rows with
* per-worker locks, refreshes active locks while scoped takes are running, and
* retries or completes rows according to the processing exit.
*
* @category store
* @since 4.0.0
*/
export const makeStoreSql = /*#__PURE__*/Effect.fnUntraced(function* (options) {
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
const tableName = options?.tableName ?? "effect_queue";
const tableNameSql = sql(tableName);
const pollInterval = Duration.max(options?.pollInterval ? Duration.fromInputUnsafe(options.pollInterval) : Duration.millis(1000), Duration.millis(1));
const lockRefreshInterval = Duration.max(options?.lockRefreshInterval ? Duration.fromInputUnsafe(options.lockRefreshInterval) : Duration.seconds(30), Duration.millis(1));
const lockExpiration = Duration.max(options?.lockExpiration ? Duration.fromInputUnsafe(options.lockExpiration) : Duration.minutes(2), Duration.millis(1));
const lockExpirationSql = sql.literal(Math.ceil(Duration.toSeconds(lockExpiration)).toString());
const workerId = crypto.randomUUID();
const sqlNow = sql.onDialectOrElse({
mssql: () => sql.literal("GETDATE()"),
mysql: () => sql.literal("NOW()"),
pg: () => sql.literal("NOW()"),
// sqlite
orElse: () => sql.literal("CURRENT_TIMESTAMP")
});
const expiresAt = sql.onDialectOrElse({
pg: () => sql`${sqlNow} - INTERVAL '${lockExpirationSql} seconds'`,
mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL ${lockExpirationSql} SECOND)`,
mssql: () => sql`DATEADD(SECOND, -${lockExpirationSql}, ${sqlNow})`,
orElse: () => sql`datetime(${sqlNow}, '-${lockExpirationSql} seconds')`
});
yield* sql.onDialectOrElse({
mysql: () => sql`CREATE TABLE IF NOT EXISTS ${tableNameSql} (
sequence BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
id VARCHAR(36) NOT NULL,
queue_name VARCHAR(100) NOT NULL,
element TEXT NOT NULL,
completed BOOLEAN NOT NULL,
attempts INT NOT NULL DEFAULT 0,
last_failure TEXT NULL,
acquired_at DATETIME NULL,
acquired_by VARCHAR(36) NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
)`,
pg: () => sql`CREATE TABLE IF NOT EXISTS ${tableNameSql} (
sequence SERIAL PRIMARY KEY,
id VARCHAR(36) NOT NULL,
queue_name VARCHAR(100) NOT NULL,
element TEXT NOT NULL,
completed BOOLEAN NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
last_failure TEXT NULL,
acquired_at TIMESTAMP NULL,
acquired_by UUID NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)`,
mssql: () => sql`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${tableNameSql} AND xtype='U')
CREATE TABLE ${tableNameSql} (
sequence INT IDENTITY(1,1) PRIMARY KEY,
id NVARCHAR(36) NOT NULL,
queue_name NVARCHAR(100) NOT NULL,
element NVARCHAR(MAX) NOT NULL,
completed BIT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
last_failure NVARCHAR(MAX) NULL,
acquired_at DATETIME2 NULL,
acquired_by UNIQUEIDENTIFIER NULL,
created_at DATETIME2 NOT NULL,
updated_at DATETIME2 NOT NULL
)`,
// sqlite
orElse: () => sql`CREATE TABLE IF NOT EXISTS ${tableNameSql} (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT NOT NULL,
queue_name TEXT NOT NULL,
element TEXT NOT NULL,
completed BOOLEAN NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
last_failure TEXT NULL,
acquired_at DATETIME NULL,
acquired_by TEXT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
)`
});
yield* sql.onDialectOrElse({
mssql: () => sql`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'idx_${tableName}_id')
CREATE UNIQUE INDEX idx_${tableNameSql}_id ON ${tableNameSql} (id)`,
mysql: () => sql`CREATE UNIQUE INDEX ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id)`.pipe(Effect.ignore),
orElse: () => sql`CREATE UNIQUE INDEX IF NOT EXISTS ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id)`
});
yield* sql.onDialectOrElse({
mssql: () => sql`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'idx_${tableName}_take')
CREATE INDEX idx_${tableNameSql}_take ON ${tableNameSql} (queue_name, completed, attempts, acquired_at)`,
mysql: () => sql`CREATE INDEX ${sql(`idx_${tableName}_take`)} ON ${tableNameSql} (queue_name, completed, attempts, acquired_at)`.pipe(Effect.ignore),
orElse: () => sql`CREATE INDEX IF NOT EXISTS ${sql(`idx_${tableName}_take`)} ON ${tableNameSql} (queue_name, completed, attempts, acquired_at)`
});
yield* sql.onDialectOrElse({
mssql: () => sql`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'idx_${tableName}_update')
CREATE INDEX ${sql(`idx_${tableName}_update`)} ON ${tableNameSql} (sequence, acquired_by)`,
mysql: () => sql`CREATE INDEX ${sql(`idx_${tableName}_update`)} ON ${tableNameSql} (sequence, acquired_by)`.pipe(Effect.ignore),
orElse: () => sql`CREATE INDEX IF NOT EXISTS ${sql(`idx_${tableName}_update`)} ON ${tableNameSql} (sequence, acquired_by)`
});
const offer = sql.onDialectOrElse({
pg: () => (id, name, element) => sql`
INSERT INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at)
VALUES (${id}, ${name}, ${element}, FALSE, 0, ${sqlNow}, ${sqlNow})
ON CONFLICT (id) DO NOTHING
`,
mysql: () => (id, name, element) => sql`
INSERT IGNORE INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at)
VALUES (${id}, ${name}, ${element}, FALSE, 0, ${sqlNow}, ${sqlNow})
`,
mssql: () => (id, name, element) => sql`
IF NOT EXISTS (SELECT 1 FROM ${tableNameSql} WHERE id = ${id})
BEGIN
INSERT INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at)
VALUES (${id}, ${name}, ${element}, 0, 0, ${sqlNow}, ${sqlNow})
END
`,
// sqlite
orElse: () => (id, name, element) => sql`
INSERT OR IGNORE INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at)
VALUES (${id}, ${name}, ${element}, FALSE, 0, ${sqlNow}, ${sqlNow})
`
});
const wrapString = sql.onDialectOrElse({
mssql: () => s => `N'${s}'`,
orElse: () => s => `'${s}'`
});
const stringLiteral = s => sql.literal(wrapString(s));
const sqlTrue = sql.onDialectOrElse({
sqlite: () => sql.literal("1"),
orElse: () => sql.literal("TRUE")
});
const workerIdSql = stringLiteral(workerId);
const elementIds = new Set();
const refreshLocks = Effect.suspend(() => {
if (elementIds.size === 0) return Effect.void;
return sql`
UPDATE ${tableNameSql}
SET acquired_at = ${sqlNow}
WHERE acquired_by = ${workerIdSql}
`;
});
const complete = (sequence, attempts) => {
elementIds.delete(sequence);
return sql`
UPDATE ${tableNameSql}
SET acquired_at = NULL, acquired_by = NULL, updated_at = ${sqlNow}, completed = ${sqlTrue}, attempts = ${attempts}
WHERE sequence = ${sequence}
AND acquired_by = ${workerIdSql}
`.pipe(Effect.retry({
times: 5,
schedule: Schedule.exponential(100, 1.5)
}), Effect.orDie);
};
const retry = (sequence, attempts, cause) => {
elementIds.delete(sequence);
return sql`
UPDATE ${tableNameSql}
SET acquired_at = NULL, acquired_by = NULL, updated_at = ${sqlNow}, attempts = ${attempts}, last_failure = ${Cause.pretty(cause)}
WHERE sequence = ${sequence}
AND acquired_by = ${workerIdSql}
`.pipe(Effect.retry({
times: 5,
schedule: Schedule.exponential(100, 1.5)
}), Effect.orDie);
};
const interrupt = ids => {
for (const id of ids) {
elementIds.delete(id);
}
return sql`
UPDATE ${tableNameSql}
SET acquired_at = NULL, acquired_by = NULL
WHERE sequence IN (${sql.literal(ids.join(","))})
AND acquired_by = ${workerIdSql}
`.pipe(Effect.retry({
times: 5,
schedule: Schedule.exponential(100, 1.5)
}), Effect.orDie);
};
yield* refreshLocks.pipe(Effect.tapCause(Effect.logWarning), Effect.retry(Schedule.spaced(500)), Effect.schedule(Schedule.fixed(lockRefreshInterval)), Effect.annotateLogs({
package: "@effect/sql",
module: "SqlPersistedQueue",
fiber: "refreshLocks"
}), Effect.forkScoped);
const mailboxes = yield* RcMap.make({
lookup: Effect.fnUntraced(function* ({
maxAttempts,
name
}) {
const queue = yield* Queue.make();
const takers = MutableRef.make(0);
const pollLatch = Latch.makeUnsafe();
const takenLatch = Latch.makeUnsafe();
yield* Effect.addFinalizer(() => Effect.flatMap(Queue.clear(queue), elements => {
if (elements.length === 0) return Effect.void;
return interrupt(Array.from(elements, e => e.sequence));
}));
const poll = sql.onDialectOrElse({
pg: () => size => sql`
WITH cte AS (
UPDATE ${tableNameSql}
SET acquired_at = ${sqlNow}, acquired_by = ${workerIdSql}
WHERE sequence IN (
SELECT sequence FROM ${tableNameSql}
WHERE queue_name = ${name}
AND completed = FALSE
AND attempts < ${maxAttempts}
AND (acquired_at IS NULL OR acquired_at < ${expiresAt})
ORDER BY updated_at ASC, sequence ASC
FOR UPDATE SKIP LOCKED
LIMIT ${sql.literal(size.toString())}
)
RETURNING sequence, id, queue_name, element, attempts, updated_at
)
SELECT sequence, id, queue_name, element, attempts FROM cte
ORDER BY updated_at ASC, sequence ASC
`,
mysql: () => size => sql`
SELECT sequence, id, queue_name, element, attempts FROM ${tableNameSql} q
WHERE queue_name = ${name}
AND completed = FALSE
AND attempts < ${maxAttempts}
AND (acquired_at IS NULL OR acquired_at < ${expiresAt})
ORDER BY updated_at ASC, sequence ASC
LIMIT ${sql.literal(size.toString())}
FOR UPDATE SKIP LOCKED
`.pipe(Effect.tap(rows => {
if (rows.length === 0) return Effect.void;
return sql`
UPDATE ${tableNameSql}
SET acquired_at = ${sqlNow}, acquired_by = ${workerIdSql}
WHERE sequence IN (${sql.literal(rows.map(r => r.sequence).join(","))})
`.unprepared;
}), sql.withTransaction),
mssql: () => size => sql`
WITH cte AS (
SELECT TOP ${sql.literal(size.toString())} sequence FROM ${tableNameSql}
WHERE queue_name = ${name}
AND completed = 0
AND attempts < ${maxAttempts}
AND (acquired_at IS NULL OR acquired_at < ${expiresAt})
ORDER BY updated_at ASC, sequence ASC
)
UPDATE q
SET acquired_at = ${sqlNow}, acquired_by = ${workerIdSql}
OUTPUT inserted.sequence, inserted.id, inserted.queue_name, inserted.element, inserted.attempts
FROM ${tableNameSql} AS q
INNER JOIN cte ON q.sequence = cte.sequence
`,
// sqlite
orElse: () => size => sql`
UPDATE ${tableNameSql}
SET acquired_at = ${sqlNow}, acquired_by = ${workerIdSql}
WHERE queue_name = ${name}
AND completed = FALSE
AND attempts < ${maxAttempts}
AND (acquired_at IS NULL OR acquired_at < ${expiresAt})
RETURNING sequence, id, queue_name, element, attempts
ORDER BY updated_at ASC, sequence ASC
LIMIT ${sql.literal(size.toString())}
`
});
yield* Effect.gen(function* () {
while (true) {
yield* pollLatch.await;
yield* Effect.yieldNow;
const results = takers.current === 0 ? [] : yield* poll(takers.current);
if (results.length === 0) {
yield* Effect.sleep(pollInterval);
continue;
}
takenLatch.closeUnsafe();
for (let i = 0; i < results.length; i++) {
const element = results[i];
element.element = JSON.parse(element.element);
}
yield* Queue.offerAll(queue, results);
yield* takenLatch.await;
yield* Effect.yieldNow;
}
}).pipe(Effect.sandbox, Effect.retry(Schedule.spaced(500)), Effect.forkScoped);
return {
queue,
takers,
pollLatch,
takenLatch
};
}),
idleTimeToLive: Duration.seconds(30)
});
return PersistedQueueStore.of({
offer: ({
element,
id,
name
}) => Effect.catchCause(Effect.suspend(() => offer(id, name, JSON.stringify(element))), cause => Effect.fail(new PersistedQueueError({
message: "Failed to offer element to persisted queue",
cause
}))),
take: ({
maxAttempts,
name
}) => Effect.uninterruptibleMask(restore => RcMap.get(mailboxes, new QueueKey({
name,
maxAttempts
})).pipe(Effect.flatMap(({
pollLatch,
queue,
takenLatch,
takers
}) => {
takers.current++;
if (takers.current === 1) {
pollLatch.openUnsafe();
}
return Effect.tap(restore(Queue.take(queue)), () => Effect.sync(() => {
takers.current--;
if (takers.current === 0) {
pollLatch.closeUnsafe();
takenLatch.openUnsafe();
} else if (Queue.sizeUnsafe(queue) === 0) {
takenLatch.openUnsafe();
}
}));
}), Effect.scoped, restore, Effect.tap(element => Effect.addFinalizer(Exit.match({
onFailure: cause => Cause.hasInterruptsOnly(cause) ? interrupt([element.sequence]) : retry(element.sequence, element.attempts + 1, cause),
onSuccess: () => complete(element.sequence, element.attempts + 1)
})))))
});
});
class QueueKey extends Data.Class {}
/**
* Provides a SQL-backed `PersistedQueueStore` using `makeStoreSql`.
*
* @category store
* @since 4.0.0
*/
export const layerStoreSql = /*#__PURE__*/flow(makeStoreSql, /*#__PURE__*/Layer.effect(PersistedQueueStore));
//# sourceMappingURL=PersistedQueue.js.map

Xet Storage Details

Size:
29.5 kB
·
Xet hash:
8bab958aa79fab2e7269c10a0ce60a99c751d1d2b2ecb3a44a1c2e8b14029467

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.