From a71b9505ea76596659c98eb6180a09a895399741 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 30 Nov 2025 00:19:28 +0000 Subject: fix: Add restate queued idempotency (#2169) * fix: Add restate queued idempotency * return on failed to acquire --- packages/plugins/queue-restate/src/semaphore.ts | 36 ++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) (limited to 'packages/plugins/queue-restate/src/semaphore.ts') diff --git a/packages/plugins/queue-restate/src/semaphore.ts b/packages/plugins/queue-restate/src/semaphore.ts index 9acecf28..694c77b3 100644 --- a/packages/plugins/queue-restate/src/semaphore.ts +++ b/packages/plugins/queue-restate/src/semaphore.ts @@ -5,6 +5,7 @@ import { Context, object, ObjectContext } from "@restatedev/restate-sdk"; interface QueueItem { awakeable: string; + idempotencyKey?: string; priority: number; } @@ -35,9 +36,18 @@ export const semaphore = object({ priority: number; capacity: number; groupId?: string; + idempotencyKey?: string; }, - ): Promise => { + ): Promise => { const state = await getState(ctx); + + if ( + req.idempotencyKey && + idempotencyKeyAlreadyExists(state.groups, req.idempotencyKey) + ) { + return false; + } + req.groupId = req.groupId ?? "__ungrouped__"; if (state.groups[req.groupId] === undefined) { @@ -51,11 +61,13 @@ export const semaphore = object({ state.groups[req.groupId].items.push({ awakeable: req.awakeableId, priority: req.priority, + idempotencyKey: req.idempotencyKey, }); tick(ctx, state, req.capacity); setState(ctx, state); + return true; }, release: async ( @@ -149,6 +161,18 @@ async function getState( }; } +function idempotencyKeyAlreadyExists( + items: Record, + key: string, +) { + for (const group of Object.values(items)) { + if (group.items.some((item) => item.idempotencyKey === key)) { + return true; + } + } + return false; +} + function setState(ctx: ObjectContext, state: QueueState) { ctx.set("itemsv2", state.groups); ctx.set("inFlight", state.inFlight); @@ -162,17 +186,22 @@ export class RestateSemaphore { private readonly capacity: number, ) {} - async acquire(priority: number, groupId?: string) { + async acquire(priority: number, groupId?: string, idempotencyKey?: string) { const awk = this.ctx.awakeable(); - await this.ctx + const res = await this.ctx .objectClient({ name: "Semaphore" }, this.id) .acquire({ awakeableId: awk.id, priority, capacity: this.capacity, groupId, + idempotencyKey, }); + if (!res) { + return false; + } + try { await awk.promise; } catch (e) { @@ -181,6 +210,7 @@ export class RestateSemaphore { throw e; } } + return true; } async release() { await this.ctx -- cgit v1.2.3-70-g09d2