lct-hack/backend/tests/test_session_lease.py

152 lines
4.6 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.

"""Lease занятий ведёт хаб поверх `SessionStore`, а не код запуска приложения."""
import asyncio
import re
from pathlib import Path
from app.session.hub import LEASE_FENCED_MESSAGE, SessionHub
from app.session.state import now_utc
from app.session.store import MemorySessionStore, SessionLeaseLost
from tests.test_session_checkpoint import dds_state
APP = Path(__file__).resolve().parents[1] / "app"
class LeaseStore(MemorySessionStore):
persistent = True
def __init__(self) -> None:
super().__init__()
self.renewed = []
self.renew_errors: dict = {}
self.expired = []
self.active = []
async def renew(self, session_id):
self.renewed.append(session_id)
error = self.renew_errors.get(session_id)
if error is not None:
raise error
async def claim_expired(self, session_id=None):
claimed, self.expired = self.expired, []
return claimed
async def restore_active(self):
return list(self.active)
_hubs: list[SessionHub] = []
def make_hub(store) -> SessionHub:
item = SessionHub(store=store)
_hubs.append(item)
return item
def run(coro):
"""Такты, запущенные подхватом, гасятся в том же цикле событий."""
async def wrapper():
try:
return await coro
finally:
while _hubs:
await _hubs.pop().shutdown()
return asyncio.run(wrapper())
def test_lost_or_unconfirmed_lease_fences_only_that_session():
store = LeaseStore()
local_hub = make_hub(store)
lost = local_hub.register(dds_state())
broken = local_hub.register(dds_state())
healthy = local_hub.register(dds_state())
ended = local_hub.register(dds_state())
ended.ended_at = now_utc()
store.renew_errors = {
lost.session_id: SessionLeaseLost("fenced"),
broken.session_id: OSError("partition"),
}
with local_hub.trainee(lost.session_id) as trainee:
run(local_hub.maintain_lease())
assert trainee.get_nowait().message == LEASE_FENCED_MESSAGE
assert lost.lease_fenced and broken.lease_fenced
assert not healthy.lease_fenced
assert ended.session_id not in store.renewed, "завершённое занятие lease не держит"
def test_expired_sessions_are_adopted_but_live_owner_is_not_replaced():
store = LeaseStore()
local_hub = make_hub(store)
live = local_hub.register(dds_state())
stale = local_hub.register(dds_state())
stale.lease_fenced = True
live_copy = live.model_copy()
stale_copy = stale.model_copy()
stale_copy.lease_fenced = False
fresh = dds_state()
store.expired = [live_copy, stale_copy, fresh]
async def scenario():
await local_hub.maintain_lease()
return set(local_hub._tickers)
tickers = run(scenario())
assert local_hub.get(live.session_id) is live
assert local_hub.get(stale.session_id) is stale_copy
assert local_hub.get(fresh.session_id) is fresh
assert {stale.session_id, fresh.session_id} <= tickers
def test_restore_registers_active_sessions_with_tickers():
store = LeaseStore()
store.active = [dds_state(), dds_state()]
local_hub = make_hub(store)
async def scenario():
restored = await local_hub.restore()
return restored, set(local_hub._tickers)
restored, tickers = run(scenario())
assert restored == 2
assert {state.session_id for state in store.active} == tickers
assert local_hub.live_count() == 2
def test_volatile_store_restores_nothing():
local_hub = make_hub(MemorySessionStore())
assert run(local_hub.restore()) == 0
def test_save_all_commits_only_live_sessions():
store = LeaseStore()
local_hub = make_hub(store)
live = local_hub.register(dds_state())
local_hub.register(dds_state()).ended_at = now_utc()
local_hub.register(dds_state()).lease_fenced = True
run(local_hub.save_all())
assert [sid for sid, _records in store.commits] == [live.session_id]
def test_live_count_skips_ended_and_fenced():
local_hub = make_hub(MemorySessionStore())
local_hub.register(dds_state())
local_hub.register(dds_state()).ended_at = now_utc()
local_hub.register(dds_state()).lease_fenced = True
assert local_hub.live_count() == 1
def test_application_code_does_not_touch_hub_internals():
offenders = [
f"{path.relative_to(APP)}:{number}"
for path in APP.rglob("*.py")
if path.name != "hub.py"
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
if re.search(r"\bhub\._", line)
]
assert offenders == []