Skip to content

Commit fec6e11

Browse files
authored
Add Options to SQL Enqueue (#1260)
Ports dbos-inc/dbos-transact-py#684 Fixes #1258
1 parent 4ae2e57 commit fec6e11

2 files changed

Lines changed: 232 additions & 3 deletions

File tree

src/sysdb_migrations/internal/migrations.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,9 @@ export function allMigrations(
421421
`CREATE TABLE "${schemaName}"."queues" (
422422
"queue_id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
423423
"name" TEXT NOT NULL UNIQUE,
424-
"concurrency" INTEGER,
425-
"worker_concurrency" INTEGER,
426-
"rate_limit_max" INTEGER,
424+
"concurrency" INT4,
425+
"worker_concurrency" INT4,
426+
"rate_limit_max" INT4,
427427
"rate_limit_period_sec" DOUBLE PRECISION,
428428
"priority_enabled" BOOLEAN NOT NULL DEFAULT FALSE,
429429
"partition_queue" BOOLEAN NOT NULL DEFAULT FALSE,
@@ -527,5 +527,108 @@ export function allMigrations(
527527
`CREATE INDEX ${c} IF NOT EXISTS "idx_workflow_status_started_at" ON "${schemaName}"."workflow_status" ("started_at_epoch_ms") WHERE "started_at_epoch_ms" IS NOT NULL`,
528528
],
529529
},
530+
{
531+
pg: [
532+
`DROP FUNCTION IF EXISTS "${schemaName}".enqueue_workflow(
533+
TEXT, TEXT, JSON[], JSON, TEXT, TEXT, TEXT, TEXT, BIGINT, BIGINT, TEXT, INTEGER, TEXT
534+
)`,
535+
`CREATE OR REPLACE FUNCTION "${schemaName}".enqueue_workflow(
536+
workflow_name TEXT,
537+
queue_name TEXT,
538+
positional_args JSON[] DEFAULT ARRAY[]::JSON[],
539+
named_args JSON DEFAULT '{}'::JSON,
540+
class_name TEXT DEFAULT NULL,
541+
config_name TEXT DEFAULT NULL,
542+
workflow_id TEXT DEFAULT NULL,
543+
app_version TEXT DEFAULT NULL,
544+
timeout_ms BIGINT DEFAULT NULL,
545+
deadline_epoch_ms BIGINT DEFAULT NULL,
546+
deduplication_id TEXT DEFAULT NULL,
547+
priority INT4 DEFAULT NULL,
548+
queue_partition_key TEXT DEFAULT NULL,
549+
authenticated_user TEXT DEFAULT NULL,
550+
authenticated_roles TEXT DEFAULT NULL,
551+
delay_until_epoch_ms BIGINT DEFAULT NULL
552+
) RETURNS TEXT AS $$
553+
DECLARE
554+
v_workflow_id TEXT;
555+
v_serialized_inputs TEXT;
556+
v_owner_xid TEXT;
557+
v_now BIGINT;
558+
v_recovery_attempts INT4 := 0;
559+
v_priority INT4;
560+
v_status TEXT;
561+
BEGIN
562+
563+
-- Validate required parameters
564+
IF workflow_name IS NULL OR workflow_name = '' THEN
565+
RAISE EXCEPTION 'Workflow name cannot be null or empty';
566+
END IF;
567+
IF queue_name IS NULL OR queue_name = '' THEN
568+
RAISE EXCEPTION 'Queue name cannot be null or empty';
569+
END IF;
570+
IF named_args IS NOT NULL AND jsonb_typeof(named_args::jsonb) != 'object' THEN
571+
RAISE EXCEPTION 'Named args must be a JSON object';
572+
END IF;
573+
IF workflow_id IS NOT NULL AND workflow_id = '' THEN
574+
RAISE EXCEPTION 'Workflow ID cannot be an empty string if provided.';
575+
END IF;
576+
IF delay_until_epoch_ms IS NOT NULL AND delay_until_epoch_ms < 0 THEN
577+
RAISE EXCEPTION 'delay_until_epoch_ms must be >= 0';
578+
END IF;
579+
580+
v_workflow_id := COALESCE(workflow_id, gen_random_uuid()::TEXT);
581+
v_owner_xid := gen_random_uuid()::TEXT;
582+
v_priority := COALESCE(priority, 0);
583+
v_serialized_inputs := json_build_object(
584+
'positionalArgs', positional_args,
585+
'namedArgs', named_args
586+
)::TEXT;
587+
v_now := EXTRACT(epoch FROM now()) * 1000;
588+
v_status := CASE WHEN delay_until_epoch_ms IS NULL THEN 'ENQUEUED' ELSE 'DELAYED' END;
589+
590+
INSERT INTO "${schemaName}".workflow_status (
591+
workflow_uuid, status, inputs,
592+
name, class_name, config_name,
593+
queue_name, deduplication_id, priority, queue_partition_key,
594+
application_version,
595+
created_at, updated_at, recovery_attempts,
596+
workflow_timeout_ms, workflow_deadline_epoch_ms,
597+
parent_workflow_id, owner_xid, serialization,
598+
authenticated_user, authenticated_roles,
599+
delay_until_epoch_ms
600+
) VALUES (
601+
v_workflow_id, v_status, v_serialized_inputs,
602+
workflow_name, class_name, config_name,
603+
queue_name, deduplication_id, v_priority, queue_partition_key,
604+
app_version,
605+
v_now, v_now, v_recovery_attempts,
606+
timeout_ms, deadline_epoch_ms,
607+
NULL, v_owner_xid, 'portable_json',
608+
authenticated_user, authenticated_roles,
609+
delay_until_epoch_ms
610+
)
611+
ON CONFLICT (workflow_uuid)
612+
DO UPDATE SET
613+
updated_at = EXCLUDED.updated_at;
614+
615+
RETURN v_workflow_id;
616+
617+
EXCEPTION
618+
WHEN unique_violation THEN
619+
RAISE EXCEPTION 'DBOS queue duplicated'
620+
USING DETAIL = format('Workflow %s with queue %s and deduplication ID %s already exists', v_workflow_id, queue_name, deduplication_id),
621+
ERRCODE = 'unique_violation';
622+
END;
623+
$$ LANGUAGE plpgsql;`,
624+
...(isCockroach
625+
? []
626+
: [
627+
`ALTER FUNCTION "${schemaName}".enqueue_workflow(
628+
TEXT, TEXT, JSON[], JSON, TEXT, TEXT, TEXT, TEXT, BIGINT, BIGINT, TEXT, INT4, TEXT, TEXT, TEXT, BIGINT
629+
) SET search_path = pg_catalog, pg_temp`,
630+
]),
631+
],
632+
},
530633
];
531634
}

tests/pg-client-functions.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,132 @@ describe('PostgreSQL Client Functions', () => {
153153
}
154154
});
155155

156+
test('pg-enqueue-with-auth-metadata', async () => {
157+
const wfid = `pg-auth-test-${Date.now()}`;
158+
const user = 'alice';
159+
const roles = ['admin', 'reader'];
160+
const dbClient = new Client(poolConfig);
161+
162+
try {
163+
await dbClient.connect();
164+
165+
await dbClient.query(
166+
`
167+
SELECT dbos.enqueue_workflow(
168+
workflow_name => 'enqueueTest',
169+
queue_name => $1,
170+
class_name => 'ClientTest',
171+
workflow_id => $2,
172+
positional_args => ARRAY[$3::JSON, $4::JSON, $5::JSON],
173+
authenticated_user => $6,
174+
authenticated_roles => $7
175+
)
176+
`,
177+
[
178+
testQueueName,
179+
wfid,
180+
JSON.stringify(42),
181+
JSON.stringify('test'),
182+
JSON.stringify({ first: 'John', last: 'Doe', age: 30 }),
183+
user,
184+
JSON.stringify(roles),
185+
],
186+
);
187+
} finally {
188+
await dbClient.end();
189+
}
190+
191+
try {
192+
await DBOS.launch();
193+
await DBOS.registerQueue(testQueueName, { onConflict: 'always_update' });
194+
195+
const handle = DBOS.retrieveWorkflow<string>(wfid);
196+
const status = await handle.getStatus();
197+
expect(status).toBeDefined();
198+
expect(status!.authenticatedUser).toBe(user);
199+
expect(status!.authenticatedRoles).toEqual(roles);
200+
201+
const result = await handle.getResult();
202+
expect(result).toBe('42-test-{"first":"John","last":"Doe","age":30}');
203+
} finally {
204+
await DBOS.shutdown();
205+
}
206+
});
207+
208+
test('pg-enqueue-with-delay', async () => {
209+
const wfid = `pg-delay-test-${Date.now()}`;
210+
// Far enough in the future that the workflow won't be promoted during the test
211+
const delayUntil = Date.now() + 60_000;
212+
const dbClient = new Client(poolConfig);
213+
214+
try {
215+
await dbClient.connect();
216+
217+
await dbClient.query(
218+
`
219+
SELECT dbos.enqueue_workflow(
220+
workflow_name => 'enqueueTest',
221+
queue_name => $1,
222+
class_name => 'ClientTest',
223+
workflow_id => $2,
224+
positional_args => ARRAY[$3::JSON, $4::JSON, $5::JSON],
225+
delay_until_epoch_ms => $6
226+
)
227+
`,
228+
[
229+
testQueueName,
230+
wfid,
231+
JSON.stringify(42),
232+
JSON.stringify('test'),
233+
JSON.stringify({ first: 'John', last: 'Doe', age: 30 }),
234+
delayUntil,
235+
],
236+
);
237+
} finally {
238+
await dbClient.end();
239+
}
240+
241+
try {
242+
await DBOS.launch();
243+
await DBOS.registerQueue(testQueueName, { onConflict: 'always_update' });
244+
245+
const handle = DBOS.retrieveWorkflow<string>(wfid);
246+
const status = await handle.getStatus();
247+
expect(status).toBeDefined();
248+
expect(status!.status).toBe('DELAYED');
249+
expect(status!.delayUntilEpochMS).toBe(delayUntil);
250+
251+
await DBOS.cancelWorkflow(wfid);
252+
} finally {
253+
await DBOS.shutdown();
254+
}
255+
});
256+
257+
test('pg-enqueue-negative-delay-rejected', async () => {
258+
const wfid = `pg-negative-delay-${Date.now()}`;
259+
const dbClient = new Client(poolConfig);
260+
261+
try {
262+
await dbClient.connect();
263+
264+
await expect(
265+
dbClient.query(
266+
`
267+
SELECT dbos.enqueue_workflow(
268+
workflow_name => 'enqueueTest',
269+
queue_name => $1,
270+
workflow_id => $2,
271+
delay_until_epoch_ms => -1
272+
)
273+
`,
274+
[testQueueName, wfid],
275+
),
276+
).rejects.toThrow(/delay_until_epoch_ms must be >= 0/);
277+
} finally {
278+
await dbClient.end();
279+
}
280+
});
281+
156282
test('pg-enqueue-gen-wfid', async () => {
157283
const dbClient = new Client(poolConfig);
158284
let wfid: string;

0 commit comments

Comments
 (0)