chore: commit all pending changes and ignore project context
This commit is contained in:
+306
-138
@@ -47,12 +47,18 @@ SESSION_IDLE_SECONDS = int(os.getenv("SESSION_IDLE_SECONDS", "300"))
|
||||
PUBLIC_HOST = os.getenv("PUBLIC_HOST", "stend.4mont.ru")
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
LOG_SLOW_REQUEST_MS = int(os.getenv("LOG_SLOW_REQUEST_MS", "2000"))
|
||||
GO_USER_LOCK_TIMEOUT_SECONDS = float(os.getenv("GO_USER_LOCK_TIMEOUT_SECONDS", "8.0"))
|
||||
GO_POOL_LOCK_TIMEOUT_SECONDS = float(os.getenv("GO_POOL_LOCK_TIMEOUT_SECONDS", "5.0"))
|
||||
POOL_DISPATCH_RETRIES = int(os.getenv("POOL_DISPATCH_RETRIES", "4"))
|
||||
POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS = float(os.getenv("POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS", "2.0"))
|
||||
POOL_DISPATCH_SLEEP_SECONDS = float(os.getenv("POOL_DISPATCH_SLEEP_SECONDS", "0.3"))
|
||||
TRAEFIK_INTERNAL_URL = os.getenv("TRAEFIK_INTERNAL_URL", "http://traefik")
|
||||
PREWARM_POOL_SIZE = int(os.getenv("PREWARM_POOL_SIZE", "0"))
|
||||
UNIVERSAL_POOL_SIZE = int(os.getenv("UNIVERSAL_POOL_SIZE", "0"))
|
||||
WEB_POOL_SIZE = int(os.getenv("WEB_POOL_SIZE", "5"))
|
||||
WEB_POOL_BUFFER = int(os.getenv("WEB_POOL_BUFFER", "2"))
|
||||
MAX_ACTIVE_SERVICES_PER_USER = int(os.getenv("MAX_ACTIVE_SERVICES_PER_USER", "4"))
|
||||
ENABLE_STARTUP_MAINTENANCE = os.getenv("ENABLE_STARTUP_MAINTENANCE", "1") == "1"
|
||||
ICON_UPLOAD_MAX_BYTES = 2 * 1024 * 1024
|
||||
ICON_UPLOAD_TYPES = {
|
||||
"image/png": "png",
|
||||
@@ -67,6 +73,7 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger("portal")
|
||||
request_id_ctx = contextvars.ContextVar("request_id", default="-")
|
||||
maintenance_lock_file = None
|
||||
|
||||
|
||||
def _normalize_log_value(value):
|
||||
@@ -791,14 +798,14 @@ def dispatch_universal_target(slot: int, service: Service) -> None:
|
||||
raise HTTPException(status_code=400, detail="Universal pool supports WEB/RDP only")
|
||||
|
||||
last_exc = None
|
||||
for _ in range(8):
|
||||
for _ in range(max(1, POOL_DISPATCH_RETRIES)):
|
||||
try:
|
||||
resp = requests.post(url, json=payload, timeout=3)
|
||||
resp = requests.post(url, json=payload, timeout=POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS)
|
||||
resp.raise_for_status()
|
||||
return
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
time.sleep(0.4)
|
||||
time.sleep(max(0.0, POOL_DISPATCH_SLEEP_SECONDS))
|
||||
if last_exc:
|
||||
raise last_exc
|
||||
|
||||
@@ -808,14 +815,14 @@ def dispatch_web_pool_target(slot: int, service: Service) -> None:
|
||||
target_url = normalize_web_target(service.target)
|
||||
url = f"http://{name}:7000/open"
|
||||
last_exc = None
|
||||
for _ in range(8):
|
||||
for _ in range(max(1, POOL_DISPATCH_RETRIES)):
|
||||
try:
|
||||
resp = requests.post(url, json={"url": target_url}, timeout=3)
|
||||
resp = requests.post(url, json={"url": target_url}, timeout=POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS)
|
||||
resp.raise_for_status()
|
||||
return
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
time.sleep(0.4)
|
||||
time.sleep(max(0.0, POOL_DISPATCH_SLEEP_SECONDS))
|
||||
if last_exc:
|
||||
raise last_exc
|
||||
|
||||
@@ -1363,14 +1370,31 @@ def find_active_session_for_user_service(db: Session, user_id: int, service_id:
|
||||
return db.scalars(q).first()
|
||||
|
||||
|
||||
def allocator_lock(db: Session, lock_id: int):
|
||||
class LockTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def allocator_lock(db: Session, lock_id: int, timeout_seconds: Optional[float] = None, poll_seconds: float = 0.05):
|
||||
class _LockCtx:
|
||||
def __enter__(self_nonlocal):
|
||||
db.execute(text("SELECT pg_advisory_lock(:lid)"), {"lid": lock_id})
|
||||
self_nonlocal._acquired = False
|
||||
if timeout_seconds is None:
|
||||
db.execute(text("SELECT pg_advisory_xact_lock(:lid)"), {"lid": lock_id})
|
||||
self_nonlocal._acquired = True
|
||||
return self_nonlocal
|
||||
|
||||
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
||||
while time.monotonic() <= deadline:
|
||||
got = db.execute(text("SELECT pg_try_advisory_xact_lock(:lid)"), {"lid": lock_id}).scalar()
|
||||
if got:
|
||||
self_nonlocal._acquired = True
|
||||
return self_nonlocal
|
||||
time.sleep(max(0.01, poll_seconds))
|
||||
raise LockTimeoutError(f"advisory lock timeout lock_id={lock_id} timeout={timeout_seconds}")
|
||||
|
||||
return self_nonlocal
|
||||
|
||||
def __exit__(self_nonlocal, exc_type, exc, tb):
|
||||
db.execute(text("SELECT pg_advisory_unlock(:lid)"), {"lid": lock_id})
|
||||
return False
|
||||
|
||||
return _LockCtx()
|
||||
@@ -1492,17 +1516,36 @@ def bootstrap_admin():
|
||||
db.close()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup_event():
|
||||
# Multiple uvicorn workers run startup in parallel. Serialize schema bootstrap
|
||||
# to avoid DDL races on first run and during schema extension.
|
||||
def try_acquire_maintenance_leader() -> bool:
|
||||
global maintenance_lock_file
|
||||
if maintenance_lock_file is not None:
|
||||
return True
|
||||
lock_file = open("/tmp/portal-maintenance.lock", "w")
|
||||
try:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
lock_file.close()
|
||||
return False
|
||||
maintenance_lock_file = lock_file
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def run_maintenance_service() -> None:
|
||||
logger.info("maintenance_service_bootstrap_started")
|
||||
with open("/tmp/portal-schema.lock", "w") as lock_file:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
ensure_schema_compatibility()
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
ensure_icons_dir()
|
||||
bootstrap_admin()
|
||||
|
||||
maintenance_lock = open("/tmp/portal-maintenance.lock", "w")
|
||||
fcntl.flock(maintenance_lock.fileno(), fcntl.LOCK_EX)
|
||||
logger.info("maintenance_service_leader_acquired")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
ensure_universal_pool()
|
||||
@@ -1517,8 +1560,46 @@ def startup_event():
|
||||
ensure_warm_pool(svc)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info("maintenance_service_loop_started")
|
||||
cleanup_loop()
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup_event():
|
||||
# Multiple uvicorn workers run startup in parallel. Serialize schema bootstrap
|
||||
# to avoid DDL races on first run and during schema extension.
|
||||
with open("/tmp/portal-schema.lock", "w") as lock_file:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
ensure_schema_compatibility()
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
ensure_icons_dir()
|
||||
bootstrap_admin()
|
||||
if not ENABLE_STARTUP_MAINTENANCE:
|
||||
logger.info("startup_maintenance_disabled")
|
||||
return
|
||||
if not try_acquire_maintenance_leader():
|
||||
logger.info("maintenance_leader_skipped")
|
||||
return
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
ensure_universal_pool()
|
||||
ensure_web_pool()
|
||||
for svc in db.scalars(
|
||||
select(Service).where(
|
||||
Service.active == True,
|
||||
Service.type.in_([ServiceType.WEB, ServiceType.RDP]),
|
||||
)
|
||||
).all():
|
||||
if svc.type == ServiceType.WEB and WEB_POOL_SIZE <= 0:
|
||||
ensure_warm_pool(svc)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
thread = threading.Thread(target=cleanup_loop, daemon=True)
|
||||
thread.start()
|
||||
logger.info("maintenance_leader_started")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@@ -1778,6 +1859,23 @@ def logout(request: Request):
|
||||
|
||||
@app.get("/go/{slug}")
|
||||
def go_service(slug: str, user: User = Depends(require_user), db: Session = Depends(get_db)):
|
||||
total_started = time.perf_counter()
|
||||
phase_ms = {}
|
||||
|
||||
def _mark(name: str, started: float) -> None:
|
||||
phase_ms[name] = int((time.perf_counter() - started) * 1000)
|
||||
|
||||
def _emit(result: str, **extra) -> None:
|
||||
payload = {
|
||||
"user_id": user.id,
|
||||
"service_slug": slug,
|
||||
"result": result,
|
||||
"total_ms": int((time.perf_counter() - total_started) * 1000),
|
||||
}
|
||||
payload.update(phase_ms)
|
||||
payload.update(extra)
|
||||
log_event("go_service_timing", **payload)
|
||||
|
||||
log_event("session_open_requested", user_id=user.id, service_slug=slug)
|
||||
service = db.scalar(select(Service).where(Service.slug == slug, Service.active == True))
|
||||
if not service:
|
||||
@@ -1786,148 +1884,218 @@ def go_service(slug: str, user: User = Depends(require_user), db: Session = Depe
|
||||
raise HTTPException(status_code=410, detail="VNC services are deprecated")
|
||||
if not has_access(db, user.id, service.id):
|
||||
raise HTTPException(status_code=403, detail="ACL denied")
|
||||
with allocator_lock(db, 92000 + int(user.id)):
|
||||
existing_user_session = find_active_session_for_user_service(db, user.id, service.id)
|
||||
if existing_user_session:
|
||||
return RedirectResponse(url=session_redirect_url(existing_user_session), status_code=303)
|
||||
|
||||
cutoff = now_utc() - dt.timedelta(seconds=SESSION_IDLE_SECONDS)
|
||||
active_rows = db.scalars(
|
||||
select(SessionModel).where(
|
||||
SessionModel.user_id == user.id,
|
||||
SessionModel.status == SessionStatus.ACTIVE,
|
||||
SessionModel.last_access_at >= cutoff,
|
||||
)
|
||||
).all()
|
||||
active_rows = sorted(active_rows, key=lambda row: row.created_at)
|
||||
active_service_ids = {row.service_id for row in active_rows}
|
||||
if service.id not in active_service_ids and len(active_service_ids) >= MAX_ACTIVE_SERVICES_PER_USER:
|
||||
oldest = next((row for row in active_rows if row.service_id != service.id), None)
|
||||
if oldest:
|
||||
terminate_session_record(db, oldest, SessionStatus.ROTATED, stop_container=True)
|
||||
db.commit()
|
||||
log_event(
|
||||
"session_rotated",
|
||||
user_id=user.id,
|
||||
closed_session_id=oldest.id,
|
||||
closed_service_id=oldest.service_id,
|
||||
new_service_id=service.id,
|
||||
user_lock_started = time.perf_counter()
|
||||
try:
|
||||
with allocator_lock(db, 92000 + int(user.id), timeout_seconds=GO_USER_LOCK_TIMEOUT_SECONDS):
|
||||
_mark("wait_user_lock_ms", user_lock_started)
|
||||
|
||||
t_existing = time.perf_counter()
|
||||
existing_user_session = find_active_session_for_user_service(db, user.id, service.id)
|
||||
_mark("check_existing_ms", t_existing)
|
||||
if existing_user_session:
|
||||
_emit("reuse_session", session_id=existing_user_session.id)
|
||||
return RedirectResponse(url=session_redirect_url(existing_user_session), status_code=303)
|
||||
|
||||
t_limit = time.perf_counter()
|
||||
cutoff = now_utc() - dt.timedelta(seconds=SESSION_IDLE_SECONDS)
|
||||
active_rows = db.scalars(
|
||||
select(SessionModel).where(
|
||||
SessionModel.user_id == user.id,
|
||||
SessionModel.status == SessionStatus.ACTIVE,
|
||||
SessionModel.last_access_at >= cutoff,
|
||||
)
|
||||
else:
|
||||
return RedirectResponse(url="/?launch_error=max_services", status_code=303)
|
||||
|
||||
if service.type == ServiceType.RDP:
|
||||
active_owner = find_active_session_for_service(db, service.id)
|
||||
if active_owner:
|
||||
if active_owner.user_id != user.id:
|
||||
owner = db.get(User, active_owner.user_id)
|
||||
owner_name = owner.username if owner else f"id={active_owner.user_id}"
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"RDP сервис уже занят пользователем {owner_name}. Попробуйте позже.",
|
||||
)
|
||||
return RedirectResponse(url=session_redirect_url(active_owner), status_code=303)
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
if service.type == ServiceType.WEB and WEB_POOL_SIZE > 0:
|
||||
try:
|
||||
with allocator_lock(db, 91001):
|
||||
ensure_web_pool()
|
||||
slot = acquire_web_pool_slot(db)
|
||||
slot_cid = f"WEBPOOLIDX:{slot}"
|
||||
terminate_active_slot_sessions(db, slot_cid)
|
||||
dispatch_web_pool_target(slot, service)
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=slot_cid,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
).all()
|
||||
active_rows = sorted(active_rows, key=lambda row: row.created_at)
|
||||
active_service_ids = {row.service_id for row in active_rows}
|
||||
_mark("check_limit_ms", t_limit)
|
||||
if service.id not in active_service_ids and len(active_service_ids) >= MAX_ACTIVE_SERVICES_PER_USER:
|
||||
oldest = next((row for row in active_rows if row.service_id != service.id), None)
|
||||
if oldest:
|
||||
t_rotate = time.perf_counter()
|
||||
terminate_session_record(db, oldest, SessionStatus.ROTATED, stop_container=True)
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
logger.exception("web_pool_dispatch_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="web_pool", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
raise HTTPException(status_code=502, detail="WEB runtime failed to switch target")
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="web_pool", slot=slot)
|
||||
audit(db, "SESSION_CREATE_WEB_POOL", f"service={service.slug} session={session_id} slot={slot}", user_id=user.id)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
|
||||
if service_uses_universal_pool(service):
|
||||
try:
|
||||
with allocator_lock(db, 91002):
|
||||
ensure_universal_pool()
|
||||
slot = acquire_universal_slot(db)
|
||||
slot_cid = f"POOLIDX:{slot}"
|
||||
terminate_active_slot_sessions(db, slot_cid)
|
||||
dispatch_universal_target(slot, service)
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
_mark("rotate_oldest_ms", t_rotate)
|
||||
log_event(
|
||||
"session_rotated",
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=slot_cid,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
closed_session_id=oldest.id,
|
||||
closed_service_id=oldest.service_id,
|
||||
new_service_id=service.id,
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
logger.exception("universal_pool_dispatch_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="universal_pool", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
raise HTTPException(status_code=502, detail="Universal runtime failed to switch target")
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="universal_pool", slot=slot)
|
||||
audit(db, "SESSION_CREATE_POOL", f"service={service.slug} session={session_id} slot={slot}", user_id=user.id)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
else:
|
||||
_emit("max_services_redirect")
|
||||
return RedirectResponse(url="/?launch_error=max_services", status_code=303)
|
||||
|
||||
if service.type == ServiceType.WEB and desired_pool_size(service) > 0:
|
||||
ensure_warm_pool(service)
|
||||
open_warm_web_url(service, service.target)
|
||||
if service.type == ServiceType.RDP:
|
||||
t_rdp_owner = time.perf_counter()
|
||||
active_owner = find_active_session_for_service(db, service.id)
|
||||
_mark("check_rdp_owner_ms", t_rdp_owner)
|
||||
if active_owner:
|
||||
if active_owner.user_id != user.id:
|
||||
owner = db.get(User, active_owner.user_id)
|
||||
owner_name = owner.username if owner else f"id={active_owner.user_id}"
|
||||
_emit("rdp_busy", owner=owner_name)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"RDP сервис уже занят пользователем {owner_name}. Попробуйте позже.",
|
||||
)
|
||||
_emit("reuse_rdp_session", session_id=active_owner.id)
|
||||
return RedirectResponse(url=session_redirect_url(active_owner), status_code=303)
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
if service.type == ServiceType.WEB and WEB_POOL_SIZE > 0:
|
||||
try:
|
||||
t_pool_lock = time.perf_counter()
|
||||
with allocator_lock(db, 91001, timeout_seconds=GO_POOL_LOCK_TIMEOUT_SECONDS):
|
||||
_mark("wait_web_pool_lock_ms", t_pool_lock)
|
||||
t_ensure = time.perf_counter()
|
||||
ensure_web_pool()
|
||||
_mark("ensure_web_pool_ms", t_ensure)
|
||||
|
||||
t_acquire = time.perf_counter()
|
||||
slot = acquire_web_pool_slot(db)
|
||||
_mark("acquire_web_slot_ms", t_acquire)
|
||||
slot_cid = f"WEBPOOLIDX:{slot}"
|
||||
|
||||
t_dispatch = time.perf_counter()
|
||||
terminate_active_slot_sessions(db, slot_cid)
|
||||
dispatch_web_pool_target(slot, service)
|
||||
_mark("dispatch_web_target_ms", t_dispatch)
|
||||
|
||||
t_commit = time.perf_counter()
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=slot_cid,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
_mark("db_commit_ms", t_commit)
|
||||
except LockTimeoutError:
|
||||
_emit("web_pool_lock_timeout")
|
||||
raise HTTPException(status_code=503, detail="Пул WEB занят. Повторите через несколько секунд.")
|
||||
except Exception as exc:
|
||||
logger.exception("web_pool_dispatch_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="web_pool", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
_emit("web_pool_create_failed", error=str(exc))
|
||||
raise HTTPException(status_code=502, detail="WEB runtime failed to switch target")
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="web_pool", slot=slot)
|
||||
audit(db, "SESSION_CREATE_WEB_POOL", f"service={service.slug} session={session_id} slot={slot}", user_id=user.id)
|
||||
_emit("session_created_web_pool", session_id=session_id, slot=slot)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
|
||||
if service_uses_universal_pool(service):
|
||||
try:
|
||||
t_pool_lock = time.perf_counter()
|
||||
with allocator_lock(db, 91002, timeout_seconds=GO_POOL_LOCK_TIMEOUT_SECONDS):
|
||||
_mark("wait_universal_pool_lock_ms", t_pool_lock)
|
||||
t_ensure = time.perf_counter()
|
||||
ensure_universal_pool()
|
||||
_mark("ensure_universal_pool_ms", t_ensure)
|
||||
|
||||
t_acquire = time.perf_counter()
|
||||
slot = acquire_universal_slot(db)
|
||||
_mark("acquire_universal_slot_ms", t_acquire)
|
||||
slot_cid = f"POOLIDX:{slot}"
|
||||
|
||||
t_dispatch = time.perf_counter()
|
||||
terminate_active_slot_sessions(db, slot_cid)
|
||||
dispatch_universal_target(slot, service)
|
||||
_mark("dispatch_universal_target_ms", t_dispatch)
|
||||
|
||||
t_commit = time.perf_counter()
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=slot_cid,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
_mark("db_commit_ms", t_commit)
|
||||
except LockTimeoutError:
|
||||
_emit("universal_pool_lock_timeout")
|
||||
raise HTTPException(status_code=503, detail="Пул RDP занят. Повторите через несколько секунд.")
|
||||
except Exception as exc:
|
||||
logger.exception("universal_pool_dispatch_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="universal_pool", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
_emit("universal_pool_create_failed", error=str(exc))
|
||||
raise HTTPException(status_code=502, detail="Universal runtime failed to switch target")
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="universal_pool", slot=slot)
|
||||
audit(db, "SESSION_CREATE_POOL", f"service={service.slug} session={session_id} slot={slot}", user_id=user.id)
|
||||
_emit("session_created_universal_pool", session_id=session_id, slot=slot)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
|
||||
if service.type == ServiceType.WEB and desired_pool_size(service) > 0:
|
||||
t_warm = time.perf_counter()
|
||||
ensure_warm_pool(service)
|
||||
open_warm_web_url(service, service.target)
|
||||
_mark("warm_pool_prepare_ms", t_warm)
|
||||
|
||||
t_commit = time.perf_counter()
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=f"POOL:{service.slug}",
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
_mark("db_commit_ms", t_commit)
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="warm_pool")
|
||||
audit(db, "SESSION_CREATE_POOL", f"service={service.slug} session={session_id}", user_id=user.id)
|
||||
_emit("session_created_warm_pool", session_id=session_id)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
|
||||
try:
|
||||
t_create = time.perf_counter()
|
||||
container_id = create_runtime_container(service, session_id)
|
||||
_mark("create_runtime_container_ms", t_create)
|
||||
except Exception as exc:
|
||||
logger.exception("session_container_create_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="single_runtime", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
_emit("single_runtime_create_failed", error=str(exc))
|
||||
raise HTTPException(status_code=502, detail="Session runtime failed to start")
|
||||
|
||||
t_commit = time.perf_counter()
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=f"POOL:{service.slug}",
|
||||
container_id=container_id,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="warm_pool")
|
||||
audit(db, "SESSION_CREATE_POOL", f"service={service.slug} session={session_id}", user_id=user.id)
|
||||
_mark("db_commit_ms", t_commit)
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="single_runtime", container_id=container_id)
|
||||
|
||||
audit(db, "SESSION_CREATE", f"service={service.slug} session={session_id}", user_id=user.id)
|
||||
t_wait = time.perf_counter()
|
||||
ready = wait_for_session_route(session_id)
|
||||
_mark("wait_session_route_ms", t_wait)
|
||||
log_event("session_route_ready", session_id=session_id, ready=ready)
|
||||
_emit("session_created_single_runtime", session_id=session_id, ready=ready)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
|
||||
try:
|
||||
container_id = create_runtime_container(service, session_id)
|
||||
except Exception as exc:
|
||||
logger.exception("session_container_create_failed slug=%s user_id=%s", slug, user.id)
|
||||
log_event("session_create_failed", level=logging.ERROR, user_id=user.id, service_slug=slug, mode="single_runtime", error=str(exc))
|
||||
audit(db, "SESSION_CREATE_FAILED", f"slug={slug} err={str(exc)}", user_id=user.id)
|
||||
raise HTTPException(status_code=502, detail="Session runtime failed to start")
|
||||
|
||||
session_obj = SessionModel(
|
||||
id=session_id,
|
||||
user_id=user.id,
|
||||
service_id=service.id,
|
||||
container_id=container_id,
|
||||
status=SessionStatus.ACTIVE,
|
||||
created_at=now_utc(),
|
||||
last_access_at=now_utc(),
|
||||
)
|
||||
db.add(session_obj)
|
||||
db.commit()
|
||||
log_event("session_created", user_id=user.id, service_slug=service.slug, session_id=session_id, mode="single_runtime", container_id=container_id)
|
||||
|
||||
audit(db, "SESSION_CREATE", f"service={service.slug} session={session_id}", user_id=user.id)
|
||||
ready = wait_for_session_route(session_id)
|
||||
log_event("session_route_ready", session_id=session_id, ready=ready)
|
||||
return RedirectResponse(url=f"/s/{session_id}/", status_code=303)
|
||||
except LockTimeoutError:
|
||||
_emit("user_lock_timeout")
|
||||
raise HTTPException(status_code=429, detail="Слишком много параллельных запусков. Повторите через несколько секунд.")
|
||||
|
||||
|
||||
@app.get("/svc/{slug}/", response_class=HTMLResponse)
|
||||
|
||||
Reference in New Issue
Block a user