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:
QTom
2025-12-09 17:04:01 +08:00
committed by QTom
parent 95870883a1
commit f5d1c25295
14 changed files with 2048 additions and 18 deletions

View File

@@ -6,6 +6,7 @@ const {
const { fromEnv } = require('@aws-sdk/credential-providers')
const logger = require('../utils/logger')
const config = require('../../config/config')
const userMessageQueueService = require('./userMessageQueueService')
class BedrockRelayService {
constructor() {
@@ -69,7 +70,70 @@ class BedrockRelayService {
// 处理非流式请求
async handleNonStreamRequest(requestBody, bedrockAccount = null) {
const accountId = bedrockAccount?.id
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 Bedrock handleNonStreamRequest')
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,
backendError: isBackendError ? queueResult.errorMessage : undefined
})
logger.warn(
`📬 User message queue ${errorType} for Bedrock account ${accountId}`,
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
}
}),
success: false
}
}
if (queueResult.acquired && !queueResult.skipped) {
queueLockAcquired = true
queueRequestId = queueResult.requestId
queueLockRenewalStopper = await userMessageQueueService.startLockRenewal(
accountId,
queueRequestId
)
}
}
const modelId = this._selectModel(requestBody, bedrockAccount)
const region = this._selectRegion(modelId, bedrockAccount)
const client = this._getBedrockClient(region, bedrockAccount)
@@ -106,12 +170,95 @@ class BedrockRelayService {
} catch (error) {
logger.error('❌ Bedrock非流式请求失败:', error)
throw this._handleBedrockError(error)
} finally {
// 📬 释放用户消息队列锁
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 Bedrock account ${accountId}:`,
releaseError.message
)
}
}
}
}
// 处理流式请求
async handleStreamRequest(requestBody, bedrockAccount = null, res) {
const accountId = bedrockAccount?.id
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 Bedrock handleStreamRequest')
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,
backendError: isBackendError ? queueResult.errorMessage : undefined
})
logger.warn(
`📬 User message queue ${errorType} for Bedrock account ${accountId} (stream)`,
isBackendError ? { backendError: queueResult.errorMessage } : {}
)
if (!res.headersSent) {
res.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`
res.write(errorEvent)
res.write('data: [DONE]\n\n')
res.end()
return { success: false, error: errorType }
}
if (queueResult.acquired && !queueResult.skipped) {
queueLockAcquired = true
queueRequestId = queueResult.requestId
queueLockRenewalStopper = await userMessageQueueService.startLockRenewal(
accountId,
queueRequestId
)
}
}
const modelId = this._selectModel(requestBody, bedrockAccount)
const region = this._selectRegion(modelId, bedrockAccount)
const client = this._getBedrockClient(region, bedrockAccount)
@@ -191,6 +338,21 @@ class BedrockRelayService {
res.end()
throw this._handleBedrockError(error)
} finally {
// 📬 释放用户消息队列锁
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 Bedrock stream account ${accountId}:`,
releaseError.message
)
}
}
}
}