v1.0.51: auto-detect stand (dev/test); init instance_groups; _finish_op full try/except

This commit is contained in:
2026-07-27 09:54:49 +04:00
parent a271e863f3
commit 1786bc67dd
4 changed files with 203 additions and 47 deletions
+131
View File
@@ -0,0 +1,131 @@
# Пошаговый аудит кода — 27.07.2026 (v1.0.50)
## Методология
Полная трассировка CREATE-потока: UI → api_test() → _finish_op() → api_test_status() → refreshInstances() → api_operations(). Каждый if, try, except, присваивание.
---
## Результат: найдено 3 бага
### 🔴 Баг #1 (КРИТИЧЕСКИЙ): Multi-worker gunicorn — in-memory dict не работает
**Файл:** `site/operations/tracker.py`
```python
_data = {
"408b7f96-...": {...}, # 4 initial items
}
def add(instance_uid, svc_id, display_name):
with _LOCK:
_data[instance_uid] = {...}
```
`_data` — module-level dict. У каждого gunicorn worker своя копия модуля → свой `_data`.
**Трассировка:**
1. POST /api/test → попадает на **воркер A**
2. `tracker_add(...)``_data` воркера A = 5 элементов ✅
3. GET /api/operations/1 → попадает на **воркер B**
4. `tracker_list()``_data` воркера B = 4 элемента ❌
**Почему это объясняет ВСЕ симптомы:**
- После F5 — рандомный воркер → опять 4
- v1.0.46-1.0.49 — ни одно решение не помогало (in-memory принципиально не跨-process)
- Инстанс в Nubes есть, в трекере нет — разные воркеры
**Решение:** вернуть файловый трекер (`/tmp/instances.json`) с межпроцессной блокировкой (`fcntl.flock` вместо `threading.Lock`).
---
### 🔴 Баг #2 (КРИТИЧЕСКИЙ): _finish_op() умирает молча на не-словаре
**Файл:** `site/routes/api_test.py`, строки 176-179
```python
def _finish_op(...):
while time.time() < deadline:
try:
data = client.get(...) # ← except ловит ТОЛЬКО это
except Exception:
time.sleep(5)
continue
op = data.get("instanceOperation", {}) # ← если data не dict → AttributeError!
```
Если Nubes API возвращает `list`, `str`, `None` или любой не-словарь — `data.get()`**AttributeError**. Этот except НЕ покрывает строку 179. Поток умирает молча. Python daemon-потоки не пишут traceback.
**Решение:** обернуть ВСЁ тело цикла (строки 166-195) в `try/except Exception: print(traceback)`.
---
### 🟡 Баг #3 (НЕКРИТИЧНЫЙ): _op_results — та же multi-worker проблема
**Файл:** `site/routes/api_test.py`, строка 152
```python
_op_results = {} # module-level
# В _finish_op (воркер A):
_op_results[op_uid] = {"status": "OK", ...}
# В api_test_status (воркер B):
if op_uid in _op_results: # ← False! (другой воркер)
return jsonify(_op_results[op_uid])
# fallback → прямой запрос в Nubes API
```
**Некритично** потому что есть fallback: если `_op_results` не содержит op_uid, `api_test_status()` делает прямой GET в Nubes API и возвращает статус. UI получает stages напрямую из Nubes, не из `_op_results`.
**НО:** fallback не обновляет `tracker_remove` для delete. При multi-worker delete не удалит инстанс из трекера.
---
## Полная трассировка CREATE (все шаги)
| Шаг | Код | Результат | Статус |
|-----|-----|-----------|--------|
| 1 | `data = request.get_json()` | `svc_id=1, op_name="create", display_name="autotest-1-xxx"` | ✅ |
| 2 | `client.post("/instances", ...)` | `instance_uid = "f192b10d-..."` | ✅ |
| 3 | `client.post("/instanceOperations", ...)` | `op_uid = "d489348e-..."` | ✅ |
| 4 | `for pid,pval: client.post("/instanceOperationCfsParams", ...)` | Параметры установлены | ✅ |
| 5 | `client.post("/instanceOperations/{op_uid}/run")` | Операция запущена | ✅ |
| 6 | `tracker_add(instance_uid, svc_id, display_name)` | `_data[uid] = {...}` (in-memory) | ✅ |
| 7 | `threading.Thread(target=_finish_op, ...)` | Поток запущен | ✅ |
| 8 | `return {status:"RUNNING", opUid, instanceUid}` | Ответ UI | ✅ |
| 9 | UI poll `/api/test/status/<opUid>` | `_op_results[opUid]` или fallback API | ⚠️ разн. воркеры |
| 10 | `_finish_op` детектит `dtFinish` | `_op_results[opUid] = {status:"OK"}` | ⚠️ воркер A |
| 11 | UI: `refreshInstances()``/api/operations/1` | `tracker_list()` → 4 элемента | ❌ воркер B |
| 12 | `api_operations()` возвращает 4 инстанса | Нового нет | ❌ |
| 13 | F5 → `selectService(1)``/api/operations/1` | Опять 4 | ❌ |
---
## Дополнительные находки (не критические)
### ⚠️ params loop — orphaned resources при ошибке
Если `client.post("/instanceOperationCfsParams", ...)` падает на mid-param:
- Инстанс УЖЕ создан в Nubes (сирота)
- Операция УЖЕ создана (сирота)
- `tracker_add` НЕ вызван (он после цикла)
- Ответ: FAIL
### ⚠️ `data["svcOperationId"]` — KeyError если поле отсутствует
Вызов API без `svcOperationId` в JSON → KeyError → FAIL. Обработано внешним try/except.
### ⚠️ `int(pid)` — ValueError если param ID не число
`int("abc")` → ValueError → FAIL. Обработано внешним try/except.
### ⚠️ `_find_uid()` итерация по ВСЕМ значениям
Может случайно найти uid во вложенном объекте. Низкий риск, но нечисто.
---
## План исправлений для v1.0.51
| # | Что | Как |
|---|-----|-----|
| 1 | Файловый трекер с fcntl.flock | Вернуть `/tmp/instances.json`, заменить `threading.Lock` на `fcntl.flock` |
| 2 | _finish_op: try/except на всё тело | Обернуть строки 166-195 в `try/except: print(traceback)` |
| 3 | (опционально) `svc_id = int(data["serviceId"])` | Защита от строкового "1" в JSON |
+1 -1
View File
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
from routes.api import bp as api_bp
from routes.api_test import bp as api_test_bp
VERSION = "1.0.50"
VERSION = "1.0.51"
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
+28 -23
View File
@@ -178,33 +178,38 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
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")
# Обновляем stages по мере появления
_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 ""
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
# tracker_add для create уже вызван синхронно в api_test()
if is_ok and is_delete:
tracker_remove(instance_uid)
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")
# Обновляем stages по мере появления
_op_results[op_uid] = {
"status": "OK" if is_ok else "FAIL",
"error": str(err) if err else "",
"status": "RUNNING",
"stages": op.get("stages", []),
"duration": round(time.time() - t0, 1),
}
return
time.sleep(5)
if dt_finish and str(dt_finish).strip():
is_ok = op.get("isSuccessful")
err = op.get("errorLog") or ""
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
# tracker_add для create уже вызван синхронно в api_test()
if is_ok and 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)
except Exception:
import traceback
print(f"[ERROR] _finish_op crashed: {traceback.format_exc()}", flush=True)
time.sleep(5)
_op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}
+43 -23
View File
@@ -8,6 +8,24 @@ from runner import load_config
bp = Blueprint("main", __name__)
STANDS = [
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
"https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
]
def _detect_endpoint(token):
"""Пробуем токен против dev и test стендов, возвращаем рабочий URL."""
for ep in STANDS:
try:
c = HttpClient(ep, token)
data = c.get("/instances", params={"pageSize": 1, "page": 1})
if data.get("results") is not None:
return ep
except Exception:
continue
return None
def _mask(s):
if not s or len(s) < 8:
@@ -74,32 +92,34 @@ def index():
services = []
instances = []
instance_groups = {}
config = {}
if active_token:
try:
client = HttpClient(
current_app.config["NUBES_API_ENDPOINT"],
active_token,
)
org = get_organization(client)
raw_svc = get_services(client)
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
# инфраструктурные сервисы (платформа/среда)
infra_ids = {2, 12, 21, 22, 25, 26, 29, 110, 150}
raw_inst = get_instances(client)
instances = [i for i in raw_inst
if i.get("explainedStatus") not in ("deleted", "not created")
and i.get("serviceId") in infra_ids]
instances.sort(key=lambda i: (0 if i.get("serviceId") == 19 else 1, i.get("displayName", "")))
endpoint = _detect_endpoint(active_token)
if endpoint:
try:
client = HttpClient(endpoint, active_token)
org = get_organization(client)
raw_svc = get_services(client)
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
# инфраструктурные сервисы (платформа/среда)
infra_ids = {2, 12, 21, 22, 25, 26, 29, 110, 150}
raw_inst = get_instances(client)
instances = [i for i in raw_inst
if i.get("explainedStatus") not in ("deleted", "not created")
and i.get("serviceId") in infra_ids]
instances.sort(key=lambda i: (0 if i.get("serviceId") == 19 else 1, i.get("displayName", "")))
# group by service type
instance_groups = {}
for i in instances:
svc_name = i.get("svc", "Прочее")
instance_groups.setdefault(svc_name, []).append(i)
config = load_config()
except Exception as e:
error = str(e)
# group by service type
instance_groups = {}
for i in instances:
svc_name = i.get("svc", "Прочее")
instance_groups.setdefault(svc_name, []).append(i)
config = load_config()
except Exception as e:
error = str(e)
else:
error = "Токен невалиден или просрочен"
return render_template("index.html",
organization=org, error=error,