mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
fix(api): rebuild the deletion queue under its lock (#2292)
This commit is contained in:
1 parent
f2eddeae4d
commit
cd42dd8ca7
3 files changed
+113
-7
No files matched your search
@@ -34,6 +34,7 @@ import {VisionarySlotInitializer} from '../stripe/VisionarySlotInitializer';
|
|||||||
import {VoiceDataInitializer} from '../voice/VoiceDataInitializer';
|
import {VoiceDataInitializer} from '../voice/VoiceDataInitializer';
|
||||||
import {JetStreamWorkerQueue} from '../worker/JetStreamWorkerQueue';
|
import {JetStreamWorkerQueue} from '../worker/JetStreamWorkerQueue';
|
||||||
import {WorkerService} from '../worker/WorkerService';
|
import {WorkerService} from '../worker/WorkerService';
|
||||||
|
import {ensureDeletionQueueState} from './DeletionQueueStartup';
|
||||||
|
|
||||||
let jsConnectionManager: JetStreamConnectionManager | null = null;
|
let jsConnectionManager: JetStreamConnectionManager | null = null;
|
||||||
|
|
||||||
@@ -134,13 +135,7 @@ export function createInitializer(config: APIConfig, logger: ILogger): () => Pro
|
|||||||
logger.info('JetStream worker service initialized');
|
logger.info('JetStream worker service initialized');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const kvDeletionQueue = getKVAccountDeletionQueue();
|
await ensureDeletionQueueState(getKVAccountDeletionQueue(), logger);
|
||||||
if (await kvDeletionQueue.needsRebuild()) {
|
|
||||||
logger.info('KV deletion queue needs rebuild, rebuilding...');
|
|
||||||
await kvDeletionQueue.rebuildState();
|
|
||||||
} else {
|
|
||||||
logger.info('KV deletion queue state is healthy');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({error}, 'Failed to verify KV deletion queue state');
|
logger.error({error}, 'Failed to verify KV deletion queue state');
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import type {ILogger} from '../ILogger';
|
||||||
|
import type {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService';
|
||||||
|
|
||||||
|
export async function ensureDeletionQueueState(
|
||||||
|
deletionQueue: KVAccountDeletionQueueService,
|
||||||
|
logger: ILogger,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!(await deletionQueue.needsRebuild())) {
|
||||||
|
logger.info('KV deletion queue state is healthy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lockToken = await deletionQueue.acquireRebuildLock();
|
||||||
|
if (!lockToken) {
|
||||||
|
logger.info('Another instance is rebuilding the KV deletion queue, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('KV deletion queue needs rebuild, rebuilding...');
|
||||||
|
try {
|
||||||
|
await deletionQueue.rebuildState(lockToken);
|
||||||
|
} finally {
|
||||||
|
await deletionQueue.releaseRebuildLock(lockToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {describe, expect, it} from 'vitest';
|
||||||
|
import {createUserID} from '../../BrandedTypes';
|
||||||
|
import {KVAccountDeletionQueueService} from '../../infrastructure/KVAccountDeletionQueueService';
|
||||||
|
import type {User} from '../../models/User';
|
||||||
|
import {MockKVProvider} from '../../test/mocks/MockKVProvider';
|
||||||
|
import {NoopLogger} from '../../test/mocks/NoopLogger';
|
||||||
|
import type {UserRepository} from '../../user/repositories/UserRepository';
|
||||||
|
import {ensureDeletionQueueState} from '../DeletionQueueStartup';
|
||||||
|
|
||||||
|
const WORKER_PAGE_COUNT = 2;
|
||||||
|
|
||||||
|
function createPendingUser(index: number): User {
|
||||||
|
return {
|
||||||
|
id: createUserID(BigInt(9100 + index)),
|
||||||
|
pendingDeletionAt: new Date('2026-06-01T00:00:00.000Z'),
|
||||||
|
deletionReasonCode: 0,
|
||||||
|
} as unknown as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorkerRepository(onPageStart: (page: number) => Promise<void>): UserRepository {
|
||||||
|
let page = 0;
|
||||||
|
return {
|
||||||
|
async scanAllUsersPage() {
|
||||||
|
page += 1;
|
||||||
|
await onPageStart(page);
|
||||||
|
return {
|
||||||
|
users: [createPendingUser(page)],
|
||||||
|
pageState: page < WORKER_PAGE_COUNT ? `page-${page}` : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as unknown as UserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ensureDeletionQueueState', () => {
|
||||||
|
it('leaves a rebuild owned by another instance alone', async () => {
|
||||||
|
const kvClient = new MockKVProvider();
|
||||||
|
let apiScans = 0;
|
||||||
|
const apiRepository = {
|
||||||
|
async scanAllUsersPage() {
|
||||||
|
apiScans += 1;
|
||||||
|
throw new Error('api pod scan failed');
|
||||||
|
},
|
||||||
|
} as unknown as UserRepository;
|
||||||
|
const apiQueue = new KVAccountDeletionQueueService(kvClient, apiRepository);
|
||||||
|
const apiFailures: Array<unknown> = [];
|
||||||
|
const workerQueue = new KVAccountDeletionQueueService(
|
||||||
|
kvClient,
|
||||||
|
createWorkerRepository(async (page) => {
|
||||||
|
if (page !== WORKER_PAGE_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ensureDeletionQueueState(apiQueue, new NoopLogger());
|
||||||
|
} catch (error) {
|
||||||
|
apiFailures.push(error);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const workerToken = await workerQueue.acquireRebuildLock();
|
||||||
|
expect(workerToken).not.toBeNull();
|
||||||
|
await workerQueue.rebuildState(workerToken);
|
||||||
|
expect(await workerQueue.releaseRebuildLock(workerToken!)).toBe(true);
|
||||||
|
|
||||||
|
const queued = await workerQueue.getReadyDeletions(Date.parse('2026-06-02T00:00:00.000Z'), 10);
|
||||||
|
expect(queued.map((deletion) => deletion.userId).sort()).toEqual([9101n, 9102n]);
|
||||||
|
expect(apiScans).toBe(0);
|
||||||
|
expect(apiFailures).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rebuilds under the lock when no other instance holds it', async () => {
|
||||||
|
const kvClient = new MockKVProvider();
|
||||||
|
const queue = new KVAccountDeletionQueueService(
|
||||||
|
kvClient,
|
||||||
|
createWorkerRepository(async () => {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await ensureDeletionQueueState(queue, new NoopLogger());
|
||||||
|
|
||||||
|
const queued = await queue.getReadyDeletions(Date.parse('2026-06-02T00:00:00.000Z'), 10);
|
||||||
|
expect(queued.map((deletion) => deletion.userId).sort()).toEqual([9101n, 9102n]);
|
||||||
|
expect(await queue.acquireRebuildLock()).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user