mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:53:45 -07:00
im replicating
This commit is contained in:
parent
78259115bd
commit
893dc60d0e
2 changed files with 163 additions and 80 deletions
|
|
@ -11,7 +11,8 @@ export default defineConfig({
|
||||||
integrations: [tailwind()],
|
integrations: [tailwind()],
|
||||||
vite: {
|
vite: {
|
||||||
define: {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
184
src/lib/db.ts
184
src/lib/db.ts
|
|
@ -2,17 +2,19 @@ import Redis from 'ioredis';
|
||||||
import type { FeedMapping } from './types';
|
import type { FeedMapping } from './types';
|
||||||
import { getChaptersForSlugs } from './api';
|
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 redisClient: Redis | null = null;
|
||||||
|
let replicaClient: Redis | null = null;
|
||||||
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
|
const DEFAULT_CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const BASE_DELAY_MS = 100;
|
const BASE_DELAY_MS = 100;
|
||||||
|
|
||||||
// Improved connection handling with exponential backoff
|
// Improved connection handling with exponential backoff
|
||||||
async function createRedisClient(): Promise<Redis> {
|
async function createRedisClient(url: string): Promise<Redis> {
|
||||||
if (!REDIS_URL) throw new Error('REDIS_URL environment variable is not set');
|
if (!url) throw new Error('Redis URL is not set');
|
||||||
|
|
||||||
return new Redis(REDIS_URL, {
|
return new Redis(url, {
|
||||||
retryStrategy: (times) => {
|
retryStrategy: (times) => {
|
||||||
if (times >= MAX_RETRIES) return null;
|
if (times >= MAX_RETRIES) return null;
|
||||||
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, times), 5000);
|
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> {
|
export async function getRedisClient(): Promise<Redis> {
|
||||||
|
// Try primary first
|
||||||
if (redisClient?.status === 'ready') {
|
if (redisClient?.status === 'ready') {
|
||||||
// Validate connection with a quick ping
|
|
||||||
try {
|
try {
|
||||||
await redisClient.ping();
|
await redisClient.ping();
|
||||||
return redisClient;
|
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
|
// Fall through to reconnect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Initializing new Redis connection...');
|
// Reconnect to primary
|
||||||
redisClient = await createRedisClient();
|
try {
|
||||||
|
console.log('Initializing new primary Redis connection...');
|
||||||
|
redisClient = await createRedisClient(PRIMARY_REDIS_URL!);
|
||||||
|
|
||||||
// Add event listeners for better monitoring
|
|
||||||
redisClient
|
redisClient
|
||||||
.on('ready', () => console.log('Redis connection ready'))
|
.on('ready', () => console.log('Primary Redis connection ready'))
|
||||||
.on('error', (err) => console.error('Redis error:', err))
|
.on('error', (err) => console.error('Primary Redis error:', err))
|
||||||
.on('reconnecting', () => console.log('Redis reconnecting'))
|
.on('reconnecting', () => console.log('Primary Redis reconnecting'))
|
||||||
.on('end', () => console.log('Redis connection closed'));
|
.on('end', () => console.log('Primary Redis connection closed'));
|
||||||
|
|
||||||
return redisClient;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch operations for better performance
|
throw new Error('All Redis connections failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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> {
|
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();
|
const pipeline = redis.pipeline();
|
||||||
|
|
||||||
mappings.forEach(({ feedId, slugs, lang }) => {
|
mappings.forEach(({ feedId, slugs, lang }) => {
|
||||||
|
|
@ -68,18 +137,19 @@ export async function storeFeedMappings(mappings: Array<{ feedId: string, slugs:
|
||||||
});
|
});
|
||||||
|
|
||||||
await pipeline.exec();
|
await pipeline.exec();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update warmChapterCache to prevent empty cache entries
|
|
||||||
export async function warmChapterCache(slugs: string[], lang: string): Promise<void> {
|
export async function warmChapterCache(slugs: string[], lang: string): Promise<void> {
|
||||||
const redis = await getRedisClient();
|
const redis = await getRedisClient();
|
||||||
const needsCache = await checkCacheStatus(slugs, lang);
|
const needsCache = await checkCacheStatus(slugs, lang);
|
||||||
|
|
||||||
if (needsCache.length > 0) {
|
if (needsCache.length > 0) {
|
||||||
console.log(`Pre-warming cache for ${needsCache.length} comics`);
|
console.log(`Pre-warming cache for ${needsCache.length} comics`);
|
||||||
|
|
||||||
|
await writeToAllRedis(async (redis) => {
|
||||||
const pipeline = redis.pipeline();
|
const pipeline = redis.pipeline();
|
||||||
|
|
||||||
// Replace empty array with actual chapter fetching
|
|
||||||
await Promise.all(needsCache.map(async slug => {
|
await Promise.all(needsCache.map(async slug => {
|
||||||
try {
|
try {
|
||||||
const chapters = await getChaptersForSlugs([slug], lang);
|
const chapters = await getChaptersForSlugs([slug], lang);
|
||||||
|
|
@ -91,10 +161,54 @@ export async function warmChapterCache(slugs: string[], lang: string): Promise<v
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await pipeline.exec();
|
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[]> {
|
export async function checkCacheStatus(slugs: string[], lang: string): Promise<string[]> {
|
||||||
const redis = await getRedisClient();
|
const redis = await getRedisClient();
|
||||||
const pipeline = redis.pipeline();
|
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);
|
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> {
|
export async function getFeedMapping(feedId: string): Promise<FeedMapping | null> {
|
||||||
const redis = await getRedisClient();
|
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)
|
// 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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue