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 @@