From f546f41f21d4208cd8444d6766c03a6cbdae2cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 27 Jul 2026 10:08:40 +0400 Subject: [PATCH] v1.0.53: file-based tracker with fcntl.flock (multi-worker gunicorn fix) --- site/app.py | 2 +- site/operations/tracker.py | 72 ++++++++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/site/app.py b/site/app.py index 342a0b8..8451e32 100644 --- a/site/app.py +++ b/site/app.py @@ -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.52" +VERSION = "1.0.53" 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") diff --git a/site/operations/tracker.py b/site/operations/tracker.py index fe43424..c5837fc 100644 --- a/site/operations/tracker.py +++ b/site/operations/tracker.py @@ -1,9 +1,11 @@ -"""Трекер созданных инстансов — in-memory dict (один gunicorn worker).""" -import threading +"""Трекер созданных инстансов — файловый с fcntl.flock (multi-worker gunicorn).""" +import fcntl +import json +import os -_LOCK = threading.Lock() +_PATH = "/tmp/instances.json" -_data = { +_INITIAL = { "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d": {"svcId": 1, "displayName": "autotest-1", "instanceUid": "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d"}, "884356cf-5212-4395-b56b-27c58a5fc1fa": {"svcId": 1, "displayName": "autotest-1-first", "instanceUid": "884356cf-5212-4395-b56b-27c58a5fc1fa"}, "6528854d-a4ea-428c-9fa4-68e85fa9b3ac": {"svcId": 1, "displayName": "curl-test-144834", "instanceUid": "6528854d-a4ea-428c-9fa4-68e85fa9b3ac"}, @@ -11,20 +13,62 @@ _data = { } +def _locked_read(): + """Читает файл под эксклюзивной блокировкой, seed при отсутствии.""" + try: + fd = os.open(_PATH, os.O_RDWR | os.O_CREAT, 0o644) + except OSError: + return dict(_INITIAL) + try: + fcntl.flock(fd, fcntl.LOCK_EX) + try: + data = os.read(fd, 65536) + if data: + return json.loads(data.decode("utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError): + pass + # Файл пуст или битый — пишем _INITIAL + data = dict(_INITIAL) + os.lseek(fd, 0, 0) + os.ftruncate(fd, 0) + os.write(fd, json.dumps(data, indent=2).encode("utf-8")) + return data + finally: + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + + +def _locked_write(data): + """Пишет файл под эксклюзивной блокировкой.""" + try: + fd = os.open(_PATH, os.O_RDWR | os.O_CREAT, 0o644) + except OSError: + return + try: + fcntl.flock(fd, fcntl.LOCK_EX) + os.lseek(fd, 0, 0) + os.ftruncate(fd, 0) + os.write(fd, json.dumps(data, indent=2).encode("utf-8")) + finally: + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + + def add(instance_uid, svc_id, display_name): - with _LOCK: - _data[instance_uid] = { - "svcId": svc_id, - "displayName": display_name, - "instanceUid": instance_uid, - } + data = _locked_read() + data[instance_uid] = { + "svcId": svc_id, + "displayName": display_name, + "instanceUid": instance_uid, + } + _locked_write(data) def remove(instance_uid): - with _LOCK: - _data.pop(instance_uid, None) + data = _locked_read() + data.pop(instance_uid, None) + _locked_write(data) def list_all(): - with _LOCK: - return list(_data.values()) + return list(_locked_read().values())