linxus fs fixes
This commit is contained in:
parent
ac23b70146
commit
0b7846fe67
292 changed files with 41947 additions and 41947 deletions
522
libs/cache/src/providers/hybrid-cache.ts
vendored
522
libs/cache/src/providers/hybrid-cache.ts
vendored
|
|
@ -1,261 +1,261 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { CacheProvider, CacheOptions, CacheStats } from '../types';
|
||||
import { RedisCache } from './redis-cache';
|
||||
import { MemoryCache } from './memory-cache';
|
||||
|
||||
/**
|
||||
* Hybrid cache provider that uses memory as L1 cache and Redis as L2 cache
|
||||
* Provides the best of both worlds: fast memory access and persistent Redis storage
|
||||
*/
|
||||
export class HybridCache implements CacheProvider {
|
||||
private memoryCache: MemoryCache;
|
||||
private redisCache: RedisCache;
|
||||
private logger = getLogger('hybrid-cache');
|
||||
private enableMetrics: boolean;
|
||||
private startTime = Date.now();
|
||||
|
||||
private stats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
constructor(options: CacheOptions = {}) {
|
||||
this.enableMetrics = options.enableMetrics ?? true;
|
||||
|
||||
// Create L1 (memory) cache with shorter TTL
|
||||
this.memoryCache = new MemoryCache({
|
||||
...options,
|
||||
ttl: options.memoryTTL ?? 300, // 5 minutes for memory
|
||||
maxMemoryItems: options.maxMemoryItems ?? 1000,
|
||||
enableMetrics: false // We'll handle metrics at hybrid level
|
||||
});
|
||||
|
||||
// Create L2 (Redis) cache with longer TTL
|
||||
this.redisCache = new RedisCache({
|
||||
...options,
|
||||
enableMetrics: false // We'll handle metrics at hybrid level
|
||||
});
|
||||
|
||||
this.logger.info('Hybrid cache initialized', {
|
||||
memoryTTL: options.memoryTTL ?? 300,
|
||||
redisTTL: options.ttl ?? 3600,
|
||||
maxMemoryItems: options.maxMemoryItems ?? 1000
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
// Try L1 cache first (memory)
|
||||
const memoryValue = await this.memoryCache.get<T>(key);
|
||||
if (memoryValue !== null) {
|
||||
this.updateStats(true);
|
||||
this.logger.debug('L1 cache hit', { key, hitRate: this.stats.hitRate });
|
||||
return memoryValue;
|
||||
}
|
||||
|
||||
// Try L2 cache (Redis)
|
||||
const redisValue = await this.redisCache.get<T>(key);
|
||||
if (redisValue !== null) {
|
||||
// Populate L1 cache for next access
|
||||
await this.memoryCache.set(key, redisValue);
|
||||
this.updateStats(true);
|
||||
this.logger.debug('L2 cache hit, populating L1', { key, hitRate: this.stats.hitRate });
|
||||
return redisValue;
|
||||
}
|
||||
|
||||
// Complete miss
|
||||
this.updateStats(false);
|
||||
this.logger.debug('Cache miss (both L1 and L2)', { key });
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache get error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
try {
|
||||
// Set in both caches
|
||||
const memoryPromise = this.memoryCache.set(key, value, Math.min(ttl ?? 300, 300));
|
||||
const redisPromise = this.redisCache.set(key, value, ttl);
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.debug('Cache set (both L1 and L2)', { key, ttl });
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache set error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
try {
|
||||
// Delete from both caches
|
||||
const memoryPromise = this.memoryCache.del(key);
|
||||
const redisPromise = this.redisCache.del(key);
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.debug('Cache delete (both L1 and L2)', { key });
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache delete error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
// Check memory first, then Redis
|
||||
const memoryExists = await this.memoryCache.exists(key);
|
||||
if (memoryExists) return true;
|
||||
|
||||
return await this.redisCache.exists(key);
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache exists error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// Clear both caches
|
||||
const memoryPromise = this.memoryCache.clear();
|
||||
const redisPromise = this.redisCache.clear();
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.info('Cache cleared (both L1 and L2)');
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache clear error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
try {
|
||||
const memoryHealthy = await this.memoryCache.health();
|
||||
const redisHealthy = await this.redisCache.health();
|
||||
|
||||
// Hybrid cache is healthy if at least one cache is working
|
||||
const isHealthy = memoryHealthy || redisHealthy;
|
||||
|
||||
this.logger.debug('Hybrid cache health check', {
|
||||
memory: memoryHealthy,
|
||||
redis: redisHealthy,
|
||||
overall: isHealthy
|
||||
});
|
||||
|
||||
return isHealthy;
|
||||
} catch (error) {
|
||||
this.logger.error('Hybrid cache health check failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStats(): CacheStats {
|
||||
return {
|
||||
...this.stats,
|
||||
uptime: Date.now() - this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed stats for both cache layers
|
||||
*/
|
||||
getDetailedStats() {
|
||||
return {
|
||||
hybrid: this.getStats(),
|
||||
memory: this.memoryCache.getStats(),
|
||||
redis: this.redisCache.getStats()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm up the memory cache with frequently accessed keys from Redis
|
||||
*/
|
||||
async warmupMemoryCache(keys: string[]): Promise<void> {
|
||||
this.logger.info('Starting memory cache warmup', { keyCount: keys.length });
|
||||
|
||||
let warmed = 0;
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const value = await this.redisCache.get(key);
|
||||
if (value !== null) {
|
||||
await this.memoryCache.set(key, value);
|
||||
warmed++;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to warm up key', { key, error });
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info('Memory cache warmup completed', {
|
||||
requested: keys.length,
|
||||
warmed
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync memory cache with Redis for specific keys
|
||||
*/
|
||||
async syncCaches(keys: string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const redisValue = await this.redisCache.get(key);
|
||||
if (redisValue !== null) {
|
||||
await this.memoryCache.set(key, redisValue);
|
||||
} else {
|
||||
await this.memoryCache.del(key);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to sync key', { key, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connections for both caches
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.redisCache.disconnect();
|
||||
this.logger.info('Hybrid cache disconnected');
|
||||
}
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { CacheProvider, CacheOptions, CacheStats } from '../types';
|
||||
import { RedisCache } from './redis-cache';
|
||||
import { MemoryCache } from './memory-cache';
|
||||
|
||||
/**
|
||||
* Hybrid cache provider that uses memory as L1 cache and Redis as L2 cache
|
||||
* Provides the best of both worlds: fast memory access and persistent Redis storage
|
||||
*/
|
||||
export class HybridCache implements CacheProvider {
|
||||
private memoryCache: MemoryCache;
|
||||
private redisCache: RedisCache;
|
||||
private logger = getLogger('hybrid-cache');
|
||||
private enableMetrics: boolean;
|
||||
private startTime = Date.now();
|
||||
|
||||
private stats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
constructor(options: CacheOptions = {}) {
|
||||
this.enableMetrics = options.enableMetrics ?? true;
|
||||
|
||||
// Create L1 (memory) cache with shorter TTL
|
||||
this.memoryCache = new MemoryCache({
|
||||
...options,
|
||||
ttl: options.memoryTTL ?? 300, // 5 minutes for memory
|
||||
maxMemoryItems: options.maxMemoryItems ?? 1000,
|
||||
enableMetrics: false // We'll handle metrics at hybrid level
|
||||
});
|
||||
|
||||
// Create L2 (Redis) cache with longer TTL
|
||||
this.redisCache = new RedisCache({
|
||||
...options,
|
||||
enableMetrics: false // We'll handle metrics at hybrid level
|
||||
});
|
||||
|
||||
this.logger.info('Hybrid cache initialized', {
|
||||
memoryTTL: options.memoryTTL ?? 300,
|
||||
redisTTL: options.ttl ?? 3600,
|
||||
maxMemoryItems: options.maxMemoryItems ?? 1000
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
// Try L1 cache first (memory)
|
||||
const memoryValue = await this.memoryCache.get<T>(key);
|
||||
if (memoryValue !== null) {
|
||||
this.updateStats(true);
|
||||
this.logger.debug('L1 cache hit', { key, hitRate: this.stats.hitRate });
|
||||
return memoryValue;
|
||||
}
|
||||
|
||||
// Try L2 cache (Redis)
|
||||
const redisValue = await this.redisCache.get<T>(key);
|
||||
if (redisValue !== null) {
|
||||
// Populate L1 cache for next access
|
||||
await this.memoryCache.set(key, redisValue);
|
||||
this.updateStats(true);
|
||||
this.logger.debug('L2 cache hit, populating L1', { key, hitRate: this.stats.hitRate });
|
||||
return redisValue;
|
||||
}
|
||||
|
||||
// Complete miss
|
||||
this.updateStats(false);
|
||||
this.logger.debug('Cache miss (both L1 and L2)', { key });
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache get error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
try {
|
||||
// Set in both caches
|
||||
const memoryPromise = this.memoryCache.set(key, value, Math.min(ttl ?? 300, 300));
|
||||
const redisPromise = this.redisCache.set(key, value, ttl);
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.debug('Cache set (both L1 and L2)', { key, ttl });
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache set error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
try {
|
||||
// Delete from both caches
|
||||
const memoryPromise = this.memoryCache.del(key);
|
||||
const redisPromise = this.redisCache.del(key);
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.debug('Cache delete (both L1 and L2)', { key });
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache delete error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
// Check memory first, then Redis
|
||||
const memoryExists = await this.memoryCache.exists(key);
|
||||
if (memoryExists) return true;
|
||||
|
||||
return await this.redisCache.exists(key);
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache exists error', {
|
||||
key,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// Clear both caches
|
||||
const memoryPromise = this.memoryCache.clear();
|
||||
const redisPromise = this.redisCache.clear();
|
||||
|
||||
await Promise.allSettled([memoryPromise, redisPromise]);
|
||||
this.logger.info('Cache cleared (both L1 and L2)');
|
||||
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
this.logger.error('Hybrid cache clear error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
try {
|
||||
const memoryHealthy = await this.memoryCache.health();
|
||||
const redisHealthy = await this.redisCache.health();
|
||||
|
||||
// Hybrid cache is healthy if at least one cache is working
|
||||
const isHealthy = memoryHealthy || redisHealthy;
|
||||
|
||||
this.logger.debug('Hybrid cache health check', {
|
||||
memory: memoryHealthy,
|
||||
redis: redisHealthy,
|
||||
overall: isHealthy
|
||||
});
|
||||
|
||||
return isHealthy;
|
||||
} catch (error) {
|
||||
this.logger.error('Hybrid cache health check failed', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStats(): CacheStats {
|
||||
return {
|
||||
...this.stats,
|
||||
uptime: Date.now() - this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed stats for both cache layers
|
||||
*/
|
||||
getDetailedStats() {
|
||||
return {
|
||||
hybrid: this.getStats(),
|
||||
memory: this.memoryCache.getStats(),
|
||||
redis: this.redisCache.getStats()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm up the memory cache with frequently accessed keys from Redis
|
||||
*/
|
||||
async warmupMemoryCache(keys: string[]): Promise<void> {
|
||||
this.logger.info('Starting memory cache warmup', { keyCount: keys.length });
|
||||
|
||||
let warmed = 0;
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const value = await this.redisCache.get(key);
|
||||
if (value !== null) {
|
||||
await this.memoryCache.set(key, value);
|
||||
warmed++;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to warm up key', { key, error });
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info('Memory cache warmup completed', {
|
||||
requested: keys.length,
|
||||
warmed
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync memory cache with Redis for specific keys
|
||||
*/
|
||||
async syncCaches(keys: string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const redisValue = await this.redisCache.get(key);
|
||||
if (redisValue !== null) {
|
||||
await this.memoryCache.set(key, redisValue);
|
||||
} else {
|
||||
await this.memoryCache.del(key);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to sync key', { key, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connections for both caches
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.redisCache.disconnect();
|
||||
this.logger.info('Hybrid cache disconnected');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue