mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-20 05:30:06 +08:00
fix: #1286
This commit is contained in:
parent
cbacc89907
commit
42abc2b9cb
@ -9,41 +9,50 @@ export interface ResourceConfig<T extends any[], R> {
|
|||||||
healthCheckInterval?: number;
|
healthCheckInterval?: number;
|
||||||
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
|
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
|
||||||
maxHealthCheckFailures?: number;
|
maxHealthCheckFailures?: number;
|
||||||
/** 资源名称(用于日志) */
|
|
||||||
name?: string;
|
|
||||||
/** 测试参数(用于健康检查) */
|
|
||||||
testArgs?: T;
|
|
||||||
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
|
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
|
||||||
healthCheckFn?: (...args: T) => Promise<boolean>;
|
healthCheckFn?: (...args: T) => Promise<boolean>;
|
||||||
|
/** 测试参数(用于健康检查) */
|
||||||
|
testArgs?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceState<T extends any[], R> {
|
interface ResourceTypeState {
|
||||||
config: ResourceConfig<T, R>;
|
/** 资源配置 */
|
||||||
|
config: {
|
||||||
|
resourceFn: (...args: any[]) => Promise<any>;
|
||||||
|
healthCheckFn?: (...args: any[]) => Promise<boolean>;
|
||||||
|
disableTime: number;
|
||||||
|
maxRetries: number;
|
||||||
|
healthCheckInterval: number;
|
||||||
|
maxHealthCheckFailures: number;
|
||||||
|
testArgs?: any[];
|
||||||
|
};
|
||||||
|
/** 是否启用 */
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
/** 禁用截止时间 */
|
||||||
disableUntil: number;
|
disableUntil: number;
|
||||||
|
/** 当前重试次数 */
|
||||||
currentRetries: number;
|
currentRetries: number;
|
||||||
|
/** 健康检查失败次数 */
|
||||||
healthCheckFailureCount: number;
|
healthCheckFailureCount: number;
|
||||||
|
/** 是否永久禁用 */
|
||||||
isPermanentlyDisabled: boolean;
|
isPermanentlyDisabled: boolean;
|
||||||
lastError?: Error;
|
/** 上次健康检查时间 */
|
||||||
lastHealthCheckTime: number;
|
lastHealthCheckTime: number;
|
||||||
registrationKey: string;
|
/** 成功次数统计 */
|
||||||
|
successCount: number;
|
||||||
|
/** 失败次数统计 */
|
||||||
|
failureCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResourceManager {
|
export class ResourceManager {
|
||||||
private resources = new Map<string, ResourceState<any, any>>();
|
private resourceTypes = new Map<string, ResourceTypeState>();
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
private healthCheckTimer?: NodeJS.Timeout;
|
|
||||||
private readonly HEALTH_CHECK_TASK_INTERVAL = 5000; // 5秒执行一次健康检查任务
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.startHealthCheckTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册资源(注册即调用,重复注册只实际注册一次)
|
* 调用资源(自动注册或复用已有配置)
|
||||||
*/
|
*/
|
||||||
async register<T extends any[], R>(
|
async callResource<T extends any[], R>(
|
||||||
key: string,
|
type: string,
|
||||||
config: ResourceConfig<T, R>,
|
config: ResourceConfig<T, R>,
|
||||||
...args: T
|
...args: T
|
||||||
): Promise<R> {
|
): Promise<R> {
|
||||||
@ -51,81 +60,64 @@ export class ResourceManager {
|
|||||||
throw new Error('ResourceManager has been destroyed');
|
throw new Error('ResourceManager has been destroyed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const registrationKey = this.generateRegistrationKey(key, config);
|
// 获取或创建资源类型状态
|
||||||
|
let state = this.resourceTypes.get(type);
|
||||||
|
|
||||||
// 检查是否已经注册
|
|
||||||
if (this.resources.has(key)) {
|
|
||||||
const existingState = this.resources.get(key)!;
|
|
||||||
|
|
||||||
// 如果是相同的配置,直接调用
|
|
||||||
if (existingState.registrationKey === registrationKey) {
|
|
||||||
return this.callResource<T, R>(key, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置不同,清理旧的并重新注册
|
|
||||||
this.unregister(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的资源状态
|
|
||||||
const state: ResourceState<T, R> = {
|
|
||||||
config: {
|
|
||||||
disableTime: 30000,
|
|
||||||
maxRetries: 3,
|
|
||||||
healthCheckInterval: 60000,
|
|
||||||
maxHealthCheckFailures: 5,
|
|
||||||
name: key,
|
|
||||||
...config
|
|
||||||
},
|
|
||||||
isEnabled: true,
|
|
||||||
disableUntil: 0,
|
|
||||||
currentRetries: 0,
|
|
||||||
healthCheckFailureCount: 0,
|
|
||||||
isPermanentlyDisabled: false,
|
|
||||||
lastHealthCheckTime: 0,
|
|
||||||
registrationKey
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resources.set(key, state);
|
|
||||||
|
|
||||||
// 注册即调用
|
|
||||||
return await this.callResource<T, R>(key, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用资源
|
|
||||||
*/
|
|
||||||
async callResource<T extends any[], R>(key: string, ...args: T): Promise<R> {
|
|
||||||
const state = this.resources.get(key) as ResourceState<T, R> | undefined;
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
throw new Error(`Resource ${key} not registered`);
|
// 首次注册
|
||||||
|
state = {
|
||||||
|
config: {
|
||||||
|
resourceFn: config.resourceFn as (...args: any[]) => Promise<any>,
|
||||||
|
healthCheckFn: config.healthCheckFn as ((...args: any[]) => Promise<boolean>) | undefined,
|
||||||
|
disableTime: config.disableTime ?? 30000,
|
||||||
|
maxRetries: config.maxRetries ?? 3,
|
||||||
|
healthCheckInterval: config.healthCheckInterval ?? 60000,
|
||||||
|
maxHealthCheckFailures: config.maxHealthCheckFailures ?? 20,
|
||||||
|
testArgs: config.testArgs as any[] | undefined,
|
||||||
|
},
|
||||||
|
isEnabled: true,
|
||||||
|
disableUntil: 0,
|
||||||
|
currentRetries: 0,
|
||||||
|
healthCheckFailureCount: 0,
|
||||||
|
isPermanentlyDisabled: false,
|
||||||
|
lastHealthCheckTime: 0,
|
||||||
|
successCount: 0,
|
||||||
|
failureCount: 0,
|
||||||
|
};
|
||||||
|
this.resourceTypes.set(type, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在调用前检查是否需要进行健康检查
|
||||||
|
await this.checkAndPerformHealthCheck(state);
|
||||||
|
|
||||||
|
// 检查资源状态
|
||||||
if (state.isPermanentlyDisabled) {
|
if (state.isPermanentlyDisabled) {
|
||||||
throw new Error(`Resource ${key} is permanently disabled due to repeated health check failures`);
|
throw new Error(`Resource type '${type}' is permanently disabled (success: ${state.successCount}, failure: ${state.failureCount})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isResourceAvailable(key)) {
|
if (!this.isResourceAvailable(type)) {
|
||||||
const disableUntilDate = new Date(state.disableUntil).toISOString();
|
const disableUntilDate = new Date(state.disableUntil).toISOString();
|
||||||
throw new Error(`Resource ${key} is currently disabled until ${disableUntilDate}`);
|
throw new Error(`Resource type '${type}' is currently disabled until ${disableUntilDate} (success: ${state.successCount}, failure: ${state.failureCount})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用资源
|
||||||
try {
|
try {
|
||||||
const result = await state.config.resourceFn(...args);
|
const result = await config.resourceFn(...args);
|
||||||
this.onResourceSuccess(state);
|
this.onResourceSuccess(state);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.onResourceFailure(state, error as Error);
|
this.onResourceFailure(state);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查资源是否可用
|
* 检查资源类型是否可用
|
||||||
*/
|
*/
|
||||||
isResourceAvailable(key: string): boolean {
|
isResourceAvailable(type: string): boolean {
|
||||||
const state = this.resources.get(key);
|
const state = this.resourceTypes.get(type);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return false;
|
return true; // 未注册的资源类型视为可用
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isPermanentlyDisabled || !state.isEnabled) {
|
if (state.isPermanentlyDisabled || !state.isEnabled) {
|
||||||
@ -136,128 +128,97 @@ export class ResourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销资源
|
* 获取资源类型统计信息
|
||||||
*/
|
*/
|
||||||
unregister(key: string): boolean {
|
getResourceStats(type: string): { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean } | null {
|
||||||
return this.resources.delete(key);
|
const state = this.resourceTypes.get(type);
|
||||||
|
if (!state) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
successCount: state.successCount,
|
||||||
|
failureCount: state.failureCount,
|
||||||
|
isEnabled: state.isEnabled,
|
||||||
|
isPermanentlyDisabled: state.isPermanentlyDisabled,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁管理器,清理所有资源
|
* 获取所有资源类型统计
|
||||||
|
*/
|
||||||
|
getAllResourceStats(): Map<string, { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean }> {
|
||||||
|
const stats = new Map();
|
||||||
|
for (const [type, state] of this.resourceTypes) {
|
||||||
|
stats.set(type, {
|
||||||
|
successCount: state.successCount,
|
||||||
|
failureCount: state.failureCount,
|
||||||
|
isEnabled: state.isEnabled,
|
||||||
|
isPermanentlyDisabled: state.isPermanentlyDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销资源类型
|
||||||
|
*/
|
||||||
|
unregister(type: string): boolean {
|
||||||
|
return this.resourceTypes.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁管理器
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
if (this.destroyed) {
|
if (this.destroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stopHealthCheckTask();
|
this.resourceTypes.clear();
|
||||||
this.resources.clear();
|
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateRegistrationKey<T extends any[], R>(key: string, config: ResourceConfig<T, R>): string {
|
/**
|
||||||
const configStr = JSON.stringify({
|
* 检查并执行健康检查(如果需要)
|
||||||
name: config.name,
|
*/
|
||||||
disableTime: config.disableTime,
|
private async checkAndPerformHealthCheck(state: ResourceTypeState): Promise<void> {
|
||||||
maxRetries: config.maxRetries,
|
// 如果资源可用或已永久禁用,无需健康检查
|
||||||
healthCheckInterval: config.healthCheckInterval,
|
if (state.isEnabled && Date.now() >= state.disableUntil) {
|
||||||
maxHealthCheckFailures: config.maxHealthCheckFailures,
|
|
||||||
functionStr: config.resourceFn.toString(),
|
|
||||||
healthCheckFnStr: config.healthCheckFn?.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${key}_${this.simpleHash(configStr)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private simpleHash(str: string): string {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return Math.abs(hash).toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResourceSuccess<T extends any[], R>(state: ResourceState<T, R>): void {
|
|
||||||
state.currentRetries = 0;
|
|
||||||
state.disableUntil = 0;
|
|
||||||
state.healthCheckFailureCount = 0;
|
|
||||||
state.lastError = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResourceFailure<T extends any[], R>(state: ResourceState<T, R>, error: Error): void {
|
|
||||||
state.currentRetries++;
|
|
||||||
state.lastError = error;
|
|
||||||
|
|
||||||
// 如果重试次数达到上限,禁用资源
|
|
||||||
if (state.currentRetries >= state.config.maxRetries!) {
|
|
||||||
state.disableUntil = Date.now() + state.config.disableTime!;
|
|
||||||
state.currentRetries = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startHealthCheckTask(): void {
|
|
||||||
if (this.healthCheckTimer) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.healthCheckTimer = setInterval(() => {
|
if (state.isPermanentlyDisabled) {
|
||||||
this.runHealthCheckTask();
|
|
||||||
}, this.HEALTH_CHECK_TASK_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private stopHealthCheckTask(): void {
|
|
||||||
if (this.healthCheckTimer) {
|
|
||||||
clearInterval(this.healthCheckTimer);
|
|
||||||
this.healthCheckTimer = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runHealthCheckTask(): Promise<void> {
|
|
||||||
if (this.destroyed) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const [key, state] of this.resources) {
|
// 检查是否还在禁用期内
|
||||||
// 跳过永久禁用或可用的资源
|
if (now < state.disableUntil) {
|
||||||
if (state.isPermanentlyDisabled || this.isResourceAvailable(key)) {
|
return;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过还在禁用期内的资源
|
|
||||||
if (now < state.disableUntil) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要进行健康检查(根据间隔时间)
|
|
||||||
const lastHealthCheck = state.lastHealthCheckTime || 0;
|
|
||||||
const healthCheckInterval = state.config.healthCheckInterval!;
|
|
||||||
|
|
||||||
if (now - lastHealthCheck < healthCheckInterval) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行健康检查
|
|
||||||
await this.performHealthCheck(state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否需要进行健康检查(根据间隔时间)
|
||||||
|
if (now - state.lastHealthCheckTime < state.config.healthCheckInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行健康检查
|
||||||
|
await this.performHealthCheck(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async performHealthCheck<T extends any[], R>(state: ResourceState<T, R>): Promise<void> {
|
private async performHealthCheck(state: ResourceTypeState): Promise<void> {
|
||||||
state.lastHealthCheckTime = Date.now();
|
state.lastHealthCheckTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let healthCheckResult: boolean;
|
let healthCheckResult: boolean;
|
||||||
|
|
||||||
// 如果有专门的健康检查函数,使用它
|
|
||||||
if (state.config.healthCheckFn) {
|
if (state.config.healthCheckFn) {
|
||||||
const testArgs = state.config.testArgs || [] as unknown as T;
|
const testArgs = state.config.testArgs || [];
|
||||||
healthCheckResult = await state.config.healthCheckFn(...testArgs);
|
healthCheckResult = await state.config.healthCheckFn(...testArgs);
|
||||||
} else {
|
} else {
|
||||||
// 否则使用原始函数进行检查
|
const testArgs = state.config.testArgs || [];
|
||||||
const testArgs = state.config.testArgs || [] as unknown as T;
|
|
||||||
await state.config.resourceFn(...testArgs);
|
await state.config.resourceFn(...testArgs);
|
||||||
healthCheckResult = true;
|
healthCheckResult = true;
|
||||||
}
|
}
|
||||||
@ -268,26 +229,42 @@ export class ResourceManager {
|
|||||||
state.disableUntil = 0;
|
state.disableUntil = 0;
|
||||||
state.currentRetries = 0;
|
state.currentRetries = 0;
|
||||||
state.healthCheckFailureCount = 0;
|
state.healthCheckFailureCount = 0;
|
||||||
state.lastError = undefined;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Health check function returned false');
|
throw new Error('Health check function returned false');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// 健康检查失败,增加失败计数
|
// 健康检查失败,增加失败计数
|
||||||
state.healthCheckFailureCount++;
|
state.healthCheckFailureCount++;
|
||||||
state.lastError = error as Error;
|
|
||||||
|
|
||||||
// 检查是否达到最大健康检查失败次数
|
// 检查是否达到最大健康检查失败次数
|
||||||
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures!) {
|
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures) {
|
||||||
// 永久禁用资源
|
// 永久禁用资源
|
||||||
state.isPermanentlyDisabled = true;
|
state.isPermanentlyDisabled = true;
|
||||||
state.disableUntil = 0;
|
state.disableUntil = 0;
|
||||||
} else {
|
} else {
|
||||||
// 继续禁用一段时间
|
// 继续禁用一段时间
|
||||||
state.disableUntil = Date.now() + state.config.disableTime!;
|
state.disableUntil = Date.now() + state.config.disableTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onResourceSuccess(state: ResourceTypeState): void {
|
||||||
|
state.currentRetries = 0;
|
||||||
|
state.disableUntil = 0;
|
||||||
|
state.healthCheckFailureCount = 0;
|
||||||
|
state.successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResourceFailure(state: ResourceTypeState): void {
|
||||||
|
state.currentRetries++;
|
||||||
|
state.failureCount++;
|
||||||
|
|
||||||
|
// 如果重试次数达到上限,禁用资源
|
||||||
|
if (state.currentRetries >= state.config.maxRetries) {
|
||||||
|
state.disableUntil = Date.now() + state.config.disableTime;
|
||||||
|
state.currentRetries = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建全局实例
|
// 创建全局实例
|
||||||
@ -295,34 +272,9 @@ export const resourceManager = new ResourceManager();
|
|||||||
|
|
||||||
// 便捷函数
|
// 便捷函数
|
||||||
export async function registerResource<T extends any[], R>(
|
export async function registerResource<T extends any[], R>(
|
||||||
key: string,
|
type: string,
|
||||||
config: ResourceConfig<T, R>,
|
config: ResourceConfig<T, R>,
|
||||||
...args: T
|
...args: T
|
||||||
): Promise<R> {
|
): Promise<R> {
|
||||||
return resourceManager.register(key, config, ...args);
|
return resourceManager.callResource(type, config, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用示例:
|
|
||||||
/*
|
|
||||||
await registerResource(
|
|
||||||
'api-with-health-check',
|
|
||||||
{
|
|
||||||
resourceFn: async (id: string) => {
|
|
||||||
const response = await fetch(`https://api.example.com/data/${id}`);
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
healthCheckFn: async (id: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.example.com/health`);
|
|
||||||
return response.ok;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
testArgs: ['health-check-id'],
|
|
||||||
healthCheckInterval: 30000,
|
|
||||||
maxHealthCheckFailures: 3
|
|
||||||
},
|
|
||||||
'user123'
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
Loading…
Reference in New Issue
Block a user