diff options
| author | Christian Cleberg <[email protected]> | 2026-04-11 22:48:38 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-11 22:48:38 -0500 |
| commit | e27bbe17fd5d8f06fc3ccb4a8de97a0775a1439c (patch) | |
| tree | 7ce854d65009decbe8631175f233f3b2c9df6373 /tests | |
| parent | 533866679755bd6e7a97cfa0f050eaa832b0b373 (diff) | |
| download | hutch-stats-e27bbe17fd5d8f06fc3ccb4a8de97a0775a1439c.tar.gz hutch-stats-e27bbe17fd5d8f06fc3ccb4a8de97a0775a1439c.tar.bz2 hutch-stats-e27bbe17fd5d8f06fc3ccb4a8de97a0775a1439c.zip | |
add durable actor queueing and staggered discovery imports
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_ingestion.py | 93 | ||||
| -rw-r--r-- | tests/test_migrations.py | 2 |
2 files changed, 90 insertions, 5 deletions
diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py index 53848e7..974dac6 100644 --- a/tests/test_ingestion.py +++ b/tests/test_ingestion.py @@ -1,10 +1,12 @@ -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta +from pathlib import Path from sqlalchemy import select from srht_contrib.config import Settings from srht_contrib.jobs.poller import PollerService from srht_contrib.models import ContributionEvent, ServiceBackfillState, SyncState, TrackedActor, TrackedRepository +from srht_contrib.scripts.enqueue_actors import enqueue_actors from srht_contrib.schemas import NormalizedEvent from srht_contrib.services.git import GitIngestionService, GitPollResult from srht_contrib.services.todo import TodoIngestionService, TodoPollResult @@ -64,7 +66,7 @@ class EmptyGitService: POLL_INTERVAL_SECONDS=60, ) - def fetch_recent_events(self, actor: str, since: datetime | None = None, repositories=None) -> GitPollResult: + def fetch_recent_events(self, actor: str, since: datetime | None = None, repositories=None, db=None) -> GitPollResult: return GitPollResult(events=[], cursor="2026-03-31T00:00:00+00:00") def fetch_backfill_batch(self, actor: str, cursor_state: dict | None = None) -> BackfillBatchResult: @@ -156,7 +158,7 @@ class QueueShrinkingGitService: POLL_INTERVAL_SECONDS=60, ) - def fetch_recent_events(self, actor: str, since: datetime | None = None, repositories=None) -> GitPollResult: + def fetch_recent_events(self, actor: str, since: datetime | None = None, repositories=None, db=None) -> GitPollResult: return GitPollResult(events=[], cursor=datetime(2026, 3, 31, tzinfo=UTC).isoformat()) def fetch_backfill_batch(self, actor: str, cursor_state: dict | None = None) -> BackfillBatchResult: @@ -530,7 +532,7 @@ def test_sync_overlap_reuses_cursor_window_and_suppresses_duplicates(db_session) assert second_inserted == 0 assert state is not None assert len(todo_service.calls) == 2 - assert todo_service.calls[1].isoformat() == "2026-03-30T00:00:00+00:00" + assert todo_service.calls[1].isoformat() == "2026-03-30T23:00:00+00:00" def test_scheduled_poll_polls_known_actors_and_seeds_default_actor(db_session) -> None: @@ -556,7 +558,7 @@ def test_scheduled_poll_polls_known_actors_and_seeds_default_actor(db_session) - tracked_actors = db_session.scalars(select(TrackedActor).order_by(TrackedActor.actor)).all() - assert results == {"~default": 0, "~known": 1} + assert results == {"~default": 1, "~known": 0} assert [actor.actor for actor in tracked_actors] == ["~default", "~known"] assert all(actor.last_poll_status == "indexed" for actor in tracked_actors) assert all(actor.last_polled_at is not None for actor in tracked_actors) @@ -655,3 +657,84 @@ def test_prune_old_events_removes_data_older_than_one_year(db_session) -> None: assert deleted == 1 assert remaining == ["todo:recent"] + + +def test_poll_tracked_actors_limits_to_due_batch_size(db_session) -> None: + settings = make_settings(DISCOVERY_BATCH_SIZE=2, INDEXED_ACTOR_REPOLL_SECONDS=3600) + todo_service = RecordingTodoService(events_by_call=[[], []]) + git_service = EmptyGitService() + poller = PollerService(todo_service=todo_service, git_service=git_service, settings=settings) + + now = datetime.now(tz=UTC) + db_session.add_all( + [ + TrackedActor( + actor="~a", + is_active=True, + discovery_state="queued", + queued_for_discovery_at=now - timedelta(minutes=3), + next_poll_after=now - timedelta(minutes=3), + recent_backfill_status="completed", + ), + TrackedActor( + actor="~b", + is_active=True, + discovery_state="queued", + queued_for_discovery_at=now - timedelta(minutes=2), + next_poll_after=now - timedelta(minutes=2), + recent_backfill_status="completed", + ), + TrackedActor( + actor="~c", + is_active=True, + discovery_state="queued", + queued_for_discovery_at=now - timedelta(minutes=1), + next_poll_after=now - timedelta(minutes=1), + recent_backfill_status="completed", + ), + ] + ) + db_session.commit() + + results = poller.poll_tracked_actors(db_session) + + assert set(results) == {"~a", "~b"} + actors = { + actor.actor: actor + for actor in db_session.scalars(select(TrackedActor).order_by(TrackedActor.actor)).all() + } + assert actors["~a"].discovery_state == "indexed" + assert actors["~b"].discovery_state == "indexed" + assert actors["~c"].discovery_state == "queued" + assert actors["~a"].poll_attempts == 1 + assert actors["~b"].poll_attempts == 1 + assert actors["~c"].poll_attempts == 0 + + +def test_enqueue_actors_staggers_without_polling(tmp_path, monkeypatch) -> None: + database_path = tmp_path / "enqueue.db" + username_path = tmp_path / "srht_usernames.txt" + username_path.write_text("alice\nbob\nalice\n~carol\n", encoding="utf-8") + monkeypatch.setenv("DATABASE_URL", f"sqlite:///{database_path}") + monkeypatch.setenv("SRHT_TOKEN", "test-token") + monkeypatch.setenv("DEFAULT_ACTOR", "~ccleberg") + + from srht_contrib.db import Base, make_engine, make_session_factory + + settings = Settings() + engine = make_engine(settings) + Base.metadata.create_all(bind=engine) + session_factory = make_session_factory(settings) + queued_at = datetime(2026, 4, 11, 12, 0, tzinfo=UTC) + + inserted = enqueue_actors(Path(username_path), stagger_seconds=60, start_at=queued_at) + + with session_factory() as db: + actors = db.scalars(select(TrackedActor).order_by(TrackedActor.actor)).all() + + assert inserted == 3 + assert [actor.actor for actor in actors] == ["~alice", "~bob", "~carol"] + assert all(actor.discovery_state == "queued" for actor in actors) + assert actors[0].next_poll_after == queued_at.replace(tzinfo=None) + assert actors[1].next_poll_after == (queued_at + timedelta(seconds=60)).replace(tzinfo=None) + assert actors[2].next_poll_after == (queued_at + timedelta(seconds=120)).replace(tzinfo=None) diff --git a/tests/test_migrations.py b/tests/test_migrations.py index a3091fc..3ce842b 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -111,6 +111,7 @@ def test_alembic_upgrade_adopts_legacy_schema(tmp_path) -> None: inspector = inspect(create_engine(database_url)) columns = {column["name"]: column for column in inspector.get_columns("tracked_repositories")} + tracked_actor_columns = {column["name"] for column in inspector.get_columns("tracked_actors")} unique_constraints = {constraint["name"] for constraint in inspector.get_unique_constraints("tracked_repositories")} with create_engine(database_url).connect() as connection: actor = connection.execute(text("SELECT actor FROM tracked_repositories WHERE id = 1")).scalar_one() @@ -121,6 +122,7 @@ def test_alembic_upgrade_adopts_legacy_schema(tmp_path) -> None: assert "discovered_repositories" in inspector.get_table_names() assert "tracked_actors" in inspector.get_table_names() assert "service_backfill_states" in inspector.get_table_names() + assert {"discovery_state", "queued_for_discovery_at", "next_poll_after", "last_claimed_at", "poll_attempts"} <= tracked_actor_columns def test_alembic_prefers_database_url_from_environment(tmp_path, monkeypatch) -> None: |
