Files
autotest/DOCS/sonnet-question-tracker.md

8.1 KiB
Raw Permalink Blame History

⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.

Вопрос к Sonnet — архитектура трекера инстансов + UI кнопок

Отправлено: 27.07.2026


Ты — senior архитектор. Разбери проблему в Flask-приложении на Nubes pythonk8s (gunicorn, 1 worker).

Контекст

Приложение app-autotest — веб-интерфейс для автотестов операций облачных сервисов Nubes. Репозиторий: https://gitea.services.ngcloud.ru/forcloud/app-autotest.git Версия: v1.0.47

Файлы для изучения

Файл Назначение
site/app.py Точка входа Flask, VERSION, blueprints
site/routes/api_test.py POST /api/test (запуск), GET /api/test/status (поллинг), _finish_op (фоновый поток)
site/routes/main.py Главная страница, /api/operations/ (список инстансов из трекера)
site/operations/tracker.py Запись/чтение /tmp/instances.json (_INITIAL, add, remove, list_all)
site/templates/index.html UI: executeOp(), showStages(), поллинг, refreshInstances()
site/api/http_client.py HTTP-клиент (Bearer auth, User-Agent)

Полные пути в репозитории:

app-autotest/site/app.py
app-autotest/site/routes/api_test.py
app-autotest/site/routes/main.py
app-autotest/site/operations/tracker.py
app-autotest/site/templates/index.html
app-autotest/site/api/http_client.py

Поток CREATE (как работает сейчас)

  1. UI: пользователь жмёт «Создать» → showParams(18, 'create') → заполняет форму → executeOp(params)
  2. UI отправляет POST /api/test {serviceId:1, operation:"create", svcOperationId:18, displayName, params, instanceUid:""}
  3. Бэкенд api_test():
    • client.post("/instances", {serviceId, displayName}) → получает instanceUid
    • client.post("/instanceOperations", {instanceUid, operation:"create"}) → получает opUid
    • client.post("/instanceOperationCfsParams", ...) — для каждого параметра
    • client.post("/instanceOperations/{opUid}/run") — запуск
    • threading.Thread(target=_finish_op, args=(client, opUid, instanceUid, ...), daemon=True).start()
    • возвращает {status:"RUNNING", opUid, instanceUid}
  4. UI начинает поллинг: GET /api/test/status/<opUid> каждые 2 секунды
  5. _finish_op (фоновый daemon-поток):
    • поллит GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages
    • обновляет _op_results[opUid] (in-memory dict)
    • когда dtFinish != null и isSuccessful == true:
      • вызывает tracker_add(instanceUid, svcId, displayName) ← ПРОБЛЕМА ЗДЕСЬ
      • устанавливает _op_results[opUid] = {status:"OK", stages, duration}
    • таймаут 300 секунд
  6. UI при status=="OK": вызывает selectService(SVC_ID)GET /api/operations/1 → читает tracker → показывает список

ПРОБЛЕМА

Инстанс создаётся в Nubes (виден в UI платформы), НО в списке инстансов приложения НЕ появляется. tracker_add не вызывается → /tmp/instances.json не обновляется → инстанс не в списке.

Происходило 2 раза подряд (v1.0.46 и v1.0.47). Оба раза инстанс в Nubes есть, в трекере — нет.

Моя гипотеза

tracker_add вызывается внутри daemon-потока _finish_op. В gunicorn:

  • Воркер может быть перезапущен платформой в любой момент
  • Daemon-потоки умирают вместе с воркером — молча, без логов
  • Python не пишет traceback при смерти daemon-потока

Текущий код _finish_op (api_test.py, строки 153-195)

def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
    """Фоном ждать dtFinish и сохранить результат."""
    import time
    t0 = time.time()
    deadline = t0 + 300
    while time.time() < deadline:
        try:
            data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
        except Exception:
            time.sleep(5)
            continue
        op = data.get("instanceOperation", {})
        dt_finish = op.get("dtFinish")
        _op_results[op_uid] = {
            "status": "RUNNING",
            "stages": op.get("stages", []),
            "duration": round(time.time() - t0, 1),
        }
        if dt_finish and str(dt_finish).strip():
            is_ok = op.get("isSuccessful")
            err = op.get("errorLog") or ""
            # Сначала трекер — чтобы UI при poll уже видел инстанс
            if is_ok:
                if is_create:
                    tracker_add(instance_uid, svc_id, display_name)
                elif is_delete:
                    tracker_remove(instance_uid)
            _op_results[op_uid] = {
                "status": "OK" if is_ok else "FAIL",
                "error": str(err) if err else "",
                "stages": op.get("stages", []),
                "duration": round(time.time() - t0, 1),
            }
            return
        time.sleep(5)
    _op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}

Вопросы

1. Где вызывать tracker_add?

Вариант А: синхронно в api_test(), сразу после получения instanceUid от API (шаг 3), до запуска потока.

  • Плюс: гарантированная запись, инстанс в трекере мгновенно
  • Минус: если операция потом упадёт — в трекере «мусорный» инстанс (но это лучше чем отсутствие)

Вариант Б: subprocess.Popen вместо threading.Thread.

  • Плюс: процесс живёт независимо от gunicorn worker
  • Минус: сложнее, нужен IPC для _op_results

Вариант В: sqlite3 с WAL-режимом.

  • Плюс: атомарная запись, конкурентный доступ
  • Минус: без persistent volume данные теряются при редеплое (как и /tmp/)

2. Что делать с _op_results?

Сейчас это module-level dict. При нескольких gunicorn workers — каждый worker имеет свой dict. Нужен ли переход на sqlite/file-based storage для _op_results?

3. Надёжность под gunicorn

Как правильно организовать фоновую работу под gunicorn на pythonk8s (1 worker, нет PV, нет Redis/RabbitMQ)?

Дополнительно: UI кнопок

Нужен CSS чтобы кнопки операций шли горизонтально в 1-2 ряда, как в Nubes UI:

delete | modify | suspend | resume | redeploy | reconcile

Сейчас они в vertical списке внутри div.inst-ops. Предложи стиль.

Ограничения платформы

  • site/ — НЕ пакет (без __init__.py, конфликт с stdlib site.py)
  • Импорты без префикса site.: from api.http_client import ...
  • app.run(host="0.0.0.0", port=5000) — обязательно
  • Gunicorn запускается платформой, предположительно 1 worker
  • Нет persistent volume — данные в /tmp/ теряются при редеплое
  • Деплой: git push → managed service подхватывает → редеплой