Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions python/apsis/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,16 @@ def count_runs(
)

def get_stats(self):
# Note: deliberately no DB count here. Counting runs in the lookback window
# requires a query over the runs table, which has no index on timestamp and
# stores wide JSON payloads inline; the scan reads GBs and takes tens of
# seconds when the page cache is cold. Since get_stats() runs on the event
# loop, that blocks all of Apsis. Report only in-memory counts, which are
# O(1). Use count_runs() if you need a run count from the DB.
# TODO: expose a total run count that doesn't require a DB query on the
# event loop, e.g. maintained incrementally or sampled off-thread.
return {
"num_runs": len(self.__expected_runs)
+ self.__run_db.count_runs(min_timestamp=self.__min_timestamp),
"num_expected_runs": len(self.__expected_runs),
"num_active_runs": len(self.__active_runs),
"publisher": self.publisher.get_stats(),
}
28 changes: 25 additions & 3 deletions test/unit/test_run_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,36 @@ def test_run_store_finished_run_not_in_memory(tmp_path):
assert run.run_id not in expected_map


def test_run_store_num_runs_no_double_count(tmp_path):
"""get_stats()['num_runs'] must count each physical run once."""
def test_run_store_count_runs_no_double_count(tmp_path):
"""count_runs() must count each physical run once."""
store = _make_store(tmp_path)
run = Run(Instance("job", {}), expected=True)
_schedule(store, run)
_transition(store, run, State.waiting)

assert store.get_stats()["num_runs"] == 1
assert store.count_runs() == 1


def test_run_store_get_stats_does_not_query_db(tmp_path):
"""
get_stats() must not count runs in the DB.

That query scans the runs table, which blocks the event loop for tens of
seconds on a large database. See RunStore.get_stats().
"""
store = _make_store(tmp_path)
run = Run(Instance("job", {}), expected=True)
_schedule(store, run)
_transition(store, run, State.waiting)

def fail(*args, **kwargs):
raise AssertionError("get_stats() must not count runs in the DB")

store._RunStore__run_db.count_runs = fail

stats = store.get_stats()
assert stats["num_active_runs"] == 1
assert "num_runs" not in stats


def test_run_store_query_since_filters_expected(tmp_path):
Expand Down
Loading