docs: receive all docs from app-autotest
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# ⚠️ 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/<svcId> (список инстансов из трекера) |
|
||||
| `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)
|
||||
|
||||
```python
|
||||
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 подхватывает → редеплой
|
||||
Reference in New Issue
Block a user