diff --git a/faceai/apps/backend/src/config.js b/faceai/apps/backend/src/config.js index 6b43c1e2..bc5c1ecd 100644 --- a/faceai/apps/backend/src/config.js +++ b/faceai/apps/backend/src/config.js @@ -28,7 +28,7 @@ export const config = { uploadRoot: process.env.FACEAI_UPLOAD_ROOT || path.join(process.env.FACEAI_RUNTIME_ROOT || '/data/runtime', 'uploads'), searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60), resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60), - processorHeartbeatGraceMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_GRACE_MS || 20 * 1000), + processorHeartbeatGraceMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_GRACE_MS || 60 * 1000), rateLimitWindowSeconds: Number(process.env.FACEAI_RATE_LIMIT_WINDOW_SECONDS || 10 * 60), - rateLimitMaxRequests: Number(process.env.FACEAI_RATE_LIMIT_MAX_REQUESTS || 5) + rateLimitMaxRequests: Number(process.env.FACEAI_RATE_LIMIT_MAX_REQUESTS || 20) }; diff --git a/faceai/apps/backend/src/server.js b/faceai/apps/backend/src/server.js index 15c31f41..891e165d 100644 --- a/faceai/apps/backend/src/server.js +++ b/faceai/apps/backend/src/server.js @@ -20,6 +20,7 @@ import { getSearchRecord, incrementRateLimit, markSearchFailed, + releaseActiveSearchLock, saveSearchRecord } from './redis-store.js'; import { getSearchQueue } from './queue.js'; @@ -126,6 +127,27 @@ async function failSearchIfProcessorUnavailable(search) { ); } +async function resolveBlockingActiveSearch(userId) { + const activeSearchId = await getActiveSearchId(redis, userId); + if (!activeSearchId) { + return null; + } + + const existingSearch = await getSearchRecord(redis, activeSearchId); + if (!existingSearch) { + await releaseActiveSearchLock(redis, userId, activeSearchId); + return null; + } + + const normalizedSearch = await failSearchIfProcessorUnavailable(existingSearch); + if (!normalizedSearch || normalizedSearch.status === 'completed' || normalizedSearch.status === 'failed') { + await releaseActiveSearchLock(redis, userId, activeSearchId); + return null; + } + + return normalizedSearch; +} + function logHealthFailure(details) { const signature = JSON.stringify(details); if (signature === lastHealthFailureSignature) { @@ -528,13 +550,14 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single( return; } - const activeSearchId = await getActiveSearchId(redis, userId); + const activeSearch = await resolveBlockingActiveSearch(userId); - if (activeSearchId) { + if (activeSearch) { res.status(409).json({ error: 'There is already an operation being processed.', code: 'ACTIVE_SEARCH_EXISTS', - activeSearchId + activeSearchId: activeSearch.id, + activeSearchStatus: activeSearch.status }); return; } diff --git a/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue b/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue index 19f21986..793214cc 100644 --- a/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue +++ b/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue @@ -1,5 +1,7 @@