Skip to content

Commit dc705f1

Browse files
authored
No-version workflows are only dequeued by latest-version executors (#1291)
1 parent c51a576 commit dc705f1

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

src/system_database.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,7 +2649,17 @@ export class SystemDatabase {
26492649
}
26502650

26512651
// Retrieve the first max_tasks workflows in the queue.
2652-
// Only retrieve workflows of the local version (or without version set)
2652+
const latestVersionResult = await client.query<{ version_name: string }>(
2653+
`SELECT version_name
2654+
FROM "${this.schemaName}".application_versions
2655+
ORDER BY version_timestamp DESC LIMIT 1`,
2656+
);
2657+
const latestVersion = latestVersionResult.rows[0]?.version_name;
2658+
const isLatestVersion = latestVersion === undefined || latestVersion === appVersion;
2659+
const versionClause = isLatestVersion
2660+
? '(application_version = $3 OR application_version IS NULL)'
2661+
: 'application_version = $3';
2662+
26532663
const lockMode = queue.concurrency ? 'FOR UPDATE NOWAIT' : 'FOR UPDATE SKIP LOCKED';
26542664
const limitClause = maxTasks !== Infinity ? `LIMIT ${maxTasks}` : '';
26552665

@@ -2659,7 +2669,7 @@ export class SystemDatabase {
26592669
FROM "${this.schemaName}".workflow_status
26602670
WHERE status = $1
26612671
AND queue_name = $2
2662-
AND (application_version IS NULL OR application_version = $3)
2672+
AND ${versionClause}
26632673
${partitionFilter.replace('$PARTITION', '$4')}
26642674
ORDER BY priority ASC, created_at ASC
26652675
${limitClause}

tests/wfqueue.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ describe('queued-wf-tests-simple', () => {
102102
TestResumeQueues.queue,
103103
TestResumeQueuesPartitioned.queue,
104104
TestConcurrencyAcrossVersions.queue,
105+
TestVersionlessDequeue.queue,
105106
waitFirstQueue,
106107
]) {
107108
await DBOS.registerQueue(ref.name, { onConflict: 'always_update', ...ref.config });
@@ -768,6 +769,18 @@ describe('queued-wf-tests-simple', () => {
768769
}
769770
}
770771

772+
class TestVersionlessDequeue {
773+
static queue: QueueRef = {
774+
name: 'TestVersionlessDequeue',
775+
config: { workerConcurrency: 1, ...testPolling },
776+
};
777+
778+
@DBOS.workflow()
779+
static async versionlessWorkflow() {
780+
return Promise.resolve(DBOS.workflowID);
781+
}
782+
}
783+
771784
test('test-concurrency-across-versions', async () => {
772785
expect(config.systemDatabaseUrl).toBeDefined();
773786
const client = await DBOSClient.create({ systemDatabaseUrl: config.systemDatabaseUrl! });
@@ -790,6 +803,43 @@ describe('queued-wf-tests-simple', () => {
790803
await client.destroy();
791804
});
792805

806+
test('test-versionless-dequeue-requires-latest-version', async () => {
807+
expect(config.systemDatabaseUrl).toBeDefined();
808+
const client = await DBOSClient.create({ systemDatabaseUrl: config.systemDatabaseUrl! });
809+
const sysdb = DBOSExecutor.globalInstance!.systemDatabase;
810+
const workerVersion = globalParams.appVersion;
811+
812+
// Register a newer application version so this worker is no longer the latest.
813+
const newerVersion = `newer-${randomUUID()}`;
814+
await sysdb.createApplicationVersion(newerVersion);
815+
await sysdb.updateApplicationVersionTimestamp(newerVersion, Date.now() + 1_000_000);
816+
817+
// Enqueue a version-less workflow (application_version IS NULL): no appVersion field.
818+
const versionlessHandle = await client.enqueue({
819+
queueName: TestVersionlessDequeue.queue.name,
820+
workflowName: 'versionlessWorkflow',
821+
workflowClassName: 'TestVersionlessDequeue',
822+
});
823+
824+
// Enqueue a workflow tagged with this worker's own (non-latest) version.
825+
const taggedHandle = await DBOS.startWorkflow(TestVersionlessDequeue, {
826+
queueName: TestVersionlessDequeue.queue.name,
827+
}).versionlessWorkflow();
828+
829+
// A matching-version worker still dequeues its own version-tagged workflow.
830+
await expect(taggedHandle.getResult()).resolves.toBeTruthy();
831+
832+
// But the version-less workflow stays ENQUEUED: this worker is not the latest version.
833+
await sleepms(1000);
834+
expect((await versionlessHandle.getStatus())?.status).toBe(StatusString.ENQUEUED);
835+
836+
// Make this worker the latest version again; the version-less workflow now completes.
837+
await sysdb.updateApplicationVersionTimestamp(workerVersion, Date.now() + 2_000_000);
838+
await expect(versionlessHandle.getResult()).resolves.toBeTruthy();
839+
840+
await client.destroy();
841+
});
842+
793843
test('test_wait_first_queue', async () => {
794844
const numTasks = WaitFirstQueueTest.numTasks;
795845
WaitFirstQueueTest.resetEvents();

0 commit comments

Comments
 (0)