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
22
33from collections.abc import AsyncIterator
44from contextlib import asynccontextmanager
5import logging
56
67from apscheduler.schedulers.background import BackgroundScheduler
78from fastapi import FastAPI
@@ -21,6 +22,9 @@ from srht_contrib.services.todo import TodoIngestionService
2122from srht_contrib.utils.identity import ActorIdentityResolver
2223
2324
25logger = logging.getLogger(__name__)
26
27
2428def build_poller(settings: Settings) -> PollerService:
2529 todo_client = SourceHutGraphQLClient(settings.todo_srht_endpoint, settings.srht_token)
2630 git_client = SourceHutGraphQLClient(settings.git_srht_endpoint, settings.srht_token)
@@ -68,6 +72,7 @@ def create_app(
6872 replace_existing=True,
6973 )
7074 scheduler.start()
75 _run_startup_poll(app)
7176 app.state.scheduler = scheduler
7277 try:
7378 yield
@@ -96,4 +101,11 @@ def _scheduled_poll(app: FastAPI) -> None:
96101 db.close()
97102
98103
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
99111app = create_app()
tests/test_polling_api.py +31
@@ -19,6 +19,7 @@ class InsertingPoller:
1919 service = type("Service", (), {"client": _Closable()})()
2020 self.todo_service = service
2121 self.git_service = service
22 self.tracked_poll_calls: list[str] = []
2223
2324 def track_actor_request(self, db, actor: str, *, update_last_requested: bool = True):
2425 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor))
@@ -47,6 +48,12 @@ class InsertingPoller:
4748 db.commit()
4849 return 1
4950
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
5057
5158class FailingPoller:
5259 def __init__(self) -> None:
@@ -65,6 +72,9 @@ class FailingPoller:
6572 def poll_all(self, db, actor: str) -> int:
6673 raise SourceHutClientError("boom")
6774
75 def poll_tracked_actors(self, db, default_actor: str | None = None) -> dict[str, int]:
76 raise SourceHutClientError("boom")
77
6878
6979def test_manual_poll_uses_same_database_session(settings: Settings, db_engine, session_factory) -> None:
7080 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
8999
90100 assert response.status_code == 502
91101 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