init2222
This commit is contained in:
@@ -53,6 +53,12 @@ export let env = {
|
||||
allowCredentials: p.CORS_ALLOW_CREDENTIALS === 'true',
|
||||
},
|
||||
port: parseInt(p.API_PORT || '3001'),
|
||||
redis: {
|
||||
host: p.REDIS_HOST || 'keydb',
|
||||
port: parseInt(p.REDIS_PORT || '6379'),
|
||||
password: p.REDIS_PASSWORD || '',
|
||||
db: parseInt(p.REDIS_DB || '0'),
|
||||
},
|
||||
relayApiKey: p.RELAY_API_KEY || null,
|
||||
tronApiKey: p.TRON_API_KEY || null,
|
||||
jupiterApiKey: p.JUPITER_API_KEY || null,
|
||||
|
||||
86
apps/api/src/config/redis.ts
Normal file
86
apps/api/src/config/redis.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* KeyDB / Redis singleton client.
|
||||
*
|
||||
* Используется для idempotency cache (см. `lib/idempotency.ts`).
|
||||
*
|
||||
* Connection:
|
||||
* REDIS_HOST=keydb (docker service name) / REDIS_PORT=6379 / REDIS_PASSWORD / REDIS_DB=0
|
||||
*
|
||||
* Startup contract: `pingRedis()` вызывается из `index.ts` и throws если KeyDB
|
||||
* unreachable — fail-fast, потому что idempotency critical для money flow.
|
||||
*/
|
||||
|
||||
import Redis, { type RedisOptions } from 'ioredis';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
let _client: Redis | null = null;
|
||||
|
||||
function buildClient(): Redis {
|
||||
const host = process.env.REDIS_HOST || 'keydb';
|
||||
const port = parseInt(process.env.REDIS_PORT || '6379', 10);
|
||||
const password = process.env.REDIS_PASSWORD || '';
|
||||
const db = parseInt(process.env.REDIS_DB || '0', 10);
|
||||
|
||||
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`Invalid REDIS_PORT ${process.env.REDIS_PORT}`);
|
||||
}
|
||||
if (!Number.isFinite(db) || db < 0 || db > 15) {
|
||||
throw new Error(`Invalid REDIS_DB ${process.env.REDIS_DB} (must be 0-15)`);
|
||||
}
|
||||
|
||||
const opts: RedisOptions = {
|
||||
host,
|
||||
port,
|
||||
db,
|
||||
lazyConnect: true,
|
||||
// Не зависать forever — fail-fast если cache недоступен
|
||||
connectTimeout: 5000,
|
||||
maxRetriesPerRequest: 3,
|
||||
// Reconnect strategy: exponential backoff, max 5s
|
||||
retryStrategy: (times) => Math.min(times * 200, 5000),
|
||||
};
|
||||
if (password) opts.password = password;
|
||||
|
||||
const client = new Redis(opts);
|
||||
|
||||
client.on('error', (err) => {
|
||||
// Не логируем secret в случае конфигурационной ошибки
|
||||
logger.error(`Redis client error: ${err.message}`);
|
||||
});
|
||||
client.on('connect', () => logger.info(`Redis connected (host=${host}:${port} db=${db})`));
|
||||
client.on('reconnecting', (delay: number) => logger.warn(`Redis reconnecting in ${delay}ms`));
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Lazily initialised singleton. */
|
||||
export function getRedis(): Redis {
|
||||
if (!_client) {
|
||||
_client = buildClient();
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startup ping. Throws on failure → caller process.exit(1).
|
||||
* Connect-on-demand (lazyConnect=true), .ping() триггерит connect + первый round-trip.
|
||||
*/
|
||||
export async function pingRedis(): Promise<void> {
|
||||
const client = getRedis();
|
||||
try {
|
||||
const pong = await client.ping();
|
||||
if (pong !== 'PONG') {
|
||||
throw new Error(`Redis PING returned ${pong} (expected PONG)`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw new Error(`Redis ping failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Graceful shutdown — closes connection cleanly. */
|
||||
export async function closeRedis(): Promise<void> {
|
||||
if (_client) {
|
||||
await _client.quit().catch(() => _client?.disconnect());
|
||||
_client = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user