chore(photo-ai): раздел 0 — жёсткие инварианты I1–I7 зафиксированы
План фото-ИИ с восстановлением лиц разбит на этапы; этот коммит закрывает
раздел 0 — семь инвариантов, которые нельзя ломать дальше. Два из них были
нарушены в текущем коде и исправлены здесь.
I5 (мягкие ошибки не сжигают попытки). Раньше любой сбой photo-ai —
503, обрыв сети, таймаут — попадал в общий catch, инкрементил attempts и
через три попытки переводил задание в error. Теперь ошибки разделены:
5xx/429/425/408 и сетевая недоступность возвращают задание в pending без
инкремента attempts, с экспоненциальной паузой 10 с → 300 с; лимит мягких
повторов (по умолчанию 60) даёт одну честную ошибку с понятным текстом.
Таймаут AbortSignal.timeout — жёсткая ошибка с попытками, как и раньше.
Счётчик мягких повторов живёт в памяти процесса и в счётчиках воркера,
метаданные повтора — в audit_log.target (soft_attempt/soft_limit), без
новых колонок. Новый аудит-код photo.job.soft_retry и подпись в audit.js.
Переменные PHOTO_AI_SOFT_MAX_RETRIES, PHOTO_AI_SOFT_BACKOFF_MS,
PHOTO_AI_SOFT_BACKOFF_MAX_MS описаны в .env.example и отдаются в
worker.config в GET /api/photo-jobs/status.
I7 (никаких прямых fs.* по uploads/). runAiEnhance писал результат
fs.writeFileSync в uploads/ и только потом persist в S3; enhanceWithSharp
делал то же через sharp toFile. Оба теперь считают буфер и пишут его
через storage.put — драйвер выбирает сам, локальной копии не остаётся.
Из worker.js убраны require('fs'), require('path') и параметр uploadsDir.
I3 (photo-ai не обязателен). photo_ai_enabled вычислялся внутри
cacheWrap('public-settings'), поэтому после перезапуска с пустым
PHOTO_AI_URL кнопка «🤖 ИИ» оставалась видимой до истечения кэша (60 с),
хотя enhance-ai уже отдавал 503. Флаг вынесен из кэша: он выводится из
PHOTO_AI_URL в памяти процесса и всегда актуален.
Проверено на стенде (журнал — в TODO_PHOTO_FACE_AI.md, раздел 0):
- I1: эталон /enhance снят на 5 фото (3 реальных, 2 синтетических),
два независимых прогона и прогон после правок совпали байт-в-байт
(sha256), /health отдаёт ok. Скрипты и эталон — в backups/ (вне git)
- I2: задание с params IS NULL и action='ai' дошло до done при
attempts=0, результат отдан из S3 (200)
- I3: с пустым PHOTO_AI_URL photo_ai_enabled=false сразу после старта,
enhance-ai → 503, остальные маршруты API живы
- I4: nvidia-ctk и nvidia-container-runtime на хосте отсутствуют, runtime
только runc — фото-ИИ поднялся на CPU, /health не падает. Проверка
PHOTO_AI_DEVICE переносится на приёмку Stage 1 (переменной ещё нет)
- I6: db/ не тронут, состав колонок photo_jobs прежний
- I7: node --check для всех изменённых JS, комментариев в диффе нет,
весь SQL параметризован
api.smoketest.js: контракт фото-воркера — согласованность
photo_ai_enabled и ai_configured, ключи мягких повторов в worker.config,
503/404 для enhance-ai на несуществующей записи (тест не создаёт реальных
заданий). Вместе с планом и чек-листом этапов.
This commit is contained in:
@@ -175,6 +175,30 @@ async function main() {
|
||||
const notifyDelGone = await api('/api/notifications/' + notifyCreate.data.id + '/read', { token, method: 'POST' });
|
||||
ok('notifications: удалённое уведомление -> 404', notifyDelGone.status === 404, notifyDelGone.status);
|
||||
|
||||
// Фото-ИИ: photo_ai_enabled обязан совпадать с ai_configured — оба выводятся из
|
||||
// PHOTO_AI_URL, и флаг не попадает в кэш public-settings, иначе после перезапуска
|
||||
// с пустым PHOTO_AI_URL кнопка «🤖 ИИ» висела бы до истечения кэша (I3).
|
||||
// enhance-ai проверяем на несуществующей записи: 503 без фото-ИИ и 404 с фото-ИИ,
|
||||
// чтобы дымовой тест не создавал реальных заданий фото-воркеру.
|
||||
const photoStatus = await api('/api/photo-jobs/status', { token });
|
||||
ok('photo-jobs/status -> 200', photoStatus.status === 200, photoStatus.status);
|
||||
ok('photo_ai_enabled согласован с ai_configured', pub.data.photo_ai_enabled === String(!!photoStatus.data.ai_configured), {
|
||||
photo_ai_enabled: pub.data.photo_ai_enabled,
|
||||
ai_configured: photoStatus.data.ai_configured,
|
||||
});
|
||||
const workerCfg = photoStatus.data.worker && photoStatus.data.worker.config;
|
||||
ok('worker.config содержит лимит мягких повторов', Boolean(workerCfg && workerCfg.soft_max_retries > 0), workerCfg);
|
||||
ok('worker.config.ai_url соответствует наличию фото-ИИ', Boolean(workerCfg) && workerCfg.ai_url === photoStatus.data.ai_url, {
|
||||
worker_ai_url: workerCfg && workerCfg.ai_url,
|
||||
ai_url: photoStatus.data.ai_url,
|
||||
});
|
||||
const enhanceAi = await api('/api/entries/99999999/photo/enhance-ai', { token, method: 'POST' });
|
||||
ok('enhance-ai: 503 без photo-ai либо 404 с photo-ai (запись не существует)', (photoStatus.data.ai_configured === false && enhanceAi.status === 503) || (photoStatus.data.ai_configured === true && enhanceAi.status === 404), {
|
||||
ai_configured: photoStatus.data.ai_configured,
|
||||
status: enhanceAi.status,
|
||||
body: enhanceAi.data,
|
||||
});
|
||||
|
||||
const logout = await api('/api/auth/logout', { token, method: 'POST' });
|
||||
ok('logout', logout.status === 200, logout.status);
|
||||
const afterLogout = await api('/api/auth/me', { token });
|
||||
|
||||
Reference in New Issue
Block a user