krz/hutch-stats

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

Commit 1c8d0bdd0a

1c8d0bdd0a0a46d7400b0827aec88a787f5a04c3

parent: 3489797460

Verified · cmc

cmc <hello@cleberg.net> · 2026-04-12 03:56 UTC

Reduce SQLite lock contention during actor enqueue

Layout: unified · split

src/srht_contrib/config.py +1
@@ -37,6 +37,7 @@ class Settings(BaseSettings):
3737 poll_interval_seconds: int = Field(default=900, alias="POLL_INTERVAL_SECONDS")
3838 sync_overlap_hours: int = Field(default=1, alias="SYNC_OVERLAP_HOURS")
3939 srht_request_delay_seconds: float = Field(default=0.5, alias="SRHT_REQUEST_DELAY_SECONDS")
40 sqlite_busy_timeout_seconds: float = Field(default=30.0, alias="SQLITE_BUSY_TIMEOUT_SECONDS")
4041 discovery_batch_size: int = Field(default=5, alias="DISCOVERY_BATCH_SIZE")
4142 indexed_actor_repoll_seconds: int = Field(default=21600, alias="INDEXED_ACTOR_REPOLL_SECONDS")
4243 discovery_error_backoff_seconds: int = Field(default=3600, alias="DISCOVERY_ERROR_BACKOFF_SECONDS")
src/srht_contrib/db.py +20 −3
@@ -3,7 +3,7 @@ from __future__ import annotations
33from collections.abc import Generator
44
55from fastapi import HTTPException, Request, status
6from sqlalchemy import Engine, create_engine, text
6from sqlalchemy import Engine, create_engine, event, text
77from sqlalchemy.pool import StaticPool
88from sqlalchemy.orm import Session, declarative_base, sessionmaker
99
@@ -13,11 +13,28 @@ Base = declarative_base()
1313
1414
1515def make_engine(settings: Settings) -> Engine:
16 connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}
16 connect_args = {}
17 if settings.database_url.startswith("sqlite"):
18 connect_args = {
19 "check_same_thread": False,
20 "timeout": settings.sqlite_busy_timeout_seconds,
21 }
1722 engine_kwargs = {"future": True, "connect_args": connect_args}
1823 if settings.database_url in {"sqlite://", "sqlite:///:memory:"}:
1924 engine_kwargs["poolclass"] = StaticPool
20 return create_engine(settings.database_url, **engine_kwargs)
25 engine = create_engine(settings.database_url, **engine_kwargs)
26
27 if settings.database_url.startswith("sqlite"):
28 @event.listens_for(engine, "connect")
29 def _configure_sqlite(dbapi_connection, connection_record) -> None: # type: ignore[unused-ignore]
30 cursor = dbapi_connection.cursor()
31 cursor.execute(f"PRAGMA busy_timeout = {int(settings.sqlite_busy_timeout_seconds * 1000)}")
32 if settings.database_url not in {"sqlite://", "sqlite:///:memory:"}:
33 cursor.execute("PRAGMA journal_mode = WAL")
34 cursor.execute("PRAGMA synchronous = NORMAL")
35 cursor.close()
36
37 return engine
2138
2239
2340def make_session_factory(settings: Settings) -> sessionmaker[Session]:
src/srht_contrib/scripts/enqueue_actors.py +18 −9
@@ -34,6 +34,9 @@ def enqueue_actors(username_file: Path, *, stagger_seconds: int = 300, start_at:
3434 queued_at = start_at or datetime.now(tz=UTC)
3535 inserted = 0
3636
37 batch_size = 250
38 queued_in_batch = 0
39
3740 with session_factory() as db:
3841 for index, actor in enumerate(usernames):
3942 next_poll_after = queued_at + timedelta(seconds=index * stagger_seconds)
@@ -49,15 +52,21 @@ def enqueue_actors(username_file: Path, *, stagger_seconds: int = 300, start_at:
4952 )
5053 db.add(tracked_actor)
5154 inserted += 1
52 continue
53
54 tracked_actor.is_active = True
55 if tracked_actor.queued_for_discovery_at is None:
56 tracked_actor.queued_for_discovery_at = queued_at
57 if tracked_actor.last_polled_at is None and tracked_actor.discovery_state != "indexed":
58 tracked_actor.discovery_state = "queued"
59 tracked_actor.next_poll_after = next_poll_after
60 db.commit()
55 else:
56 tracked_actor.is_active = True
57 if tracked_actor.queued_for_discovery_at is None:
58 tracked_actor.queued_for_discovery_at = queued_at
59 if tracked_actor.last_polled_at is None and tracked_actor.discovery_state != "indexed":
60 tracked_actor.discovery_state = "queued"
61 tracked_actor.next_poll_after = next_poll_after
62
63 queued_in_batch += 1
64 if queued_in_batch >= batch_size:
65 db.commit()
66 queued_in_batch = 0
67
68 if queued_in_batch:
69 db.commit()
6170
6271 return inserted
6372