mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(kv): claim parked jobs by member instead of secondary key (#2232)
This commit is contained in:
1 parent
5c2dca1c51
commit
eeea391b63
6 files changed
+91
-12
No files matched your search
@@ -13,6 +13,11 @@ export interface ParkedScheduledJob {
|
||||
ledgerJobId: string | null;
|
||||
}
|
||||
|
||||
export interface ReadyScheduledJob {
|
||||
job: ParkedScheduledJob;
|
||||
member: string;
|
||||
}
|
||||
|
||||
const QUEUE_KEY = 'scheduled_job_queue';
|
||||
const SECONDARY_KEY_PREFIX = 'scheduled_job_queue:';
|
||||
|
||||
@@ -75,22 +80,22 @@ export class KVScheduledJobQueueService {
|
||||
}
|
||||
}
|
||||
|
||||
async claimJob(jobIdentity: string): Promise<boolean> {
|
||||
async claimJob(jobIdentity: string, member = ''): Promise<boolean> {
|
||||
try {
|
||||
return await this.kvClient.removeBulkDeletion(QUEUE_KEY, this.getSecondaryKey(jobIdentity));
|
||||
return await this.kvClient.removeBulkDeletion(QUEUE_KEY, this.getSecondaryKey(jobIdentity), member);
|
||||
} catch (error) {
|
||||
Logger.error({error, jobIdentity}, 'Failed to claim parked scheduled job');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getReadyJobs(nowMs: number, limit: number): Promise<Array<ParkedScheduledJob>> {
|
||||
async getReadyJobs(nowMs: number, limit: number): Promise<Array<ReadyScheduledJob>> {
|
||||
try {
|
||||
const results = await this.kvClient.zrangebyscore(QUEUE_KEY, '-inf', nowMs, 'LIMIT', 0, limit);
|
||||
const jobs: Array<ParkedScheduledJob> = [];
|
||||
const jobs: Array<ReadyScheduledJob> = [];
|
||||
for (const result of results) {
|
||||
try {
|
||||
jobs.push(this.deserializeQueueItem(result));
|
||||
jobs.push({job: this.deserializeQueueItem(result), member: result});
|
||||
} catch (error) {
|
||||
Logger.error({error, result}, 'Failed to parse parked scheduled job entry');
|
||||
}
|
||||
|
||||
@@ -667,9 +667,13 @@ export class MockKVProvider implements IKVProvider {
|
||||
this.expiries.delete(secondaryKey);
|
||||
}
|
||||
|
||||
async removeBulkDeletion(queueKey: string, secondaryKey: string): Promise<boolean> {
|
||||
this.removeBulkDeletionSpy(queueKey, secondaryKey);
|
||||
async removeBulkDeletion(queueKey: string, secondaryKey: string, member = ''): Promise<boolean> {
|
||||
this.removeBulkDeletionSpy(queueKey, secondaryKey, member);
|
||||
this.evictIfExpired(secondaryKey);
|
||||
if (member !== '' && (await this.zrem(queueKey, member)) === 1) {
|
||||
this.deleteKey(secondaryKey);
|
||||
return true;
|
||||
}
|
||||
const value = this.stringStore.get(secondaryKey);
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
|
||||
@@ -28,9 +28,9 @@ const processScheduledJobQueue: WorkerTaskHandler = async (_payload, helpers) =>
|
||||
let claimedElsewhereCount = 0;
|
||||
let droppedCount = 0;
|
||||
let failedCount = 0;
|
||||
for (const job of readyJobs) {
|
||||
for (const {job, member} of readyJobs) {
|
||||
try {
|
||||
const claimed = await scheduledJobQueueService.claimJob(job.jobIdentity);
|
||||
const claimed = await scheduledJobQueueService.claimJob(job.jobIdentity, member);
|
||||
if (!claimed) {
|
||||
claimedElsewhereCount += 1;
|
||||
continue;
|
||||
|
||||
@@ -167,13 +167,14 @@ describe('Scheduled job due queue', () => {
|
||||
expect(await scheduledJobQueue.getReadyJobs(Date.now(), 10)).toHaveLength(0);
|
||||
const due = await scheduledJobQueue.getReadyJobs(runAt.getTime() - LANE_ACK_WAIT_MS, 10);
|
||||
expect(due).toHaveLength(1);
|
||||
expect(due[0]).toEqual({
|
||||
expect(due[0]!.job).toEqual({
|
||||
jobIdentity: '4242',
|
||||
taskType: TASK_TYPE,
|
||||
payload: {scheduledMessageId: '77'},
|
||||
runAtMs: runAt.getTime(),
|
||||
ledgerJobId: '4242',
|
||||
});
|
||||
expect(due[0]!.member).toContain('"jobIdentity":"4242"');
|
||||
});
|
||||
|
||||
it('releases a parked job exactly once when its due time arrives', async () => {
|
||||
@@ -277,3 +278,66 @@ describe('Scheduled job due queue', () => {
|
||||
expect(await scheduledJobQueue.getQueueSize()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parked job claim survives secondary key loss', () => {
|
||||
it('claims by member when the secondary key was evicted', async () => {
|
||||
const kv = new MockKVProvider();
|
||||
const service = new KVScheduledJobQueueService(kv);
|
||||
const runAtMs = Date.now() + 86_400_000;
|
||||
const job = {
|
||||
jobIdentity: 'job-evicted',
|
||||
taskType: TASK_TYPE,
|
||||
payload: {message_id: '1'} as WorkerJobPayload,
|
||||
runAtMs,
|
||||
ledgerJobId: null,
|
||||
};
|
||||
|
||||
await service.parkJob(job, new Date(Date.now() - 1000));
|
||||
expect(await service.getQueueSize()).toBe(1);
|
||||
|
||||
await kv.del('scheduled_job_queue:job-evicted');
|
||||
|
||||
const ready = await service.getReadyJobs(Date.now(), 10);
|
||||
expect(ready).toHaveLength(1);
|
||||
|
||||
expect(await service.claimJob(ready[0]!.job.jobIdentity, ready[0]!.member)).toBe(true);
|
||||
expect(await service.getQueueSize()).toBe(0);
|
||||
});
|
||||
|
||||
it('leaves the entry wedged when claiming without the member', async () => {
|
||||
const kv = new MockKVProvider();
|
||||
const service = new KVScheduledJobQueueService(kv);
|
||||
const job = {
|
||||
jobIdentity: 'job-wedged',
|
||||
taskType: TASK_TYPE,
|
||||
payload: {message_id: '2'} as WorkerJobPayload,
|
||||
runAtMs: Date.now() + 86_400_000,
|
||||
ledgerJobId: null,
|
||||
};
|
||||
|
||||
await service.parkJob(job, new Date(Date.now() - 1000));
|
||||
await kv.del('scheduled_job_queue:job-wedged');
|
||||
|
||||
expect(await service.claimJob('job-wedged')).toBe(false);
|
||||
expect(await service.getQueueSize()).toBe(1);
|
||||
});
|
||||
|
||||
it('still reports a second claim of the same member as already claimed', async () => {
|
||||
const kv = new MockKVProvider();
|
||||
const service = new KVScheduledJobQueueService(kv);
|
||||
const job = {
|
||||
jobIdentity: 'job-twice',
|
||||
taskType: TASK_TYPE,
|
||||
payload: {message_id: '3'} as WorkerJobPayload,
|
||||
runAtMs: Date.now() + 86_400_000,
|
||||
ledgerJobId: null,
|
||||
};
|
||||
|
||||
await service.parkJob(job, new Date(Date.now() - 1000));
|
||||
const ready = await service.getReadyJobs(Date.now(), 10);
|
||||
const member = ready[0]!.member;
|
||||
|
||||
expect(await service.claimJob('job-twice', member)).toBe(true);
|
||||
expect(await service.claimJob('job-twice', member)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user