"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerRunnerService = void 0;
const __ΩInstanceType = ['T', 'args', 'new', 'InstanceType', 'l>e"!R"RPde#!P"@2"h"!/#qk#\'QRb!Pde"!p)w$y'];
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const bullmq_1 = require("bullmq");
const app_1 = require("../../app");
const resolver_1 = require("../../app/resolver");
const state_1 = require("../../app/state");
const healthcheck_service_1 = require("../../health/healthcheck.service");
const helpers_1 = require("../../helpers");
const decorators_1 = require("../../helpers/framework/decorators");
const redis_1 = require("../../helpers/redis/redis");
const telemetry_1 = require("../../telemetry");
const leader_1 = require("../leader");
const logger_1 = require("../logger");
const queue_1 = require("./queue");
const recorder_1 = require("./recorder");
/*@ts-ignore*/
var { __ΩJobClass } = require('./types');
const types_1 = require("./types");
class WorkerRunnerService {
    constructor(hcSvc, dbProvider) {
        this.hcSvc = hcSvc;
        this.dbProvider = dbProvider;
        this.jobHandlers = (Map.Ω = [['&'], [() => __ΩInstanceType, () => __ΩJobClass, 'n"o!"']], new Map());
        this.appConfig = (0, resolver_1.getAppConfig)();
        this.queueName = this.appConfig.BULL_QUEUE;
        this.logger = (0, logger_1.createLogger)(this, { queue: this.queueName });
    }
    async start() {
        this.queue = queue_1.WorkerQueueRegistry.getQueue(this.queueName);
        const allJobClasses = (decorators_1.getRegisteredClasses.Ω = [[() => __ΩJobClass, 'n!']], (0, decorators_1.getRegisteredClasses)(types_1.WorkerSymbol));
        for (const jobClass of allJobClasses) {
            if (jobClass.QUEUE_NAME === this.queueName) {
                this.logger.info('Registering job', { id: { name: jobClass.name, schedule: jobClass.CRON_SCHEDULE } });
                const handlerInstance = (0, resolver_1.resolveDeep)(jobClass);
                if (!handlerInstance)
                    throw new Error(`Cannot resolve job handler: ${jobClass.name}`);
                this.jobHandlers.set(jobClass.name, handlerInstance);
                if (jobClass.CRON_SCHEDULE) {
                    this.queue.add(jobClass.name, {}, { repeat: { pattern: jobClass.CRON_SCHEDULE } });
                }
            }
        }
        const { options, prefix } = (0, redis_1.createRedisOptions)('BULL');
        this.worker = new bullmq_1.Worker(this.queueName, __assignType(async (job) => this.withJobSpan(job, () => (0, helpers_1.withContextData)({
            job: {
                queue: this.queueName,
                id: job.id,
                name: job.name
            },
            traceId: (0, telemetry_1.getTraceContext)()?.traceId
        }, async () => {
            const handler = this.jobHandlers.get(job.name);
            if (!handler) {
                throw new Error(`Job ${job.name} is not registered`);
            }
            try {
                this.runningJob = job;
                return await handler.handle(job.data);
            }
            finally {
                this.runningJob = undefined;
            }
        })), ['job', '', 'P"2!"/"']), {
            concurrency: 1,
            connection: options,
            prefix: `${prefix}:bmq`
        });
        this.worker.on('active', __assignType(job => {
            this.logger.info('Job activated', { job: { name: job.name, id: job.id } });
        }, ['job', '', 'P"2!"/"']));
        this.worker.on('completed', __assignType(job => {
            this.logger.info('Job completed', { job: { name: job.name, id: job.id } });
        }, ['job', '', 'P"2!"/"']));
        this.worker.on('failed', __assignType((job, err) => {
            this.logger.error(`Job failed: ${err.message}`, err, { job: job ? { name: job.name, id: job.id } : undefined });
        }, ['job', 'err', '', 'P"2!"2""/#']));
        this.worker.on('stalled', __assignType(jobId => {
            this.logger.warn('Job stalled', { jobId });
        }, ['jobId', '', 'P"2!"/"']));
        this.worker.on('error', __assignType(err => {
            this.logger.error('Worker error', err);
        }, ['err', '', 'P"2!"/"']));
        this.worker.on('ready', () => {
            this.logger.info('Worker ready');
        });
        this.logger.info('Worker started');
        this.hcSvc.register('Worker Runner', async () => {
            if (!this.isRedisReady()) {
                throw new Error('Worker Redis connection is not ready');
            }
        });
        // Set up recorder with leader election
        this.recorder = new recorder_1.WorkerRecorderService(this.dbProvider);
        await this.recorder.ensureTableExists();
        this.leader = new leader_1.LeaderService('worker-recorder');
        this.leader.setBecameLeaderCallback(async () => {
            this.logger.info('This runner is now the recorder leader');
            await this.recorder.start();
        });
        this.leader.setLostLeaderCallback(async () => {
            this.logger.info('This runner lost recorder leadership');
            await this.recorder.stop();
        });
        this.leader.start();
    }
    async withJobSpan(job, fn) {
        if (job.repeatJobKey) {
            let innerSpan;
            const result = await (0, telemetry_1.withRootSpan)(`Job ${job.name}`, { jobId: job.id, schedulerTrace: (0, telemetry_1.getTraceContext)()?.traceId }, () => {
                innerSpan = (0, telemetry_1.getTraceContext)();
                return fn();
            });
            if (innerSpan)
                (0, telemetry_1.setSpanAttributes)({ jobTraceId: innerSpan.traceId });
            return result;
        }
        else {
            return fn();
        }
    }
    isRedisReady() {
        return this.worker?.['blockingConnection']['_client'].status === 'ready' || this.worker?.['blockingConnection']['_client'].status === 'wait';
    }
    async shutdown() {
        // Stop leader election first (releases Redis lock)
        if (this.leader) {
            await this.leader.stop();
        }
        // Explicitly stop recorder (LeaderService.stop() does NOT call lostLeaderCallback)
        if (this.recorder) {
            await this.recorder.stop();
        }
        // there's something crazy going on with worker shutdown when it hasn't successfully connected to Redis
        if (this.isRedisReady()) {
            await this.worker?.pause(true);
            while (this.runningJob) {
                this.logger.warn('Waiting for job to finish', { job: { name: this.runningJob.name, id: this.runningJob.id } });
                await (0, helpers_1.sleepSecs)(1);
            }
            await this.worker?.close();
        }
        else {
            this.worker?.close(true);
            this.worker?.disconnect();
        }
    }
}
exports.WorkerRunnerService = WorkerRunnerService;
WorkerRunnerService.__type = [() => app_1.BaseAppConfig, 'appConfig', 'queueName', () => logger_1.ExtendedLogger, 'logger', () => bullmq_1.Worker, 'worker', () => bullmq_1.Queue, 'queue', 'jobHandlers', function () { return (Map.Ω = [['&'], [() => __ΩInstanceType, () => __ΩJobClass, 'n"o!"']], new Map()); }, () => bullmq_1.Job, 'runningJob', () => recorder_1.WorkerRecorderService, 'recorder', () => leader_1.LeaderService, 'leader', () => healthcheck_service_1.HealthcheckService, 'hcSvc', () => state_1.DBProvider, 'dbProvider', 'constructor', 'start', () => bullmq_1.Job, 'job', '', 'fn', 'withJobSpan', 'isRedisReady', 'shutdown', 'WorkerRunnerService', 'P7!3";&3#;P7$3%;P7&3\'8;P7(3)8;!3*;>+P7,3-8;P7.3/8;P70318;PP7223;P7425;"06P"07PP7829P"`/:2;"0<;P"0=;P"0>5w?'];
//# sourceMappingURL=runner.js.map