Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ENABLE_SECURE_COOKIES=true
# - Session 追踪:5 分钟上下文缓存优化(避免频繁切换供应商)
# - Fail Open 策略:Redis 不可用时自动降级,不影响服务可用性
ENABLE_RATE_LIMIT=true # 是否启用限流功能(默认:true)
REDIS_URL=redis://localhost:6379 # Redis 连接地址(Docker 部署使用 redis://redis:6379)
REDIS_URL=redis://localhost:6379 # Redis 连接地址(Docker 部署使用 redis://redis:6379,支持 rediss:// TLS

# Session 配置
SESSION_TTL=300 # Session 过期时间(秒,默认 300 = 5 分钟)
Expand Down
30 changes: 28 additions & 2 deletions src/lib/log-cleanup/cleanup-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,35 @@ function getCleanupQueue(): Queue.Queue {
redisUrl: redisUrl.replace(/:[^:]*@/, ":***@"), // 隐藏密码
});

// --- START SNI/TLS FIX ---
const useTls = redisUrl.startsWith("rediss://");
// Bull 需要一个 RedisOptions 对象
const redisQueueOptions: Queue.QueueOptions["redis"] = {};

try {
// 使用 Node.js 内置的 URL 解析器
const url = new URL(redisUrl);
redisQueueOptions.host = url.hostname;
redisQueueOptions.port = parseInt(url.port || (useTls ? "6379" : "6379"));
redisQueueOptions.password = url.password;
redisQueueOptions.username = url.username; // 传递用户名

if (useTls) {
logger.info("[CleanupQueue] Using TLS connection (rediss://)");
redisQueueOptions.tls = {
host: url.hostname, // 显式 SNI 修复
};
}
} catch (e) {
logger.error("[CleanupQueue] Failed to parse REDIS_URL, connection will fail:", e);
// 如果 URL 格式错误,则抛出异常停止启动
throw new Error("Invalid REDIS_URL format");
}
// --- END SNI/TLS FIX ---

// 创建队列实例
_cleanupQueue = new Queue("log-cleanup", {
redis: redisUrl, // 直接使用 URL 字符串
redis: redisQueueOptions, // 替换:使用我们解析后的对象
defaultJobOptions: {
attempts: 3, // 失败重试 3 次
backoff: {
Expand Down Expand Up @@ -190,4 +216,4 @@ export async function stopCleanupQueue() {
await _cleanupQueue.close();
logger.info({ action: "cleanup_queue_closed" });
}
}
}
30 changes: 28 additions & 2 deletions src/lib/notification/notification-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,35 @@ function getNotificationQueue(): Queue.Queue<NotificationJobData> {
redisUrl: redisUrl.replace(/:[^:]*@/, ":***@"), // 隐藏密码
});

// --- START SNI/TLS FIX ---
const useTls = redisUrl.startsWith("rediss://");
// Bull 需要一个 RedisOptions 对象
const redisQueueOptions: Queue.QueueOptions["redis"] = {};

try {
// 使用 Node.js 内置的 URL 解析器
const url = new URL(redisUrl);
redisQueueOptions.host = url.hostname;
redisQueueOptions.port = parseInt(url.port || (useTls ? "6379" : "6379"));
redisQueueOptions.password = url.password;
redisQueueOptions.username = url.username; // 传递用户名

if (useTls) {
logger.info("[NotificationQueue] Using TLS connection (rediss://)");
redisQueueOptions.tls = {
host: url.hostname, // 显式 SNI 修复
};
}
} catch (e) {
logger.error("[NotificationQueue] Failed to parse REDIS_URL, connection will fail:", e);
// 如果 URL 格式错误,则抛出异常停止启动
throw new Error("Invalid REDIS_URL format");
}
// --- END SNI/TLS FIX ---

// 创建队列实例
_notificationQueue = new Queue<NotificationJobData>("notifications", {
redis: redisUrl, // 直接使用 URL 字符串
redis: redisQueueOptions, // 替换:使用我们解析后的对象
defaultJobOptions: {
attempts: 3, // 失败重试 3 次
backoff: {
Expand Down Expand Up @@ -318,4 +344,4 @@ export async function stopNotificationQueue() {
await _notificationQueue.close();
logger.info({ action: "notification_queue_closed" });
}
}
}
33 changes: 29 additions & 4 deletions src/lib/redis/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Redis from "ioredis";
import Redis, { RedisOptions } from "ioredis";
import { logger } from "@/lib/logger";

let redisClient: Redis | null = null;
Expand All @@ -22,7 +22,10 @@ export function getRedisClient(): Redis | null {
}

try {
redisClient = new Redis(redisUrl, {
const useTls = redisUrl.startsWith("rediss://");

// 1. 定义基础配置
const redisOptions: RedisOptions = {
enableOfflineQueue: false, // 快速失败
maxRetriesPerRequest: 3,
retryStrategy(times) {
Expand All @@ -34,8 +37,28 @@ export function getRedisClient(): Redis | null {
logger.warn(`[Redis] Retry ${times}/5 after ${delay}ms`);
return delay;
},
});
};

// 2. 如果使用 rediss://,则添加显式的 TLS 和 SNI (host) 配置
if (useTls) {
logger.info("[Redis] Using TLS connection (rediss://)");
try {
// 从 URL 中解析 hostname,用于 SNI
const url = new URL(redisUrl);
redisOptions.tls = {
host: url.hostname,
};
} catch (e) {
logger.error("[Redis] Failed to parse REDIS_URL for TLS host:", e);
// 如果 URL 解析失败,回退
redisOptions.tls = {};
}
}

// 3. 使用组合后的配置创建客户端
redisClient = new Redis(redisUrl, redisOptions);

// 4. 保持原始的事件监听器
redisClient.on("connect", () => {
logger.info("[Redis] Connected successfully");
});
Expand All @@ -48,7 +71,9 @@ export function getRedisClient(): Redis | null {
logger.warn("[Redis] Connection closed");
});

// 5. 返回客户端实例
return redisClient;

} catch (error) {
logger.error("[Redis] Failed to initialize:", error);
return null;
Expand All @@ -60,4 +85,4 @@ export async function closeRedis(): Promise<void> {
await redisClient.quit();
redisClient = null;
}
}
}