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
+129 -124
View File
@@ -32,6 +32,7 @@ from operations.executor import execute_operation
from operations.poll import poll_until_done
from db.pool import get_conn, put_conn
from db.save_run import save_run
from db.scenario_defs import unlock_scenario
# Все инстансы созданные сценарием имеют такой префикс в displayName
AUTOTEST_PREFIX = "autotest-scenario-"
@@ -174,135 +175,139 @@ def run_scenario(client, steps, client_id, stand, user_email, app_version, scena
bindings = {} # output_name → instance_uid (новый формат, именованные ссылки)
instance_map = {} # service_id → instance_uid (fallback, старый формат)
for i, step in enumerate(steps):
step_num = i + 1 # шаги нумеруются с 1 (для пользователя)
svc_id = step.get("service_id")
op_name = (step.get("operation") or "").strip()
symbolic_params = step.get("params", {})
try:
for i, step in enumerate(steps):
step_num = i + 1 # шаги нумеруются с 1 (для пользователя)
svc_id = step.get("service_id")
op_name = (step.get("operation") or "").strip()
symbolic_params = step.get("params", {})
try:
# Валидация базовых полей шага
if not isinstance(svc_id, int) or svc_id <= 0:
raise ValueError(f"Invalid service_id: {svc_id!r}")
if not op_name:
raise ValueError("Empty operation name")
# ═══ Шаг 1: получить svcOperationId для операции ═══
detail = get_service_detail(client, svc_id)
ops = detail.get("operations", [])
op = next((o for o in ops if o.get("operation") == op_name), None)
if not op:
raise ValueError(f"Operation '{op_name}' not found for service {svc_id}")
svc_op_id = op["svcOperationId"]
# ═══ Шаг 2: резолвить параметры (символ. имена → числовые ID) ═══
tmpl_data = client.get(f"/instanceOperations/default/{svc_op_id}")
cfs_params = tmpl_data.get("svcOperation", {}).get("cfsParams", [])
resolved_params = _resolve_params(cfs_params, symbolic_params)
# ═══ Шаг 3: резолвинг instance_uid ═══
is_create = (op_name == "create")
if is_create:
# Для create: instance_uid=None (создаём новый)
# displayName генерируем уникальный: autotest-scenario-{name}-{random6}
instance_uid = None
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
descr = f"scenario {scenario_name} step {step_num}"
else:
# Для не-create: резолвим instance_uid
instance_uid = _resolve_instance_uid(step, bindings, instance_map)
if not instance_uid:
raise ValueError(
f"No instance for step {step_num} — need CREATE before non-create operations")
display_name = None
descr = None
# ═══ Шаг 4: запуск через ЕДИНЫЙ executor ═══
# Это гарантирует идентичное поведение с ручным режимом
result = execute_operation(
client, svc_id, op_name, instance_uid, resolved_params,
svc_op_id=svc_op_id, display_name=display_name, descr=descr,
client_id=client_id, stand=stand
)
if not result["ok"]:
raise RuntimeError(
f"{op_name}: {result['error']} (step: {result['failed_step']})")
instance_uid = result["instance_uid"]
op_uid = result["op_uid"]
step_display = result["display_name"]
# Обновить карты для резолвинга следующих шагов
instance_map[svc_id] = instance_uid # fallback: последний инстанс сервиса
output_name = (step.get("output") or "").strip()
if is_create and output_name:
bindings[output_name] = instance_uid # именованная ссылка
_update_bindings(scenario_run_id, bindings)
# ═══ Шаг 5: сохранить RUNNING в БД (до поллинга) ═══
try:
save_run(client_id, stand, user_email,
svc_id, "", op_name, svc_op_id,
op_uid, instance_uid, step_display,
"RUNNING", 0, "",
resolved_params, [], app_version,
scenario_run_id, step_num)
except Exception as e:
print(f"[SCENARIO] save_run(RUNNING) failed for step {step_num}: {e}", flush=True)
# Валидация базовых полей шага
if not isinstance(svc_id, int) or svc_id <= 0:
raise ValueError(f"Invalid service_id: {svc_id!r}")
if not op_name:
raise ValueError("Empty operation name")
# ═══ Шаг 6: поллинг до завершения ═══
poll_result = poll_until_done(client, op_uid)
step_status = poll_result["status"] # OK / FAIL / TIMEOUT
step_error = poll_result["error_log"]
step_duration = poll_result["duration"]
step_stages = poll_result["stages"]
svc_final = poll_result["svc"]
# ═══ Шаг 1: получить svcOperationId для операции ═══
detail = get_service_detail(client, svc_id)
ops = detail.get("operations", [])
op = next((o for o in ops if o.get("operation") == op_name), None)
if not op:
raise ValueError(f"Operation '{op_name}' not found for service {svc_id}")
svc_op_id = op["svcOperationId"]
# ═══ Шаг 7: сохранить финальный результат + instance_meta ═══
try:
# Получить полную информацию об инстансе для анализа
instance_meta = None
# ═══ Шаг 2: резолвить параметры (символ. имена → числовые ID) ═══
tmpl_data = client.get(f"/instanceOperations/default/{svc_op_id}")
cfs_params = tmpl_data.get("svcOperation", {}).get("cfsParams", [])
resolved_params = _resolve_params(cfs_params, symbolic_params)
# ═══ Шаг 3: резолвинг instance_uid ═══
is_create = (op_name == "create")
if is_create:
# Для create: instance_uid=None (создаём новый)
# displayName генерируем уникальный: autotest-scenario-{name}-{random6}
instance_uid = None
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
descr = f"scenario {scenario_name} step {step_num}"
else:
# Для не-create: резолвим instance_uid
instance_uid = _resolve_instance_uid(step, bindings, instance_map)
if not instance_uid:
raise ValueError(
f"No instance for step {step_num} — need CREATE before non-create operations")
display_name = None
descr = None
# ═══ Шаг 4: запуск через ЕДИНЫЙ executor ═══
# Это гарантирует идентичное поведение с ручным режимом
result = execute_operation(
client, svc_id, op_name, instance_uid, resolved_params,
svc_op_id=svc_op_id, display_name=display_name, descr=descr,
client_id=client_id, stand=stand
)
if not result["ok"]:
raise RuntimeError(
f"{op_name}: {result['error']} (step: {result['failed_step']})")
instance_uid = result["instance_uid"]
op_uid = result["op_uid"]
step_display = result["display_name"]
# Обновить карты для резолвинга следующих шагов
instance_map[svc_id] = instance_uid # fallback: последний инстанс сервиса
output_name = (step.get("output") or "").strip()
if is_create and output_name:
bindings[output_name] = instance_uid # именованная ссылка
_update_bindings(scenario_run_id, bindings)
# ═══ Шаг 5: сохранить RUNNING в БД (до поллинга) ═══
try:
inst_data = client.get(f"/instances/{instance_uid}")
inst = inst_data.get("instance", {}) if isinstance(inst_data, dict) else {}
if inst:
# Сохраняем ВСЁ кроме очевидных/избыточных полей
instance_meta = {k: v for k, v in inst.items()
if k not in ('instanceUid', 'displayName', 'svc', 'serviceId', 'explainedStatus')}
except Exception:
pass # не смогли получить meta — не критично
save_run(client_id, stand, user_email,
svc_id, svc_final, op_name, svc_op_id,
op_uid, instance_uid, step_display,
step_status, step_duration, step_error,
resolved_params, step_stages, app_version,
scenario_run_id, step_num,
instance_meta=instance_meta)
save_run(client_id, stand, user_email,
svc_id, "", op_name, svc_op_id,
op_uid, instance_uid, step_display,
"RUNNING", 0, "",
resolved_params, [], app_version,
scenario_run_id, step_num)
except Exception as e:
print(f"[SCENARIO] save_run(RUNNING) failed for step {step_num}: {e}", flush=True)
# ═══ Шаг 6: поллинг до завершения ═══
poll_result = poll_until_done(client, op_uid)
step_status = poll_result["status"] # OK / FAIL / TIMEOUT
step_error = poll_result["error_log"]
step_duration = poll_result["duration"]
step_stages = poll_result["stages"]
svc_final = poll_result["svc"]
# ═══ Шаг 7: сохранить финальный результат + instance_meta ═══
try:
# Получить полную информацию об инстансе для анализа
instance_meta = None
try:
inst_data = client.get(f"/instances/{instance_uid}")
inst = inst_data.get("instance", {}) if isinstance(inst_data, dict) else {}
if inst:
# Сохраняем ВСЁ кроме очевидных/избыточных полей
instance_meta = {k: v for k, v in inst.items()
if k not in ('instanceUid', 'displayName', 'svc', 'serviceId', 'explainedStatus')}
except Exception:
pass # не смогли получить meta — не критично
save_run(client_id, stand, user_email,
svc_id, svc_final, op_name, svc_op_id,
op_uid, instance_uid, step_display,
step_status, step_duration, step_error,
resolved_params, step_stages, app_version,
scenario_run_id, step_num,
instance_meta=instance_meta)
except Exception as e:
print(f"[SCENARIO] save_run failed for step {step_num}: {e}", flush=True)
# ═══ Шаг 8: обновить scenario_runs (прогресс для UI) ═══
# Пока не последний шаг — статус RUNNING
_save_scenario_run(scenario_run_id,
"RUNNING" if step_num < total else step_status,
step_num, round(time.time() - t0, 1),
step_error if step_status != "OK" else None)
# Если шаг FAIL — останавливаем сценарий
if step_status != "OK":
_save_scenario_run(scenario_run_id, step_status, step_num,
round(time.time() - t0, 1), step_error)
return
except Exception as e:
print(f"[SCENARIO] save_run failed for step {step_num}: {e}", flush=True)
# ═══ Шаг 8: обновить scenario_runs (прогресс для UI) ═══
# Пока не последний шаг — статус RUNNING
_save_scenario_run(scenario_run_id,
"RUNNING" if step_num < total else step_status,
step_num, round(time.time() - t0, 1),
step_error if step_status != "OK" else None)
# Если шаг FAIL — останавливаем сценарий
if step_status != "OK":
_save_scenario_run(scenario_run_id, step_status, step_num,
round(time.time() - t0, 1), step_error)
# Любая ошибка → FAIL, сценарий остановлен
import traceback
err = f"Step {step_num}: {e}"
print(f"[SCENARIO] {err}", flush=True)
traceback.print_exc()
_save_scenario_run(scenario_run_id, "FAIL", step_num,
round(time.time() - t0, 1), err)
return
except Exception as e:
# Любая ошибка → FAIL, сценарий остановлен
import traceback
err = f"Step {step_num}: {e}"
print(f"[SCENARIO] {err}", flush=True)
traceback.print_exc()
_save_scenario_run(scenario_run_id, "FAIL", step_num,
round(time.time() - t0, 1), err)
return
# Все шаги пройдены успешно
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))
# Все шаги пройдены успешно
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))
finally:
# Освободить advisory lock ВСЕГДА (даже при исключении)
unlock_scenario(client_id, stand)