v1.0.51: auto-detect stand (dev/test); init instance_groups; _finish_op full try/except
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user