krz/hutch-stats

Server-side utility for calculating contributions for sourcehut users. contributions server sourcehut stats

Commit e2a9508577

e2a9508577f3c0d69dccad0c098a6da7fd95a363

parent: 9d88cda282

Verified · cmc

cmc <hello@cleberg.net> · 2026-04-11 19:23 UTC

feat: run an initial poll when scheduler starts

Layout: unified · split

src/srht_contrib/main.py +12
@@ -2,6 +2,7 @@ from __future__ import annotations
2 2
3from collections.abc import AsyncIterator 3from collections.abc import AsyncIterator
4from contextlib import asynccontextmanager 4from contextlib import asynccontextmanager
5import logging
5 6
6from apscheduler.schedulers.background import BackgroundScheduler 7from apscheduler.schedulers.background import BackgroundScheduler
7from fastapi import FastAPI 8from fastapi import FastAPI
@@ -21,6 +22,9 @@ from srht_contrib.services.todo import TodoIngestionService
21from srht_contrib.utils.identity import ActorIdentityResolver 22from srht_contrib.utils.identity import ActorIdentityResolver
22 23
23 24
25logger = logging.getLogger(__name__)
26
27
24def build_poller(settings: Settings) -> PollerService: 28def build_poller(settings: Settings) -> PollerService:
25 todo_client = SourceHutGraphQLClient(settings.todo_srht_endpoint, settings.srht_token) 29 todo_client = SourceHutGraphQLClient(settings.todo_srht_endpoint, settings.srht_token)
26 git_client = SourceHutGraphQLClient(settings.git_srht_endpoint, settings.srht_token) 30 git_client = SourceHutGraphQLClient(settings.git_srht_endpoint, settings.srht_token)
@@ -68,6 +72,7 @@ def create_app(
68 replace_existing=True, 72 replace_existing=True,
69 ) 73 )
70 scheduler.start() 74 scheduler.start()
75 _run_startup_poll(app)
71 app.state.scheduler = scheduler 76 app.state.scheduler = scheduler
72 try: 77 try:
73 yield 78 yield
@@ -96,4 +101,11 @@ def _scheduled_poll(app: FastAPI) -> None:
96 db.close() 101 db.close()
97 102
98 103
104def _run_startup_poll(app: FastAPI) -> None:
105 try:
106 _scheduled_poll(app)
107 except Exception:
108 logger.exception("Initial scheduled poll failed during application startup")
109
110
99app = create_app() 111app = create_app()
tests/test_polling_api.py +31
@@ -19,6 +19,7 @@ class InsertingPoller:
19 service = type("Service", (), {"client": _Closable()})() 19 service = type("Service", (), {"client": _Closable()})()
20 self.todo_service = service 20 self.todo_service = service
21 self.git_service = service 21 self.git_service = service
22 self.tracked_poll_calls: list[str] = []
22 23
23 def track_actor_request(self, db, actor: str, *, update_last_requested: bool = True): 24 def track_actor_request(self, db, actor: str, *, update_last_requested: bool = True):
24 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor)) 25 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor))
@@ -47,6 +48,12 @@ class InsertingPoller:
47 db.commit() 48 db.commit()
48 return 1 49 return 1
49 50
51 def poll_tracked_actors(self, db, default_actor: str | None = None) -> dict[str, int]:
52 if default_actor is not None:
53 self.tracked_poll_calls.append(default_actor)
54 return {default_actor: self.poll_all(db, default_actor)}
55 return {}
56
50 57
51class FailingPoller: 58class FailingPoller:
52 def __init__(self) -> None: 59 def __init__(self) -> None:
@@ -65,6 +72,9 @@ class FailingPoller:
65 def poll_all(self, db, actor: str) -> int: 72 def poll_all(self, db, actor: str) -> int:
66 raise SourceHutClientError("boom") 73 raise SourceHutClientError("boom")
67 74
75 def poll_tracked_actors(self, db, default_actor: str | None = None) -> dict[str, int]:
76 raise SourceHutClientError("boom")
77
68 78
69def test_manual_poll_uses_same_database_session(settings: Settings, db_engine, session_factory) -> None: 79def test_manual_poll_uses_same_database_session(settings: Settings, db_engine, session_factory) -> None:
70 app = create_app(settings, engine=db_engine, session_factory=session_factory, poller=InsertingPoller()) 80 app = create_app(settings, engine=db_engine, session_factory=session_factory, poller=InsertingPoller())
@@ -89,3 +99,24 @@ def test_manual_poll_maps_sourcehut_failures_to_502(settings: Settings, db_engin
89 99
90 assert response.status_code == 502 100 assert response.status_code == 502
91 assert "SourceHut polling failed" in response.json()["detail"] 101 assert "SourceHut polling failed" in response.json()["detail"]
102
103
104def test_scheduler_runs_initial_poll_on_startup(settings: Settings, db_engine, session_factory) -> None:
105 scheduler_settings = settings.model_copy(update={"enable_scheduler": True})
106 poller = InsertingPoller()
107 app = create_app(scheduler_settings, engine=db_engine, session_factory=session_factory, poller=poller)
108
109 with TestClient(app):
110 pass
111
112 assert poller.tracked_poll_calls == ["~ccleberg"]
113
114
115def test_startup_poll_failure_does_not_block_app_start(settings: Settings, db_engine, session_factory) -> None:
116 scheduler_settings = settings.model_copy(update={"enable_scheduler": True})
117 app = create_app(scheduler_settings, engine=db_engine, session_factory=session_factory, poller=FailingPoller())
118
119 with TestClient(app) as client:
120 response = client.get("/health")
121
122 assert response.status_code == 200