linxus fs fixes
This commit is contained in:
parent
ac23b70146
commit
0b7846fe67
292 changed files with 41947 additions and 41947 deletions
526
libs/cache/src/providers/redis-cache.ts
vendored
526
libs/cache/src/providers/redis-cache.ts
vendored
|
|
@ -1,263 +1,263 @@
|
|||
import Redis from 'ioredis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { dragonflyConfig } from '@stock-bot/config';
|
||||
import { CacheProvider, CacheOptions, CacheStats } from '../types';
|
||||
|
||||
/**
|
||||
* Redis-based cache provider with comprehensive error handling and metrics
|
||||
*/
|
||||
export class RedisCache implements CacheProvider {
|
||||
private redis: Redis;
|
||||
private logger = getLogger('redis-cache');
|
||||
private defaultTTL: number;
|
||||
private keyPrefix: string;
|
||||
private enableMetrics: boolean;
|
||||
private isConnected = false;
|
||||
private startTime = Date.now();
|
||||
|
||||
private stats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
constructor(options: CacheOptions = {}) {
|
||||
this.defaultTTL = options.ttl ?? 3600; // 1 hour default
|
||||
this.keyPrefix = options.keyPrefix ?? 'cache:';
|
||||
this.enableMetrics = options.enableMetrics ?? true;
|
||||
|
||||
const redisConfig = {
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
password: dragonflyConfig.DRAGONFLY_PASSWORD || undefined,
|
||||
username: dragonflyConfig.DRAGONFLY_USERNAME || undefined,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE,
|
||||
maxRetriesPerRequest: dragonflyConfig.DRAGONFLY_MAX_RETRIES,
|
||||
retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY,
|
||||
connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT,
|
||||
commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT,
|
||||
keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE ? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000 : 0,
|
||||
...(dragonflyConfig.DRAGONFLY_TLS && {
|
||||
tls: {
|
||||
cert: dragonflyConfig.DRAGONFLY_TLS_CERT_FILE || undefined,
|
||||
key: dragonflyConfig.DRAGONFLY_TLS_KEY_FILE || undefined,
|
||||
ca: dragonflyConfig.DRAGONFLY_TLS_CA_FILE || undefined,
|
||||
rejectUnauthorized: !dragonflyConfig.DRAGONFLY_TLS_SKIP_VERIFY,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
this.redis = new Redis(redisConfig);
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.redis.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
this.logger.info('Redis cache connected', {
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE
|
||||
});
|
||||
});
|
||||
|
||||
this.redis.on('ready', () => {
|
||||
this.logger.info('Redis cache ready for commands');
|
||||
});
|
||||
|
||||
this.redis.on('error', (error) => {
|
||||
this.isConnected = false;
|
||||
this.stats.errors++;
|
||||
this.logger.error('Redis cache connection error', { error: error.message });
|
||||
});
|
||||
|
||||
this.redis.on('close', () => {
|
||||
this.isConnected = false;
|
||||
this.logger.warn('Redis cache connection closed');
|
||||
});
|
||||
|
||||
this.redis.on('reconnecting', () => {
|
||||
this.logger.info('Redis cache reconnecting...');
|
||||
});
|
||||
}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return `${this.keyPrefix}${key}`;
|
||||
}
|
||||
|
||||
private updateStats(hit: boolean, error = false): void {
|
||||
if (!this.enableMetrics) return;
|
||||
|
||||
if (error) {
|
||||
this.stats.errors++;
|
||||
} else if (hit) {
|
||||
this.stats.hits++;
|
||||
} else {
|
||||
this.stats.misses++;
|
||||
}
|
||||
|
||||
this.stats.total = this.stats.hits + this.stats.misses;
|
||||
this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0;
|
||||
this.stats.uptime = Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
private async safeExecute<T>(
|
||||
operation: () => Promise<T>,
|
||||
fallback: T,
|
||||
operationName: string
|
||||
): Promise<T> {
|
||||
if (!this.isConnected) {
|
||||
this.logger.warn(`Redis not connected for ${operationName}, using fallback`);
|
||||
this.updateStats(false, true);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
this.logger.error(`Redis ${operationName} failed`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
this.updateStats(false, true);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const value = await this.redis.get(fullKey);
|
||||
|
||||
if (value === null) {
|
||||
this.updateStats(false);
|
||||
this.logger.debug('Cache miss', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.updateStats(true);
|
||||
this.logger.debug('Cache hit', { key, hitRate: this.stats.hitRate });
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
// Return as-is if not valid JSON
|
||||
return value as unknown as T;
|
||||
}
|
||||
},
|
||||
null,
|
||||
'get'
|
||||
);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
const expiry = ttl ?? this.defaultTTL;
|
||||
|
||||
await this.redis.setex(fullKey, expiry, serialized);
|
||||
this.logger.debug('Cache set', { key, ttl: expiry });
|
||||
},
|
||||
undefined,
|
||||
'set'
|
||||
);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
await this.redis.del(fullKey);
|
||||
this.logger.debug('Cache delete', { key });
|
||||
},
|
||||
undefined,
|
||||
'del'
|
||||
);
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const result = await this.redis.exists(fullKey);
|
||||
return result === 1;
|
||||
},
|
||||
false,
|
||||
'exists'
|
||||
);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const pattern = `${this.keyPrefix}*`;
|
||||
const keys = await this.redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
this.logger.info('Cache cleared', { keysDeleted: keys.length });
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
'clear'
|
||||
);
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
try {
|
||||
const pong = await this.redis.ping();
|
||||
return pong === 'PONG' && this.isConnected;
|
||||
} catch (error) {
|
||||
this.logger.error('Redis health check failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStats(): CacheStats {
|
||||
return {
|
||||
...this.stats,
|
||||
uptime: Date.now() - this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trading-specific convenience methods
|
||||
*/
|
||||
async cacheMarketData(symbol: string, timeframe: string, data: any[], ttl = 300): Promise<void> {
|
||||
const key = `market:${symbol}:${timeframe}`;
|
||||
await this.set(key, data, ttl);
|
||||
}
|
||||
|
||||
async getMarketData<T>(symbol: string, timeframe: string): Promise<T | null> {
|
||||
const key = `market:${symbol}:${timeframe}`;
|
||||
return this.get<T>(key);
|
||||
}
|
||||
|
||||
async cacheIndicator(
|
||||
symbol: string,
|
||||
indicator: string,
|
||||
period: number,
|
||||
data: number[],
|
||||
ttl = 600
|
||||
): Promise<void> {
|
||||
const key = `indicator:${symbol}:${indicator}:${period}`;
|
||||
await this.set(key, data, ttl);
|
||||
}
|
||||
|
||||
async getIndicator(symbol: string, indicator: string, period: number): Promise<number[] | null> {
|
||||
const key = `indicator:${symbol}:${indicator}:${period}`;
|
||||
return this.get<number[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Redis connection
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.redis.quit();
|
||||
this.logger.info('Redis cache disconnected');
|
||||
}
|
||||
}
|
||||
import Redis from 'ioredis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { dragonflyConfig } from '@stock-bot/config';
|
||||
import { CacheProvider, CacheOptions, CacheStats } from '../types';
|
||||
|
||||
/**
|
||||
* Redis-based cache provider with comprehensive error handling and metrics
|
||||
*/
|
||||
export class RedisCache implements CacheProvider {
|
||||
private redis: Redis;
|
||||
private logger = getLogger('redis-cache');
|
||||
private defaultTTL: number;
|
||||
private keyPrefix: string;
|
||||
private enableMetrics: boolean;
|
||||
private isConnected = false;
|
||||
private startTime = Date.now();
|
||||
|
||||
private stats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
constructor(options: CacheOptions = {}) {
|
||||
this.defaultTTL = options.ttl ?? 3600; // 1 hour default
|
||||
this.keyPrefix = options.keyPrefix ?? 'cache:';
|
||||
this.enableMetrics = options.enableMetrics ?? true;
|
||||
|
||||
const redisConfig = {
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
password: dragonflyConfig.DRAGONFLY_PASSWORD || undefined,
|
||||
username: dragonflyConfig.DRAGONFLY_USERNAME || undefined,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE,
|
||||
maxRetriesPerRequest: dragonflyConfig.DRAGONFLY_MAX_RETRIES,
|
||||
retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY,
|
||||
connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT,
|
||||
commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT,
|
||||
keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE ? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000 : 0,
|
||||
...(dragonflyConfig.DRAGONFLY_TLS && {
|
||||
tls: {
|
||||
cert: dragonflyConfig.DRAGONFLY_TLS_CERT_FILE || undefined,
|
||||
key: dragonflyConfig.DRAGONFLY_TLS_KEY_FILE || undefined,
|
||||
ca: dragonflyConfig.DRAGONFLY_TLS_CA_FILE || undefined,
|
||||
rejectUnauthorized: !dragonflyConfig.DRAGONFLY_TLS_SKIP_VERIFY,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
this.redis = new Redis(redisConfig);
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.redis.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
this.logger.info('Redis cache connected', {
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE
|
||||
});
|
||||
});
|
||||
|
||||
this.redis.on('ready', () => {
|
||||
this.logger.info('Redis cache ready for commands');
|
||||
});
|
||||
|
||||
this.redis.on('error', (error) => {
|
||||
this.isConnected = false;
|
||||
this.stats.errors++;
|
||||
this.logger.error('Redis cache connection error', { error: error.message });
|
||||
});
|
||||
|
||||
this.redis.on('close', () => {
|
||||
this.isConnected = false;
|
||||
this.logger.warn('Redis cache connection closed');
|
||||
});
|
||||
|
||||
this.redis.on('reconnecting', () => {
|
||||
this.logger.info('Redis cache reconnecting...');
|
||||
});
|
||||
}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return `${this.keyPrefix}${key}`;
|
||||
}
|
||||
|
||||
private updateStats(hit: boolean, error = false): void {
|
||||
if (!this.enableMetrics) return;
|
||||
|
||||
if (error) {
|
||||
this.stats.errors++;
|
||||
} else if (hit) {
|
||||
this.stats.hits++;
|
||||
} else {
|
||||
this.stats.misses++;
|
||||
}
|
||||
|
||||
this.stats.total = this.stats.hits + this.stats.misses;
|
||||
this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0;
|
||||
this.stats.uptime = Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
private async safeExecute<T>(
|
||||
operation: () => Promise<T>,
|
||||
fallback: T,
|
||||
operationName: string
|
||||
): Promise<T> {
|
||||
if (!this.isConnected) {
|
||||
this.logger.warn(`Redis not connected for ${operationName}, using fallback`);
|
||||
this.updateStats(false, true);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
this.logger.error(`Redis ${operationName} failed`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
this.updateStats(false, true);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const value = await this.redis.get(fullKey);
|
||||
|
||||
if (value === null) {
|
||||
this.updateStats(false);
|
||||
this.logger.debug('Cache miss', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
this.updateStats(true);
|
||||
this.logger.debug('Cache hit', { key, hitRate: this.stats.hitRate });
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
// Return as-is if not valid JSON
|
||||
return value as unknown as T;
|
||||
}
|
||||
},
|
||||
null,
|
||||
'get'
|
||||
);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
const expiry = ttl ?? this.defaultTTL;
|
||||
|
||||
await this.redis.setex(fullKey, expiry, serialized);
|
||||
this.logger.debug('Cache set', { key, ttl: expiry });
|
||||
},
|
||||
undefined,
|
||||
'set'
|
||||
);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
await this.redis.del(fullKey);
|
||||
this.logger.debug('Cache delete', { key });
|
||||
},
|
||||
undefined,
|
||||
'del'
|
||||
);
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return this.safeExecute(
|
||||
async () => {
|
||||
const fullKey = this.getKey(key);
|
||||
const result = await this.redis.exists(fullKey);
|
||||
return result === 1;
|
||||
},
|
||||
false,
|
||||
'exists'
|
||||
);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.safeExecute(
|
||||
async () => {
|
||||
const pattern = `${this.keyPrefix}*`;
|
||||
const keys = await this.redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
this.logger.info('Cache cleared', { keysDeleted: keys.length });
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
'clear'
|
||||
);
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
try {
|
||||
const pong = await this.redis.ping();
|
||||
return pong === 'PONG' && this.isConnected;
|
||||
} catch (error) {
|
||||
this.logger.error('Redis health check failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStats(): CacheStats {
|
||||
return {
|
||||
...this.stats,
|
||||
uptime: Date.now() - this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trading-specific convenience methods
|
||||
*/
|
||||
async cacheMarketData(symbol: string, timeframe: string, data: any[], ttl = 300): Promise<void> {
|
||||
const key = `market:${symbol}:${timeframe}`;
|
||||
await this.set(key, data, ttl);
|
||||
}
|
||||
|
||||
async getMarketData<T>(symbol: string, timeframe: string): Promise<T | null> {
|
||||
const key = `market:${symbol}:${timeframe}`;
|
||||
return this.get<T>(key);
|
||||
}
|
||||
|
||||
async cacheIndicator(
|
||||
symbol: string,
|
||||
indicator: string,
|
||||
period: number,
|
||||
data: number[],
|
||||
ttl = 600
|
||||
): Promise<void> {
|
||||
const key = `indicator:${symbol}:${indicator}:${period}`;
|
||||
await this.set(key, data, ttl);
|
||||
}
|
||||
|
||||
async getIndicator(symbol: string, indicator: string, period: number): Promise<number[] | null> {
|
||||
const key = `indicator:${symbol}:${indicator}:${period}`;
|
||||
return this.get<number[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Redis connection
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.redis.quit();
|
||||
this.logger.info('Redis cache disconnected');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue