feat(us-025): model usage statistics with rolling RPM windows and SQLite persistence

Adds _RollingCounter and _StatsCollector to the tracker: three rolling windows
(hour=60×1min, day=24×1hr, month=30×1day) track request RPMs per model.
GET /v1/stats returns combined local + peer stats. POST /v1/stats/gossip lets
trackers push their local slice for additive merge in hive mode.
SQLite persistence via --stats-db flag; stats survive tracker restarts.
Records a stat for each proxied /v1/chat/completions request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dobromir Popov
2026-06-30 13:37:33 +03:00
parent fff11c4d8b
commit d9110b623b
3 changed files with 333 additions and 0 deletions

View File

@@ -1089,3 +1089,99 @@ def test_two_node_pipeline_via_tracker():
node_a.stop()
node_b.stop()
tracker.stop()
# ---------------------------------------------------------------- US-025 stats
def test_stats_endpoint_returns_empty_models_initially():
tracker = TrackerServer()
port = tracker.start()
try:
resp = _get_json(f"http://127.0.0.1:{port}/v1/stats")
assert "models" in resp
assert isinstance(resp["models"], dict)
finally:
tracker.stop()
def test_stats_rolling_counter_rpm_reflects_recorded_requests():
from meshnet_tracker.server import _RollingCounter
counter = _RollingCounter(num_buckets=60, bucket_seconds=60)
now = 1_000_000.0
for _ in range(60):
counter.record(now)
rpm = counter.rpm(now)
assert rpm == 1.0 # 60 requests over 60 minutes = 1 rpm
def test_stats_collector_records_and_returns_rpms():
from meshnet_tracker.server import _StatsCollector
collector = _StatsCollector()
now = 1_000_000.0
for _ in range(30):
collector.record_request("my-model", now=now)
combined = collector.get_combined_stats(now=now)
assert "my-model" in combined
assert combined["my-model"]["rpm_last_hour"] > 0
assert combined["my-model"]["rpm_last_day"] > 0
assert combined["my-model"]["rpm_last_month"] > 0
def test_stats_collector_merges_peer_rpms_additively():
from meshnet_tracker.server import _StatsCollector
collector = _StatsCollector()
now = 1_000_000.0
collector.record_request("modelX", now=now) # 1 local request in the current minute bucket
peer_rpms = {"modelX": {"rpm_last_hour": 5.0, "rpm_last_day": 3.0, "rpm_last_month": 1.0}}
collector.merge_peer_rpms("http://peer-tracker:8080", peer_rpms)
combined = collector.get_combined_stats(now=now)
assert combined["modelX"]["rpm_last_hour"] > 5.0 # local + peer
assert combined["modelX"]["rpm_last_day"] > 3.0
assert combined["modelX"]["rpm_last_month"] >= 1.0 # local rounds to 0 at 4dp, peer is 1.0
def test_stats_sqlite_persistence_survives_restart(tmp_path):
from meshnet_tracker.server import _StatsCollector
db = str(tmp_path / "stats.db")
now = 1_000_000.0
collector = _StatsCollector(db_path=db)
for _ in range(10):
collector.record_request("persistent-model", now=now)
collector.save_to_db()
collector2 = _StatsCollector(db_path=db)
combined = collector2.get_combined_stats(now=now)
assert "persistent-model" in combined
assert combined["persistent-model"]["rpm_last_hour"] > 0
def test_stats_gossip_endpoint_merges_peer_slice():
tracker = TrackerServer()
port = tracker.start()
try:
peer_payload = {
"tracker_url": "http://peer:9000",
"stats": {
"remote-model": {
"rpm_last_hour": 2.5,
"rpm_last_day": 1.0,
"rpm_last_month": 0.5,
}
},
}
_post_json(f"http://127.0.0.1:{port}/v1/stats/gossip", peer_payload)
combined = _get_json(f"http://127.0.0.1:{port}/v1/stats")
assert "remote-model" in combined["models"]
assert combined["models"]["remote-model"]["rpm_last_hour"] == 2.5
finally:
tracker.stop()