mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:35:09 +00:00
feat: 添加用户消息串行队列功能,防止同账户并发请求触发限流
- 新增 userMessageQueueService.js 实现基于 Redis 的队列锁机制 - 在 claudeRelayService、claudeConsoleRelayService、bedrockRelayService、ccrRelayService 中集成队列锁 - 添加 Redis 原子性 Lua 脚本:acquireUserMessageLock、releaseUserMessageLock、refreshUserMessageLock - 支持锁续租机制,防止长时间请求锁过期 - 添加可配置参数:USER_MESSAGE_QUEUE_ENABLED、USER_MESSAGE_QUEUE_DELAY_MS、USER_MESSAGE_QUEUE_TIMEOUT_MS - 添加 Web 管理界面配置入口 - 添加 logger.performance 方法用于结构化性能日志 - 添加完整单元测试 (tests/userMessageQueue.test.js)
This commit is contained in:
@@ -9,6 +9,7 @@ const {
|
||||
sanitizeErrorMessage,
|
||||
isAccountDisabledError
|
||||
} = require('../utils/errorSanitizer')
|
||||
const userMessageQueueService = require('./userMessageQueueService')
|
||||
|
||||
class ClaudeConsoleRelayService {
|
||||
constructor() {
|
||||
@@ -29,8 +30,73 @@ class ClaudeConsoleRelayService {
|
||||
let account = null
|
||||
const requestId = uuidv4() // 用于并发追踪
|
||||
let concurrencyAcquired = false
|
||||
let queueLockAcquired = false
|
||||
let queueRequestId = null
|
||||
let queueLockRenewalStopper = null
|
||||
|
||||
try {
|
||||
// 📬 用户消息队列处理:如果是用户消息请求,需要获取队列锁
|
||||
if (userMessageQueueService.isUserMessageRequest(requestBody)) {
|
||||
// 校验 accountId 非空,避免空值污染队列锁键
|
||||
if (!accountId || accountId === '') {
|
||||
logger.error('❌ accountId missing for queue lock in console relayRequest')
|
||||
throw new Error('accountId missing for queue lock')
|
||||
}
|
||||
const queueResult = await userMessageQueueService.acquireQueueLock(accountId)
|
||||
if (!queueResult.acquired && !queueResult.skipped) {
|
||||
// 区分 Redis 后端错误和队列超时
|
||||
const isBackendError = queueResult.error === 'queue_backend_error'
|
||||
const errorCode = isBackendError ? 'QUEUE_BACKEND_ERROR' : 'QUEUE_TIMEOUT'
|
||||
const errorType = isBackendError ? 'queue_backend_error' : 'queue_timeout'
|
||||
const errorMessage = isBackendError
|
||||
? 'Queue service temporarily unavailable, please retry later'
|
||||
: 'User message queue wait timeout, please retry later'
|
||||
const statusCode = isBackendError ? 500 : 503
|
||||
|
||||
// 结构化性能日志,用于后续统计
|
||||
logger.performance('user_message_queue_error', {
|
||||
errorType,
|
||||
errorCode,
|
||||
accountId,
|
||||
statusCode,
|
||||
apiKeyName: apiKeyData.name,
|
||||
backendError: isBackendError ? queueResult.errorMessage : undefined
|
||||
})
|
||||
|
||||
logger.warn(
|
||||
`📬 User message queue ${errorType} for console account ${accountId}, key: ${apiKeyData.name}`,
|
||||
isBackendError ? { backendError: queueResult.errorMessage } : {}
|
||||
)
|
||||
return {
|
||||
statusCode,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-message-queue-error': errorType
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: errorType,
|
||||
code: errorCode,
|
||||
message: errorMessage
|
||||
}
|
||||
}),
|
||||
accountId
|
||||
}
|
||||
}
|
||||
if (queueResult.acquired && !queueResult.skipped) {
|
||||
queueLockAcquired = true
|
||||
queueRequestId = queueResult.requestId
|
||||
queueLockRenewalStopper = await userMessageQueueService.startLockRenewal(
|
||||
accountId,
|
||||
queueRequestId
|
||||
)
|
||||
logger.debug(
|
||||
`📬 User message queue lock acquired for console account ${accountId}, requestId: ${queueRequestId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账户信息
|
||||
account = await claudeConsoleAccountService.getAccount(accountId)
|
||||
if (!account) {
|
||||
@@ -366,6 +432,21 @@ class ClaudeConsoleRelayService {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 📬 释放用户消息队列锁
|
||||
if (queueLockAcquired && queueRequestId && accountId) {
|
||||
try {
|
||||
if (queueLockRenewalStopper) {
|
||||
queueLockRenewalStopper()
|
||||
}
|
||||
await userMessageQueueService.releaseQueueLock(accountId, queueRequestId)
|
||||
} catch (releaseError) {
|
||||
logger.error(
|
||||
`❌ Failed to release user message queue lock for account ${accountId}:`,
|
||||
releaseError.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,8 +465,73 @@ class ClaudeConsoleRelayService {
|
||||
const requestId = uuidv4() // 用于并发追踪
|
||||
let concurrencyAcquired = false
|
||||
let leaseRefreshInterval = null // 租约刷新定时器
|
||||
let queueLockAcquired = false
|
||||
let queueRequestId = null
|
||||
let queueLockRenewalStopper = null
|
||||
|
||||
try {
|
||||
// 📬 用户消息队列处理:如果是用户消息请求,需要获取队列锁
|
||||
if (userMessageQueueService.isUserMessageRequest(requestBody)) {
|
||||
// 校验 accountId 非空,避免空值污染队列锁键
|
||||
if (!accountId || accountId === '') {
|
||||
logger.error(
|
||||
'❌ accountId missing for queue lock in console relayStreamRequestWithUsageCapture'
|
||||
)
|
||||
throw new Error('accountId missing for queue lock')
|
||||
}
|
||||
const queueResult = await userMessageQueueService.acquireQueueLock(accountId)
|
||||
if (!queueResult.acquired && !queueResult.skipped) {
|
||||
// 区分 Redis 后端错误和队列超时
|
||||
const isBackendError = queueResult.error === 'queue_backend_error'
|
||||
const errorCode = isBackendError ? 'QUEUE_BACKEND_ERROR' : 'QUEUE_TIMEOUT'
|
||||
const errorType = isBackendError ? 'queue_backend_error' : 'queue_timeout'
|
||||
const errorMessage = isBackendError
|
||||
? 'Queue service temporarily unavailable, please retry later'
|
||||
: 'User message queue wait timeout, please retry later'
|
||||
const statusCode = isBackendError ? 500 : 503
|
||||
|
||||
// 结构化性能日志,用于后续统计
|
||||
logger.performance('user_message_queue_error', {
|
||||
errorType,
|
||||
errorCode,
|
||||
accountId,
|
||||
statusCode,
|
||||
stream: true,
|
||||
apiKeyName: apiKeyData.name,
|
||||
backendError: isBackendError ? queueResult.errorMessage : undefined
|
||||
})
|
||||
|
||||
logger.warn(
|
||||
`📬 User message queue ${errorType} for console account ${accountId} (stream), key: ${apiKeyData.name}`,
|
||||
isBackendError ? { backendError: queueResult.errorMessage } : {}
|
||||
)
|
||||
if (!responseStream.headersSent) {
|
||||
responseStream.writeHead(statusCode, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
'x-user-message-queue-error': errorType
|
||||
})
|
||||
}
|
||||
const errorEvent = `event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: errorType, code: errorCode, message: errorMessage } })}\n\n`
|
||||
responseStream.write(errorEvent)
|
||||
responseStream.write('data: [DONE]\n\n')
|
||||
responseStream.end()
|
||||
return
|
||||
}
|
||||
if (queueResult.acquired && !queueResult.skipped) {
|
||||
queueLockAcquired = true
|
||||
queueRequestId = queueResult.requestId
|
||||
queueLockRenewalStopper = await userMessageQueueService.startLockRenewal(
|
||||
accountId,
|
||||
queueRequestId
|
||||
)
|
||||
logger.debug(
|
||||
`📬 User message queue lock acquired for console account ${accountId} (stream), requestId: ${queueRequestId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账户信息
|
||||
account = await claudeConsoleAccountService.getAccount(accountId)
|
||||
if (!account) {
|
||||
@@ -517,6 +663,21 @@ class ClaudeConsoleRelayService {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 📬 释放用户消息队列锁
|
||||
if (queueLockAcquired && queueRequestId && accountId) {
|
||||
try {
|
||||
if (queueLockRenewalStopper) {
|
||||
queueLockRenewalStopper()
|
||||
}
|
||||
await userMessageQueueService.releaseQueueLock(accountId, queueRequestId)
|
||||
} catch (releaseError) {
|
||||
logger.error(
|
||||
`❌ Failed to release user message queue lock for stream account ${accountId}:`,
|
||||
releaseError.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user