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 |
|
||||
+1
-1
@@ -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")
|
||||
|
||||
@@ -177,6 +177,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
|
||||
t0 = time.time()
|
||||
deadline = t0 + 300
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
try:
|
||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
||||
except Exception:
|
||||
@@ -205,6 +206,10 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
|
||||
}
|
||||
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)}
|
||||
|
||||
|
||||
|
||||
+24
-4
@@ -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,13 +92,13 @@ def index():
|
||||
|
||||
services = []
|
||||
instances = []
|
||||
instance_groups = {}
|
||||
config = {}
|
||||
if active_token:
|
||||
endpoint = _detect_endpoint(active_token)
|
||||
if endpoint:
|
||||
try:
|
||||
client = HttpClient(
|
||||
current_app.config["NUBES_API_ENDPOINT"],
|
||||
active_token,
|
||||
)
|
||||
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", "")))
|
||||
@@ -100,6 +118,8 @@ def index():
|
||||
config = load_config()
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
else:
|
||||
error = "Токен невалиден или просрочен"
|
||||
|
||||
return render_template("index.html",
|
||||
organization=org, error=error,
|
||||
|
||||
Reference in New Issue
Block a user