fly-trader / tests /test_supervisor.py
bryceweiner's picture
release 1 (7ab2355a0c) (part 2)
6ca93dc verified
Raw History Blame Contribute Delete
5.61 kB
import threading, time
from fly_trader.ops import supervisor as S
def test_supervisor_thread_lifecycle(monkeypatch, db_conn):
ticks = {"n": 0}
def fake(stop_event=None):
while not stop_event.is_set():
ticks["n"] += 1
time.sleep(0.05)
monkeypatch.setattr(S, "_entry", lambda name: fake)
monkeypatch.setattr(S.Supervisor, "external", lambda self: {})
sup = S.Supervisor()
assert sup.start("discover", started_by="test")
time.sleep(0.3)
assert sup.alive("discover") and ticks["n"] > 2
assert not sup.start("discover") # idempotent while alive
assert sup.stop("discover", grace_s=5)
assert not sup.alive("discover")
st = sup.status()["discover"]
assert st["stopped_at"] is not None and st["error"] is None
def test_supervisor_records_crash(monkeypatch, db_conn):
def boom(stop_event=None):
raise ValueError("kaput")
monkeypatch.setattr(S, "_entry", lambda name: boom)
monkeypatch.setattr(S.Supervisor, "external", lambda self: {})
sup = S.Supervisor()
sup.start("runner", started_by="test")
sup.threads["runner"].join(5)
assert "kaput" in sup.status()["runner"]["error"]
def test_a_previous_consoles_row_with_our_own_pid_is_stale(monkeypatch, db_conn):
"""In a container the console is always pid 1, so the row the last boot left carries this boot's pid. A new console
has registered nothing yet: such a row is stale, and without closing it every restart refused to register."""
import os
from fly_trader.db.connection import transaction
with transaction() as conn:
conn.execute("UPDATE processes SET stopped_at = now() WHERE stopped_at IS NULL AND cmd[1] = 'thread'")
rid = conn.execute("INSERT INTO processes (name, pid, cmd, log_path, started_by) VALUES ('discover', %s, %s, 'logs/discover.log', 'test') RETURNING id",
(os.getpid(), ["thread", "discover"])).fetchone()["id"]
monkeypatch.setattr(S, "_entry", lambda name: (lambda stop_event=None: stop_event.wait(5)))
monkeypatch.setattr(S.Supervisor, "external", lambda self: {})
sup = S.Supervisor()
with transaction() as conn:
row = conn.execute("SELECT stopped_at, exit_code FROM processes WHERE id = %s", (rid,)).fetchone()
assert row["stopped_at"] is not None and row["exit_code"] == -1 # closed at construction, not at start
assert sup.start("discover", started_by="test") # and the worker registers again
sup.stop("discover", grace_s=5)
def test_a_new_console_waits_for_the_one_it_replaces(monkeypatch, db_conn):
"""Killing a console and starting the next one within seconds left the old one still winding its workers down: their
rows looked live, registration was refused, and the market feed never came back. The new console waits for the old
pid to go before judging its rows stale."""
from types import SimpleNamespace
from fly_trader.db.connection import transaction
old_pid = 999_999_001; seen = {"calls": 0}
with transaction() as conn:
conn.execute("UPDATE processes SET stopped_at = now() WHERE stopped_at IS NULL AND cmd[1] = 'thread'")
rid = conn.execute("INSERT INTO processes (name, pid, cmd, log_path, started_by) VALUES ('pumpstream', %s, %s, 'logs/pumpstream.log', 'test') RETURNING id",
(old_pid, ["thread", "pumpstream"])).fetchone()["id"]
import psutil
real = psutil.process_iter
def fake_iter(): # the old console is alive for the first two looks, then gone
seen["calls"] += 1
return list(real()) + ([SimpleNamespace(pid=old_pid)] if seen["calls"] <= 2 else [])
monkeypatch.setattr(psutil, "process_iter", fake_iter); monkeypatch.setattr(S.time, "sleep", lambda s: None)
monkeypatch.setattr(S, "_entry", lambda name: (lambda stop_event=None: stop_event.wait(5)))
monkeypatch.setattr(S.Supervisor, "external", lambda self: {})
sup = S.Supervisor()
with transaction() as conn:
row = conn.execute("SELECT stopped_at FROM processes WHERE id = %s", (rid,)).fetchone()
assert row["stopped_at"] is not None and seen["calls"] >= 3 # waited, saw it go, closed the row
assert sup.start("pumpstream", started_by="test"); sup.stop("pumpstream", grace_s=5)
def test_rh_stream_autostarts_with_an_autostart_row_from_before_robinhood_chain(monkeypatch, db_conn):
"""The shipped autostart names rh_stream; an older install's row does not, and the feed still starts while RH is on."""
import json
from fly_trader import config
started = []
monkeypatch.setattr(S.Supervisor, "start", lambda self, n, started_by="console": started.append(n) or True)
monkeypatch.setattr(S.Supervisor, "alive", lambda self, n: False)
with db_conn.cursor() as cur:
cur.execute("INSERT INTO ui_settings (key, value) VALUES ('autostart', %s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
(json.dumps({"pumpstream": True, "runner": True, "vault": False}),))
db_conn.commit()
try:
monkeypatch.setattr(config, "RH_ENABLED", True)
assert set(S.Supervisor().autostart()) == {"pumpstream", "runner", "rh_stream"}
started.clear(); monkeypatch.setattr(config, "RH_ENABLED", False)
assert set(S.Supervisor().autostart()) == {"pumpstream", "runner"}
finally:
with db_conn.cursor() as cur:
cur.execute("DELETE FROM ui_settings WHERE key = 'autostart'")
db_conn.commit()