mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-24 03:58:05 +09:00
One shape for process polling loops: Event-based stop (immediate, not sleep-bounded), lock-guarded idempotent start/stop, optional app-context per tick, real logger error handling (the sockets loops reported errors via print() — unreadable in prod), and a run_while predicate for subscriber-scoped loops. The metrics and container-status broadcasters convert; thread_ownership now carries ONE entry for the helper's spawn site instead of a row per hand-rolled thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
1.9 KiB
Python
74 lines
1.9 KiB
Python
"""Plan 77 E5 — BackgroundLoop: Event-stopped, idempotent, error-proof."""
|
|
import threading
|
|
import time
|
|
|
|
from app.utils.background_loop import BackgroundLoop
|
|
|
|
|
|
def test_ticks_and_stops_promptly():
|
|
ticks = []
|
|
loop = BackgroundLoop('t', 0.01, lambda: ticks.append(1))
|
|
assert loop.start() is True
|
|
time.sleep(0.15)
|
|
assert ticks, 'loop never ticked'
|
|
loop.stop(join_timeout=1)
|
|
assert not loop.running
|
|
n = len(ticks)
|
|
time.sleep(0.05)
|
|
assert len(ticks) == n, 'loop kept ticking after stop()'
|
|
|
|
|
|
def test_start_is_idempotent():
|
|
started = threading.Event()
|
|
loop = BackgroundLoop('t2', 0.01, started.set)
|
|
assert loop.start() is True
|
|
assert loop.start() is False # already running
|
|
started.wait(1)
|
|
loop.stop(join_timeout=1)
|
|
|
|
|
|
def test_tick_errors_do_not_kill_the_loop():
|
|
ticks = []
|
|
|
|
def tick():
|
|
ticks.append(1)
|
|
raise RuntimeError('boom')
|
|
|
|
loop = BackgroundLoop('t3', 0.01, tick)
|
|
loop.start()
|
|
time.sleep(0.1)
|
|
loop.stop(join_timeout=1)
|
|
assert len(ticks) >= 2, 'loop died on first tick error'
|
|
|
|
|
|
def test_run_while_predicate_ends_loop():
|
|
alive = {'v': True}
|
|
ticks = []
|
|
loop = BackgroundLoop('t4', 0.01, lambda: ticks.append(1),
|
|
run_while=lambda: alive['v'])
|
|
loop.start()
|
|
time.sleep(0.05)
|
|
alive['v'] = False
|
|
time.sleep(0.1)
|
|
assert not loop.running
|
|
loop.stop()
|
|
|
|
|
|
def test_restart_after_natural_exit():
|
|
"""A loop whose run_while ended can be started again (the sockets
|
|
broadcaster pattern: last subscriber leaves, a new one arrives)."""
|
|
gate = {'v': True}
|
|
ticks = []
|
|
loop = BackgroundLoop('t5', 0.01, lambda: ticks.append(1),
|
|
run_while=lambda: gate['v'])
|
|
loop.start()
|
|
time.sleep(0.05)
|
|
gate['v'] = False
|
|
time.sleep(0.1)
|
|
assert not loop.running
|
|
gate['v'] = True
|
|
assert loop.start() is True
|
|
time.sleep(0.05)
|
|
assert loop.running
|
|
loop.stop(join_timeout=1)
|