im replicating

This commit is contained in:
ld3zian 2025-02-28 16:56:28 -07:00
parent 78259115bd
commit 893dc60d0e
2 changed files with 163 additions and 80 deletions

View file

@ -11,7 +11,8 @@ export default defineConfig({
integrations: [tailwind()],
vite: {
define: {
'process.env.REDIS_URL': JSON.stringify(process.env.REDIS_URL)
'process.env.PRIMARY_REDIS_URL': JSON.stringify(process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL),
'process.env.REPLICA_REDIS_URL': JSON.stringify(process.env.REPLICA_REDIS_URL)
}
}
});

View file

@ -2,17 +2,19 @@ import Redis from 'ioredis';
import type { FeedMapping } from './types';
import { getChaptersForSlugs } from './api';
const REDIS_URL = import.meta.env.REDIS_URL || process.env.REDIS_URL;
const PRIMARY_REDIS_URL = import.meta.env.PRIMARY_REDIS_URL || process.env.PRIMARY_REDIS_URL || process.env.REDIS_URL;
const REPLICA_REDIS_URL = import.meta.env.REPLICA_REDIS_URL || process.env.REPLICA_REDIS_URL;
let redisClient: Redis | null = null;
let replicaClient: Redis | null = null;
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 100;
// Improved connection handling with exponential backoff
async function createRedisClient(): Promise<Redis> {
if (!REDIS_URL) throw new Error('REDIS_URL environment variable is not set');
async function createRedisClient(url: string): Promise<Redis> {
if (!url) throw new Error('Redis URL is not set');
return new Redis(REDIS_URL, {
return new Redis(url, {
retryStrategy: (times) => {
if (times >= MAX_RETRIES) return null;
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, times), 5000);
@ -28,34 +30,101 @@ async function createRedisClient(): Promise<Redis> {
});
}
// Singleton pattern with connection pooling
// Get the best available Redis client
export async function getRedisClient(): Promise<Redis> {
// Try primary first
if (redisClient?.status === 'ready') {
// Validate connection with a quick ping
try {
await redisClient.ping();
return redisClient;
} catch {
} catch (error) {
console.warn('Primary Redis connection failed, will try replica:', error);
// Fall through to try replica
}
}
// Try replica if available
if (REPLICA_REDIS_URL && replicaClient?.status === 'ready') {
try {
await replicaClient.ping();
return replicaClient;
} catch (error) {
console.warn('Replica Redis connection failed:', error);
// Fall through to reconnect
}
}
console.log('Initializing new Redis connection...');
redisClient = await createRedisClient();
// Reconnect to primary
try {
console.log('Initializing new primary Redis connection...');
redisClient = await createRedisClient(PRIMARY_REDIS_URL!);
// Add event listeners for better monitoring
redisClient
.on('ready', () => console.log('Redis connection ready'))
.on('error', (err) => console.error('Redis error:', err))
.on('reconnecting', () => console.log('Redis reconnecting'))
.on('end', () => console.log('Redis connection closed'));
.on('ready', () => console.log('Primary Redis connection ready'))
.on('error', (err) => console.error('Primary Redis error:', err))
.on('reconnecting', () => console.log('Primary Redis reconnecting'))
.on('end', () => console.log('Primary Redis connection closed'));
return redisClient;
} catch (error) {
console.error('Failed to connect to primary Redis:', error);
// If primary fails and we have a replica URL, try connecting to replica
if (REPLICA_REDIS_URL) {
console.log('Falling back to replica Redis...');
replicaClient = await createRedisClient(REPLICA_REDIS_URL);
replicaClient
.on('ready', () => console.log('Replica Redis connection ready'))
.on('error', (err) => console.error('Replica Redis error:', err))
.on('reconnecting', () => console.log('Replica Redis reconnecting'))
.on('end', () => console.log('Replica Redis connection closed'));
return replicaClient;
}
throw new Error('All Redis connections failed');
}
}
// Batch operations for better performance
// Write to both primary and replica for redundancy
export async function writeToAllRedis(operation: (redis: Redis) => Promise<any>): Promise<void> {
const errors = [];
// Try primary
try {
if (!redisClient || redisClient.status !== 'ready') {
redisClient = await createRedisClient(PRIMARY_REDIS_URL!);
}
await operation(redisClient);
} catch (error) {
console.error('Failed to write to primary Redis:', error);
errors.push(error);
}
// Try replica if available (for redundancy)
if (REPLICA_REDIS_URL) {
try {
if (!replicaClient || replicaClient.status !== 'ready') {
replicaClient = await createRedisClient(REPLICA_REDIS_URL);
}
await operation(replicaClient);
} catch (error) {
console.error('Failed to write to replica Redis:', error);
errors.push(error);
}
}
// If both writes failed, throw an error
if (errors.length === (REPLICA_REDIS_URL ? 2 : 1)) {
throw new Error('Failed to write to any Redis instance');
}
}
// Update existing functions to use the new methods
export async function storeFeedMappings(mappings: Array<{ feedId: string, slugs: string[], lang: string }>): Promise<void> {
const redis = await getRedisClient();
await writeToAllRedis(async (redis) => {
const pipeline = redis.pipeline();
mappings.forEach(({ feedId, slugs, lang }) => {
@ -68,18 +137,19 @@ export async function storeFeedMappings(mappings: Array<{ feedId: string, slugs:
});
await pipeline.exec();
});
}
// Update warmChapterCache to prevent empty cache entries
export async function warmChapterCache(slugs: string[], lang: string): Promise<void> {
const redis = await getRedisClient();
const needsCache = await checkCacheStatus(slugs, lang);
if (needsCache.length > 0) {
console.log(`Pre-warming cache for ${needsCache.length} comics`);
await writeToAllRedis(async (redis) => {
const pipeline = redis.pipeline();
// Replace empty array with actual chapter fetching
await Promise.all(needsCache.map(async slug => {
try {
const chapters = await getChaptersForSlugs([slug], lang);
@ -91,10 +161,54 @@ export async function warmChapterCache(slugs: string[], lang: string): Promise<v
}));
await pipeline.exec();
});
}
}
// Add batch cache checking
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> {
const mapping = {
slugs,
lang,
created_at: new Date().toISOString()
};
try {
await writeToAllRedis(async (redis) => {
await redis.set(
feedId,
JSON.stringify(mapping),
'EX',
DEFAULT_CACHE_TTL
);
});
} catch (error) {
console.error('Error storing feed mapping:', error);
throw new Error('Failed to store feed mapping');
}
}
// Initialize both connections on startup
(async () => {
try {
// Initialize primary
if (PRIMARY_REDIS_URL) {
redisClient = await createRedisClient(PRIMARY_REDIS_URL);
await redisClient.ping();
console.log('Primary Redis pre-connected successfully');
}
// Initialize replica if available
if (REPLICA_REDIS_URL) {
replicaClient = await createRedisClient(REPLICA_REDIS_URL);
await replicaClient.ping();
console.log('Replica Redis pre-connected successfully');
}
} catch (error) {
console.error('Redis pre-connection failed:', error);
}
})();
// Batch operations for better performance
export async function checkCacheStatus(slugs: string[], lang: string): Promise<string[]> {
const redis = await getRedisClient();
const pipeline = redis.pipeline();
@ -108,27 +222,6 @@ export async function checkCacheStatus(slugs: string[], lang: string): Promise<s
return slugs.filter((_, index) => results![index][1] === 0);
}
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise<void> {
const redis = await getRedisClient();
const mapping = {
slugs,
lang,
created_at: new Date().toISOString()
};
try {
await redis.set(
feedId,
JSON.stringify(mapping),
'EX',
DEFAULT_CACHE_TTL
);
} catch (error) {
console.error('Error storing feed mapping:', error);
throw new Error('Failed to store feed mapping');
}
}
export async function getFeedMapping(feedId: string): Promise<FeedMapping | null> {
const redis = await getRedisClient();
@ -150,14 +243,3 @@ export async function getFeedMapping(feedId: string): Promise<FeedMapping | null
}
// No need for manual cleanup as we're using TTL (expire)
// Warm connection on startup
(async () => {
try {
const client = await getRedisClient();
await client.ping();
console.log('Redis pre-connected successfully');
} catch (error) {
console.error('Redis pre-connection failed:', error);
}
})();