Files
app-autotest/site/operations/tracker.py
T

119 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Трекер созданных инстансов.
Хранит UID'ы инстансов, созданных через приложение, в JSON-файлах.
Нужен ТОЛЬКО как краткосрочный fallback — когда инстанс создан, но ещё
не появился в ответе GET /instances (облако может задержать на секунды).
Файлы изолированы по пользователю и стенду:
/tmp/instances-{clientId}-{stand}.json
Блокировка fcntl.flock для безопасной работы с несколькими воркерами gunicorn.
При редеплое /tmp/ теряется — это НЕ критично, cloud-first выдача всё покажет.
Функции:
add(client_id, stand, uid, svc_id, name) — добавить инстанс
remove(client_id, stand, uid) — удалить (после успешного delete)
list_all(client_id, stand) — все записи списком
"""
import fcntl
import json
import os
import time
def _path(client_id, stand):
"""Путь к файлу трекера для конкретного пользователя и стенда."""
return f"/tmp/instances-{client_id}-{stand}.json"
def _acquire_lock(fd):
"""Эксклюзивная блокировка файла с таймаутом 2 секунды.
Использует LOCK_NB (неблокирующий) + retry с шагом 50ms.
Если за 2 секунды не взяли лок — возвращает False (не блокируемся навечно)."""
deadline = time.time() + 2
while True:
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return True
except BlockingIOError:
if time.time() >= deadline:
return False
time.sleep(0.05)
def _locked_read(path):
"""Читает JSON-файл под эксклюзивной блокировкой.
Если файла нет — создаёт и возвращает пустой {}.
Если JSON битый — возвращает {} (переживёт перезапись).
Максимальный размер: 65536 байт."""
try:
# O_RDWR — чтение+запись, O_CREAT — создать если нет
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
except OSError:
return {} # нет прав — молча возвращаем пустой
try:
if not _acquire_lock(fd):
return {} # не смогли заблокировать за 2с — не рискуем
try:
data = os.read(fd, 65536)
if data:
return json.loads(data.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
pass # битый файл — переживём, при следующей записи перезапишется
return {}
finally:
fcntl.flock(fd, fcntl.LOCK_UN) # ВСЕГДА снимаем блокировку
os.close(fd)
def _locked_write(path, data):
"""Пишет JSON-файл под эксклюзивной блокировкой.
Полностью перезаписывает файл: lseek(0) + ftruncate + write."""
try:
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
except OSError:
return
try:
if not _acquire_lock(fd):
return # не смогли заблокировать — не пишем (данные не потеряются, просто не сохранятся)
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(client_id, stand, instance_uid, svc_id, display_name):
"""Добавить инстанс в трекер.
Читает текущий файл → добавляет запись → пишет обратно.
Если инстанс уже есть — перезаписывает (идемпотентно)."""
p = _path(client_id, stand)
data = _locked_read(p)
data[instance_uid] = {
"svcId": svc_id,
"displayName": display_name,
"instanceUid": instance_uid,
}
_locked_write(p, data)
def remove(client_id, stand, instance_uid):
"""Удалить инстанс из трекера (после успешного delete).
pop с default=None — не падает если инстанса уже нет."""
p = _path(client_id, stand)
data = _locked_read(p)
data.pop(instance_uid, None)
_locked_write(p, data)
def list_all(client_id, stand):
"""Все записи трекера — список dict'ов {svcId, displayName, instanceUid}."""
return list(_locked_read(_path(client_id, stand)).values())