mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import path from 'node:path';
|
|
import { Worker } from 'node:worker_threads';
|
|
import { fileURLToPath } from 'url';
|
|
import { Injectable, Scope } from 'graphql-modules';
|
|
import { Logger, registerWorkerLogging } from '../../shared/providers/logger';
|
|
import { BatchProcessedEvent, BatchProcessEvent } from './persisted-document-ingester';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
type PendingTaskRecord = {
|
|
resolve: (data: BatchProcessedEvent) => void;
|
|
reject: (err: unknown) => void;
|
|
};
|
|
|
|
@Injectable({
|
|
scope: Scope.Singleton,
|
|
global: true,
|
|
})
|
|
export class PersistedDocumentScheduler {
|
|
private logger: Logger;
|
|
private workers: Array<
|
|
(input: BatchProcessEvent['data']) => Promise<BatchProcessedEvent['data']>
|
|
>;
|
|
|
|
constructor(logger: Logger) {
|
|
this.logger = logger.child({ source: 'PersistedDocumentScheduler' });
|
|
this.workers = Array.from({ length: 4 }, (_, i) => this.createWorker(i));
|
|
}
|
|
|
|
private createWorker(index: number) {
|
|
this.logger.debug('Creating worker %s', index);
|
|
const name = `persisted-documents-worker-${index}`;
|
|
const worker = new Worker(path.join(__dirname, 'persisted-documents-worker.js'), {
|
|
name,
|
|
});
|
|
const tasks = new Map<string, PendingTaskRecord>();
|
|
|
|
worker.on('error', error => {
|
|
console.error(error);
|
|
this.logger.error('Worker error %s', error);
|
|
});
|
|
|
|
worker.on('exit', code => {
|
|
this.logger.error('Worker stopped with exit code %s', String(code));
|
|
if (code === 0) {
|
|
return;
|
|
}
|
|
|
|
this.logger.debug('Re-Creating worker %s', index);
|
|
this.workers[index] = this.createWorker(index);
|
|
|
|
this.logger.debug('Cancel pending tasks %s', index);
|
|
for (const [, task] of tasks) {
|
|
task.reject(new Error('Worker stopped.'));
|
|
}
|
|
});
|
|
|
|
registerWorkerLogging(this.logger, worker, name);
|
|
|
|
worker.on(
|
|
'message',
|
|
(data: BatchProcessedEvent | { event: 'error'; id: string; err: Error }) => {
|
|
if (data.event === 'error') {
|
|
tasks.get(data.id)?.reject(data.err);
|
|
}
|
|
|
|
if (data.event === 'processedBatch') {
|
|
tasks.get(data.id)?.resolve(data);
|
|
}
|
|
},
|
|
);
|
|
|
|
const { logger } = this;
|
|
|
|
return async function batchProcess(data: BatchProcessEvent['data']) {
|
|
const id = crypto.randomUUID();
|
|
const d = Promise.withResolvers<BatchProcessedEvent>();
|
|
const timeout = setTimeout(() => {
|
|
task.reject(new Error('Timeout, worker did not respond within time.'));
|
|
}, 20_000);
|
|
|
|
const task: PendingTaskRecord = {
|
|
resolve: data => {
|
|
tasks.delete(id);
|
|
clearTimeout(timeout);
|
|
d.resolve(data);
|
|
},
|
|
reject: err => {
|
|
tasks.delete(id);
|
|
clearTimeout(timeout);
|
|
d.reject(err);
|
|
},
|
|
};
|
|
|
|
tasks.set(id, task);
|
|
const time = process.hrtime();
|
|
|
|
worker.postMessage({
|
|
event: 'PROCESS',
|
|
id,
|
|
data,
|
|
});
|
|
|
|
const result = await d.promise.finally(() => {
|
|
const endTime = process.hrtime(time);
|
|
logger.debug('Time taken: %ds %dms', endTime[0], endTime[1] / 1000000);
|
|
});
|
|
|
|
return result.data;
|
|
};
|
|
}
|
|
|
|
private getRandomWorker() {
|
|
return this.workers[Math.floor(Math.random() * this.workers.length)];
|
|
}
|
|
|
|
async processBatch(data: BatchProcessEvent['data']) {
|
|
return this.getRandomWorker()(data);
|
|
}
|
|
}
|