UNPKG

koatty_schedule

Version:
968 lines (958 loc) 35.8 kB
/*! * @Author: richen * @Date: 2025-06-23 00:55:12 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ import { IOCContainer } from 'koatty_container'; import { Redlock } from '@sesamecare-oss/redlock'; import { Redis } from 'ioredis'; import { DefaultLogger } from 'koatty_logger'; import { Helper } from 'koatty_lib'; import { CronJob } from 'cron'; /* * @Description: Configuration management for koatty_schedule * @Usage: * @Author: richen * @Date: 2024-01-17 15:30:00 * @LastEditTime: 2024-01-17 16:30:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ const COMPONENT_SCHEDULED = 'COMPONENT_SCHEDULED'; /** * Decorator types supported by the system */ var DecoratorType; (function (DecoratorType) { DecoratorType["SCHEDULED"] = "SCHEDULED"; DecoratorType["REDLOCK"] = "REDLOCK"; })(DecoratorType || (DecoratorType = {})); /** * Validate cron expression format * @param cron - Cron expression to validate * @throws {Error} When cron expression is invalid */ function validateCronExpression(cron) { if (!cron || typeof cron !== 'string') { throw new Error('Cron expression must be a non-empty string'); } const cronParts = cron.trim().split(/\s+/); // Cron expressions should have 5 or 6 parts (with or without seconds) if (cronParts.length < 5 || cronParts.length > 6) { throw new Error(`Invalid cron expression format. Expected 5 or 6 parts, got ${cronParts.length}`); } // For 6-part cron (with seconds), validate each part if (cronParts.length === 6) { const [seconds, minutes, hours] = cronParts; // Basic validation for obvious invalid values if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(seconds)) { throw new Error('Invalid seconds field in cron expression'); } if (!/^(\*|[0-9]|[0-5][0-9]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(minutes)) { throw new Error('Invalid minutes field in cron expression'); } if (!/^(\*|[0-9]|1[0-9]|2[0-3]|\*\/[0-9]+|[0-9]+-[0-9]+|[0-9]+(,[0-9]+)*)$/.test(hours)) { throw new Error('Invalid hours field in cron expression'); } // Check for simple out-of-range values const secondsValue = parseInt(seconds); if (!isNaN(secondsValue) && (secondsValue < 0 || secondsValue > 59)) { throw new Error('Seconds value must be between 0 and 59'); } } // Additional basic checks for common invalid patterns if (cron.includes('60')) { // Check if 60 appears as a standalone number (not part of a larger number) const parts = cron.split(/[\s,\-\/]/); if (parts.some(part => part === '60')) { throw new Error('Invalid time value: 60 is not valid for any time field'); } } } /** * Validate RedLock method-level options * @param options - RedLock method options to validate * @throws {Error} When options are invalid */ function validateRedLockMethodOptions(options) { if (!options || typeof options !== 'object') { throw new Error('RedLock method options must be an object'); } if (options.lockTimeOut !== undefined) { if (typeof options.lockTimeOut !== 'number' || options.lockTimeOut <= 0) { throw new Error('lockTimeOut must be a positive number'); } } if (options.clockDriftFactor !== undefined) { if (typeof options.clockDriftFactor !== 'number' || options.clockDriftFactor < 0 || options.clockDriftFactor > 1) { throw new Error('clockDriftFactor must be a number between 0 and 1'); } } if (options.maxRetries !== undefined) { if (typeof options.maxRetries !== 'number' || options.maxRetries < 0) { throw new Error('maxRetries must be a non-negative number'); } } if (options.retryDelayMs !== undefined) { if (typeof options.retryDelayMs !== 'number' || options.retryDelayMs < 0) { throw new Error('retryDelayMs must be a non-negative number'); } } } //==================== Global Configuration Management ==================== /** * Global configuration storage */ let globalScheduledOptions = {}; /** * Get global scheduled options * @returns Global scheduled options */ function getGlobalScheduledOptions() { return globalScheduledOptions; } /** * Get effective timezone with priority: user specified > global > default * @param userTimezone - User specified timezone * @returns Effective timezone */ function getEffectiveTimezone(options, userTimezone) { return userTimezone || options.timezone || 'Asia/Beijing'; } /** * Get effective RedLock method options with priority: method options > global options > defaults * @param methodOptions - Method-level RedLock options * @returns Effective RedLock method options with all defaults applied */ function getEffectiveRedLockOptions(methodOptions) { const globalOptions = getGlobalScheduledOptions(); return { lockTimeOut: methodOptions?.lockTimeOut || globalOptions.lockTimeOut || 10000, clockDriftFactor: methodOptions?.clockDriftFactor || globalOptions.clockDriftFactor || 0.01, maxRetries: methodOptions?.maxRetries || globalOptions.maxRetries || 3, retryDelayMs: methodOptions?.retryDelayMs || globalOptions.retryDelayMs || 200 }; } /* * @Description: RedLock utility for distributed locks * @Usage: * @Author: richen * @Date: 2021-11-17 16:18:33 * @LastEditTime: 2024-01-17 15:00:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * Default RedLock configuration */ const defaultRedLockConfig = { lockTimeOut: 10000, clockDriftFactor: 0.01, maxRetries: 3, retryDelayMs: 200, driftFactor: 0.01, retryCount: 3, retryDelay: 200, retryJitter: 200, automaticExtensionThreshold: 500, redisConfig: { host: '127.0.0.1', port: 6379, password: '', db: 0, keyPrefix: 'redlock:' } }; /** * RedLock distributed lock manager * Integrated with koatty IOC container * Implements singleton pattern for safe instance management */ class RedLocker { static instance = null; static instanceLock = Symbol('RedLocker.instanceLock'); redlock = null; redis = null; config; isInitialized = false; initializationPromise = null; // 私有构造函数防止外部直接实例化 constructor(options) { this.config = { ...defaultRedLockConfig, ...options }; // Register this instance in IOC container this.registerInContainer(); } /** * Register RedLocker in IOC container * @private */ registerInContainer() { try { // Register as a singleton component in IOC container IOCContainer.reg('RedLocker', this, { type: 'COMPONENT', args: [] }); DefaultLogger.Debug('RedLocker registered in IOC container'); } catch (_error) { DefaultLogger.Warn('Failed to register RedLocker in IOC container:', _error); } } /** * Get RedLocker singleton instance with thread-safe initialization * @static * @param options - RedLock configuration options (only used for first initialization) * @returns RedLocker singleton instance */ static getInstance(options) { // 双重检查锁定模式确保线程安全 if (!RedLocker.instance) { // 首次创建时使用选项,后续调用忽略选项参数 if (RedLocker.instance === null) { try { // 尝试从IOC容器获取已存在的实例 const containerInstance = IOCContainer.get('RedLocker', 'COMPONENT'); if (containerInstance) { RedLocker.instance = containerInstance; DefaultLogger.Debug('Retrieved existing RedLocker instance from IOC container'); } else { // 创建新的单例实例 RedLocker.instance = new RedLocker(options); DefaultLogger.Debug('Created new RedLocker singleton instance'); } } catch { // IOC容器不可用时直接创建 RedLocker.instance = new RedLocker(options); DefaultLogger.Debug('Created new RedLocker instance outside IOC container'); } } } else if (options) { // 如果实例已存在但传入了新选项,记录警告 DefaultLogger.Warn('RedLocker instance already exists, ignoring new options. Use updateConfig() to change configuration.'); } return RedLocker.instance; } /** * Reset singleton instance (主要用于测试) * @static */ static resetInstance() { if (RedLocker.instance) { RedLocker.instance.close().catch(err => DefaultLogger.Warn('Error while closing RedLocker instance during reset:', err)); RedLocker.instance = null; } } /** * Initialize RedLock with Redis connection * Uses cached promise to avoid duplicate initialization * @private */ async initialize() { // 如果已经初始化,直接返回 if (this.isInitialized) { return; } // 如果正在初始化,等待现有的初始化完成 if (this.initializationPromise) { return this.initializationPromise; } // 创建初始化Promise并缓存 this.initializationPromise = this.performInitialization(); try { await this.initializationPromise; } catch (error) { // 初始化失败时清理缓存,允许重试 this.initializationPromise = null; throw error; } } /** * 执行实际的初始化操作 * @private */ async performInitialization() { try { // Try to get Redis instance from IOC container first try { this.redis = IOCContainer.get('Redis', 'COMPONENT'); DefaultLogger.Debug('Using Redis instance from IOC container'); } catch { // Create new Redis connection if not available in container this.redis = new Redis({ host: this.config.redisConfig.host, port: this.config.redisConfig.port, password: this.config.redisConfig.password || undefined, db: this.config.redisConfig.db || 0, keyPrefix: this.config.redisConfig.keyPrefix, maxRetriesPerRequest: 3 }); DefaultLogger.Debug('Created new Redis connection for RedLocker'); } if (!this.redis) { throw new Error('Failed to initialize Redis connection'); } // Initialize Redlock with the Redis instance this.redlock = new Redlock([this.redis], { driftFactor: this.config.driftFactor, retryCount: this.config.retryCount, retryDelay: this.config.retryDelay, retryJitter: this.config.retryJitter, automaticExtensionThreshold: this.config.automaticExtensionThreshold }); // Set up error handlers this.redlock.on('clientError', (err) => { DefaultLogger.Error('Redis client error in RedLock:', err); }); this.isInitialized = true; DefaultLogger.Info('RedLocker initialized successfully'); } catch (error) { this.isInitialized = false; DefaultLogger.Error('Failed to initialize RedLocker:', error); throw new Error(`RedLocker initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Acquire a distributed lock * @param resources - Resource identifiers to lock * @param ttl - Time to live in milliseconds * @returns Promise<Lock> */ async acquire(resources, ttl) { if (!Array.isArray(resources) || resources.length === 0) { throw new Error('Resources array cannot be empty'); } const lockTtl = ttl || this.config.lockTimeOut; if (lockTtl <= 0) { throw new Error('Lock TTL must be positive'); } // Ensure RedLocker is initialized await this.initialize(); if (!this.redlock) { throw new Error('RedLock is not initialized'); } try { // Add key prefix to resources const prefixedResources = resources.map(resource => `${this.config.redisConfig.keyPrefix}${resource}`); DefaultLogger.Debug(`Acquiring lock for resources: ${prefixedResources.join(', ')} with TTL: ${lockTtl}ms`); const lock = await this.redlock.acquire(prefixedResources, lockTtl); DefaultLogger.Debug(`Lock acquired successfully for resources: ${prefixedResources.join(', ')}`); return lock; } catch (error) { DefaultLogger.Error(`Failed to acquire lock for resources: ${resources.join(', ')}`, error); // 保留原始错误信息,避免过度包装 if (error instanceof Error) { error.message = `Lock acquisition failed: ${error.message}`; throw error; } throw new Error(`Lock acquisition failed: Unknown error`); } } /** * Release a lock * @param lock - Lock instance to release */ async release(lock) { if (!lock) { throw new Error('Lock instance is required'); } try { await lock.release(); DefaultLogger.Debug('Lock released successfully'); } catch (error) { DefaultLogger.Error('Failed to release lock:', error); // 保留原始错误信息 if (error instanceof Error) { error.message = `Lock release failed: ${error.message}`; throw error; } throw new Error(`Lock release failed: Unknown error`); } } /** * Extend a lock's TTL * @param lock - Lock instance to extend * @param ttl - New TTL in milliseconds * @returns Extended lock */ async extend(lock, ttl) { if (!lock) { throw new Error('Lock instance is required'); } if (ttl <= 0) { throw new Error('TTL must be positive'); } try { const extendedLock = await lock.extend(ttl); DefaultLogger.Debug(`Lock extended successfully with TTL: ${ttl}ms`); return extendedLock; } catch (error) { DefaultLogger.Error('Failed to extend lock:', error); throw new Error(`Lock extension failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Check if RedLocker is initialized * @returns true if initialized, false otherwise */ isReady() { return this.isInitialized && !!this.redlock && !!this.redis; } /** * Get current configuration * @returns Current RedLock configuration */ getConfig() { return { ...this.config }; } /** * Update configuration (requires reinitialization) * @param options - New RedLock options */ updateConfig(options) { if (options) { this.config = { ...this.config, ...options }; } // 清理初始化状态,强制重新初始化 this.isInitialized = false; this.initializationPromise = null; this.redlock = null; DefaultLogger.Debug('RedLocker configuration updated, will reinitialize on next use'); } /** * Close Redis connection and cleanup */ async close() { try { if (this.redis && this.redis.status === 'ready') { await this.redis.quit(); DefaultLogger.Debug('Redis connection closed'); } this.redis = null; this.redlock = null; this.isInitialized = false; } catch (error) { DefaultLogger.Error('Error closing RedLocker:', error); } } /** * Get container registration status * @returns Registration information */ getContainerInfo() { try { const instance = IOCContainer.get('RedLocker', 'COMPONENT'); return { registered: !!instance, identifier: 'RedLocker' }; } catch { return { registered: false, identifier: 'RedLocker' }; } } /** * Health check for RedLocker * @returns Health status */ async healthCheck() { try { await this.initialize(); const redisStatus = this.redis?.status || 'unknown'; const isReady = this.isReady(); return { status: isReady ? 'healthy' : 'unhealthy', details: { initialized: this.isInitialized, redisStatus, redlockReady: !!this.redlock, containerRegistered: this.getContainerInfo().registered } }; } catch (error) { return { status: 'unhealthy', details: { error: error instanceof Error ? error.message : 'Unknown error', initialized: this.isInitialized } }; } } } /* * @Description: * @Usage: * @Author: richen * @Date: 2024-01-16 19:53:14 * @LastEditTime: 2024-11-07 16:47:58 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * 返回一个 Promise,在指定时间后 reject * @param ms * @returns */ function timeoutPromise(ms) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { clearTimeout(timeoutId); reject(new Error('TIME_OUT_ERROR')); }, ms); }); } /* * @Description: Decorator preprocessing mechanism for koatty_schedule * @Usage: * @Author: richen * @Date: 2024-01-17 16:00:00 * @LastEditTime: 2024-01-17 16:00:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * Initiation schedule locker client. * * @param {RedLockOptions} options - RedLock 配置选项 * @param {Koatty} app - Koatty 应用实例 * @returns {Promise<void>} */ async function initRedLock(options, app) { if (!app || !Helper.isFunction(app.once)) { DefaultLogger.Warn(`RedLock initialization skipped: Koatty app not available or not initialized`); return; } app.once("appReady", async function () { try { if (Helper.isEmpty(options)) { throw Error(`Missing RedLock configuration. Please write a configuration item with the key name 'RedLock' in the db.ts file.`); } // 获取RedLocker实例,在首次使用时自动初始化 const redLocker = RedLocker.getInstance(options); await redLocker.initialize(); DefaultLogger.Info('RedLock initialized successfully'); } catch (error) { DefaultLogger.Error('Failed to initialize RedLock:', error); throw error; } }); } /** * Create redLocker Descriptor with improved error handling and type safety * @param descriptor - Property descriptor * @param name - Lock name * @param method - Method name * @param methodOptions - Method-level RedLock options * @returns Enhanced property descriptor */ function redLockerDescriptor(descriptor, name, method, methodOptions) { // 参数验证 if (!descriptor) { throw new Error('Property descriptor is required'); } if (!name || typeof name !== 'string') { throw new Error('Lock name must be a non-empty string'); } if (!method || typeof method !== 'string') { throw new Error('Method name must be a non-empty string'); } const { value, configurable, enumerable } = descriptor; // 验证原始函数 if (typeof value !== 'function') { throw new Error('Descriptor value must be a function'); } /** * Enhanced function wrapper with proper lock renewal and safety */ const valueFunction = async (self, initialLock, lockTime, timeout, props) => { let currentLock = initialLock; let remainingTime = timeout; const maxExtensions = 3; // 限制续期次数防止无限循环 let extensionCount = 0; try { while (remainingTime > 0 && extensionCount < maxExtensions) { try { // 执行业务方法,与超时竞争 const result = await Promise.race([ value.apply(self, props), timeoutPromise(remainingTime) ]); return result; // 成功执行,返回业务结果 } catch (error) { // 处理超时错误,尝试续期锁 if (error instanceof Error && error.message === 'TIME_OUT_ERROR') { extensionCount++; DefaultLogger.Debug(`Method ${method} execution timeout, attempting lock extension ${extensionCount}/${maxExtensions}`); try { // 续期锁,获得新的锁实例 currentLock = await currentLock.extend(lockTime); remainingTime = lockTime - 200; // 预留200ms用于锁操作 DefaultLogger.Debug(`Lock extended for method: ${method}, remaining time: ${remainingTime}ms`); // 继续循环,重新执行业务方法 continue; } catch (extendError) { DefaultLogger.Error(`Failed to extend lock for method: ${method}`, extendError); throw new Error(`Lock extension failed: ${extendError instanceof Error ? extendError.message : 'Unknown error'}`); } } else { // 非超时错误,直接抛出 throw error; } } } // 达到最大续期次数或剩余时间不足 throw new Error(`Method ${method} execution timeout after ${extensionCount} lock extensions`); } finally { // 确保锁被释放 try { await currentLock.release(); DefaultLogger.Debug(`Lock released for method: ${method}`); } catch (releaseError) { DefaultLogger.Warn(`Failed to release lock for method: ${method}`, releaseError); } } }; return { configurable, enumerable, writable: true, async value(...props) { try { const redlock = RedLocker.getInstance(); const lockOptions = getEffectiveRedLockOptions(methodOptions); // Acquire a lock. const lockTime = lockOptions.lockTimeOut || 10000; if (lockTime <= 200) { throw new Error("Lock timeout must be greater than 200ms to allow for proper execution"); } const lock = await redlock.acquire([method, name], lockTime); const timeout = lockTime - 200; DefaultLogger.Debug(`Lock acquired for method: ${method}, timeout: ${timeout}ms`); return await valueFunction(this, lock, lockTime, timeout, props); } catch (error) { DefaultLogger.Error(`RedLock operation failed for method: ${method}`, error); throw error; } }, }; } /** * Generate lock name for RedLock decorator */ function generateLockName(configName, methodName, target) { if (configName) { return configName; } try { const targetObj = target; const identifier = IOCContainer.getIdentifier(targetObj); if (identifier) { return `${identifier}_${methodName}`; } } catch { // Fallback if IOC container is not available } const targetWithConstructor = target; const className = targetWithConstructor.constructor?.name || 'Unknown'; return `${className}_${methodName}`; } /* * @Description: * @Usage: * @Author: richen * @Date: 2025-06-09 16:00:00 * @LastEditTime: 2025-06-09 16:00:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * Redis-based distributed lock decorator * * @export * @param {string} [name] - The locker name. If name is duplicated, lock sharing contention will result. * If not provided, a unique name will be auto-generated using method name + random suffix. * IMPORTANT: Auto-generated names are unique per method deployment and not predictable. * @param {RedLockMethodOptions} [options] - Lock configuration options for this method * * @returns {MethodDecorator} * @throws {Error} When decorator is used on wrong class type or invalid configuration * * @example * ```typescript * class UserService { * @RedLock('user_update_lock', { lockTimeOut: 5000, maxRetries: 2 }) * async updateUser(id: string, data: any) { * // This method will be protected by a distributed lock with predictable name * } * * @RedLock() // Auto-generated unique name like "deleteUser_abc123_xyz789" * async deleteUser(id: string) { * // This method will be protected by a distributed lock with auto-generated unique name * } * } * ``` */ function RedLock(lockName, options) { return (target, propertyKey, descriptor) => { const methodName = propertyKey.toString(); // 验证装饰器使用的类型(从原型对象获取类构造函数) const targetClass = target.constructor; const componentType = IOCContainer.getType(targetClass); if (componentType !== "SERVICE" && componentType !== "COMPONENT") { throw Error("@RedLock decorator can only be used on SERVICE or COMPONENT classes."); } // 验证方法名 if (!methodName || typeof methodName !== 'string') { throw Error("Method name is required for @RedLock decorator"); } // 验证方法描述符 if (!descriptor || typeof descriptor.value !== 'function') { throw Error("@RedLock decorator can only be applied to methods"); } // 生成锁名称:用户指定的 > 基于类名和方法名生成 const finalLockName = lockName || generateLockName(lockName, methodName, target); // 验证选项 if (options) { validateRedLockMethodOptions(options); } // 保存类到IOC容器 IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name); try { // 直接在装饰器中包装方法,而不是延迟处理 const enhancedDescriptor = redLockerDescriptor(descriptor, finalLockName, methodName, options); return enhancedDescriptor; } catch (error) { throw new Error(`Failed to apply RedLock to ${methodName}: ${error.message}`); } }; } /* * @Description: * @Usage: * @Author: richen * @Date: 2025-06-09 16:00:00 * @LastEditTime: 2025-06-09 16:00:00 * @License: BSD (3-Clause) * @Copyright (c): <richenlin(at)gmail.com> */ /** * Schedule task decorator with optimized preprocessing * * @export * @param {string} cron - Cron expression for task scheduling * @param {string} [timezone='Asia/Beijing'] - Timezone for the schedule * * Cron expression format: * * Seconds: 0-59 * * Minutes: 0-59 * * Hours: 0-23 * * Day of Month: 1-31 * * Months: 1-12 (Jan-Dec) * * Day of Week: 1-7 (Sun-Sat) * * @returns {MethodDecorator} * @throws {Error} When cron expression is invalid or decorator is used on wrong class type */ function Scheduled(cron, timezone = 'Asia/Beijing') { // 参数验证 if (Helper.isEmpty(cron)) { throw Error("Cron expression is required and cannot be empty"); } // 验证cron表达式格式 try { validateCronExpression(cron); } catch (error) { throw Error(`Invalid cron expression: ${error.message}`); } // 验证时区 if (timezone && typeof timezone !== 'string') { throw Error("Timezone must be a string"); } return (target, propertyKey, descriptor) => { // 验证装饰器使用的类型(从原型对象获取类构造函数) const targetClass = target.constructor; const componentType = IOCContainer.getType(targetClass); if (componentType !== "SERVICE" && componentType !== "COMPONENT") { throw Error("@Scheduled decorator can only be used on SERVICE or COMPONENT classes."); } // 验证方法名 const methodName = propertyKey.toString(); if (!methodName || typeof methodName !== 'string') { throw Error("Method name is required for @Scheduled decorator"); } // 验证方法描述符 if (!descriptor || typeof descriptor.value !== 'function') { throw Error("@Scheduled decorator can only be applied to methods"); } // 保存类到IOC容器 IOCContainer.saveClass("COMPONENT", targetClass, targetClass.name); // 保存调度元数据到 IOC 容器 IOCContainer.attachClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, { method: methodName, cron, timezone // 保存确定的时区值 }, target, methodName); }; } /** * @ author: richen * @ copyright: Copyright (c) - <richenlin(at)gmail.com> * @ license: MIT * @ version: 2020-07-06 10:29:20 */ /** * 初始化调度任务系统 * 在appReady时触发批量注入调度任务,确保所有初始化工作完成 * * @param {Koatty} app - Koatty 应用实例 * @param {any} options - 调度任务配置 */ async function initSchedule(options, app) { if (!app || !Helper.isFunction(app.once)) { DefaultLogger.Warn(`Schedule initialization skipped: Koatty app not available or not initialized`); return; } app.once("appReady", async function () { try { await injectSchedule(options); DefaultLogger.Info('Schedule system initialized successfully'); } catch (error) { DefaultLogger.Error('Failed to initialize Schedule system:', error); throw error; } }); } /** * Inject schedule job with enhanced error handling and validation * * @param {unknown} target - Target class * @param {string} method - Method name * @param {string} cron - Cron expression * @param {string} [timezone] - Timezone */ /** * 批量注入调度任务 - 从IOC容器读取类元数据并创建所有CronJob */ async function injectSchedule(options) { try { DefaultLogger.Debug('Starting batch schedule injection...'); const componentList = IOCContainer.listClass("COMPONENT"); for (const component of componentList) { const classMetadata = IOCContainer.getClassMetadata(COMPONENT_SCHEDULED, DecoratorType.SCHEDULED, component.target); if (!classMetadata) { continue; } let scheduledCount = 0; for (const [className, metadata] of classMetadata) { try { const instance = IOCContainer.get(className); if (!instance) { continue; } // 查找所有调度方法的元数据 for (const [key, value] of Object.entries(metadata)) { if (key.startsWith('SCHEDULED')) { const scheduleData = value; const targetMethod = instance[scheduleData.method]; if (!Helper.isFunction(targetMethod)) { DefaultLogger.Warn(`Schedule injection skipped: method ${scheduleData.method} is not a function in ${className}`); continue; } const taskName = `${className}_${scheduleData.method}`; const tz = getEffectiveTimezone(options, scheduleData.timezone); new CronJob(scheduleData.cron, () => { DefaultLogger.Debug(`The schedule job ${taskName} started.`); Promise.resolve(targetMethod.call(instance)) .then(() => { DefaultLogger.Debug(`The schedule job ${taskName} completed.`); }) .catch((error) => { DefaultLogger.Error(`The schedule job ${taskName} failed:`, error); }); }, null, // onComplete true, // start tz // timeZone ); scheduledCount++; DefaultLogger.Debug(`Schedule job ${taskName} registered with cron: ${scheduleData.cron}`); } } } catch (error) { DefaultLogger.Error(`Failed to process class ${className}:`, error); } } DefaultLogger.Info(`Batch schedule injection completed. ${scheduledCount} jobs registered.`); } } catch (error) { DefaultLogger.Error('Failed to inject schedules:', error); } } /** * @ author: richen * @ copyright: Copyright (c) - <richenlin(at)gmail.com> * @ license: MIT * @ version: 2020-07-06 10:30:11 */ /** * @deprecated Use RedLock instead. This will be removed in v3.0.0 */ const SchedulerLock = RedLock; /** * defaultOptions */ const defaultOptions = { timezone: "Asia/Beijing", lockTimeOut: 10000, clockDriftFactor: 0.01, maxRetries: 3, retryDelayMs: 200, redisConfig: { host: "localhost", port: 6379, password: "", db: 0, keyPrefix: "redlock:" } }; /** * @param options - The options for the scheduled job * @param app - The Koatty application instance */ async function KoattyScheduled(options, app) { options = { ...defaultOptions, ...options }; // 初始化RedLock(appReady时触发,确保所有依赖就绪) await initRedLock(options, app); // 初始化调度任务系统(appReady时触发,确保所有组件都已初始化) await initSchedule(options, app); } export { KoattyScheduled, RedLock, Scheduled, SchedulerLock };