v1.2.20: remaining Codex fixes — _op_results lock, advisory lock for scenarios, _ensure_schema logging, stale async generation token, validate-cfs explicit JSONDecodeError

This commit is contained in:
2026-07-31 18:07:24 +04:00
parent 58b823a260
commit 5065019ffd
7 changed files with 222 additions and 172 deletions
+31 -24
View File
@@ -24,6 +24,7 @@
"""
from flask import Blueprint, current_app, jsonify, request
import threading
import uuid
import os
import json
@@ -261,23 +262,27 @@ def api_test():
# Результаты фоновых операций: opUid → {status, error, stages, duration, _ts}
# _ts — timestamp добавления, для TTL-очистки (макс. 500 записей или старше 1 часа)
# ЗАЩИЩЕНО _op_results_lock — несколько потоков _finish_op + main thread api_test_status
_op_results = {}
_op_results_lock = threading.Lock()
_MAX_OP_RESULTS = 500
def _cleanup_op_results():
"""Удалить старые записи: старше 1 часа или сверх лимита."""
"""Удалить старые записи: старше 1 часа или сверх лимита.
Потокобезопасно — под _op_results_lock."""
import time
now = time.time()
# Удалить старше 1 часа
stale = [k for k, v in _op_results.items() if now - v.get("_ts", 0) > 3600]
for k in stale:
del _op_results[k]
# Если всё ещё много — удалить самые старые
if len(_op_results) > _MAX_OP_RESULTS:
sorted_keys = sorted(_op_results.keys(), key=lambda k: _op_results[k].get("_ts", 0))
for k in sorted_keys[:len(_op_results) - _MAX_OP_RESULTS]:
del _op_results[k]
with _op_results_lock:
# Удалить старше 1 часа (pop с default — безопасно при конкурентном доступе)
stale = [k for k, v in _op_results.items() if now - v.get("_ts", 0) > 3600]
for k in stale:
_op_results.pop(k, None)
# Если всё ещё много — удалить самые старые
if len(_op_results) > _MAX_OP_RESULTS:
sorted_keys = sorted(_op_results.keys(), key=lambda k: _op_results[k].get("_ts", 0))
for k in sorted_keys[:len(_op_results) - _MAX_OP_RESULTS]:
_op_results.pop(k, None)
def _get_instance_display_name(client, instance_uid):
@@ -291,7 +296,7 @@ def _get_instance_display_name(client, instance_uid):
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False, params=None, user_email="", app_version=""):
"""Фоном ждать dtFinish и сохранить результат."""
"""Фоном ждать dtFinish и сохранить результат (потокобезопасно для _op_results)."""
import time
_cleanup_op_results()
t0 = time.time()
@@ -299,15 +304,16 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
# Поллинг через общий модуль
poll_result = poll_until_done(client, op_uid)
# Обновить _op_results для UI
_op_results[op_uid] = {
"status": poll_result["status"],
"displayName": display_name,
"error": poll_result["error_log"],
"stages": poll_result["stages"],
"duration": poll_result["duration"],
"_ts": time.time(),
}
# Обновить _op_results для UI — под локом
with _op_results_lock:
_op_results[op_uid] = {
"status": poll_result["status"],
"displayName": display_name,
"error": poll_result["error_log"],
"stages": poll_result["stages"],
"duration": poll_result["duration"],
"_ts": time.time(),
}
# tracker_remove при успешном delete
if poll_result["status"] == "OK" and is_delete:
@@ -346,10 +352,11 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
@bp.route("/api/test/status/<op_uid>")
def api_test_status(op_uid):
"""Получить текущий статус операции (поллинг с UI)."""
# сначала проверяем фоновый трекер
if op_uid in _op_results:
return jsonify(_op_results[op_uid])
"""Получить текущий статус операции (поллинг с UI) — потокобезопасно."""
# сначала проверяем фоновый трекер — под локом
with _op_results_lock:
if op_uid in _op_results:
return jsonify(_op_results[op_uid])
# иначе спрашиваем API напрямую
try:
data = get_client().get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")