from datetime import timedelta import pytest from django.utils import timezone from apps.sources.models import Source, SourceLease from apps.sources.services.scheduling import ( acquire_source_lease, calculate_failure_backoff_seconds, release_source_lease, ) @pytest.mark.django_db def test_source_lease_blocks_concurrent_workers_and_recovers_when_expired(source): now = timezone.now() lease_a = acquire_source_lease(source_id=source.pk, worker_token="worker-a", now=now) assert lease_a is not None assert acquire_source_lease(source_id=source.pk, worker_token="worker-b", now=now) is None assert release_source_lease(source_id=source.pk, worker_token="worker-a", now=now) lease_b = acquire_source_lease( source_id=source.pk, worker_token="worker-b", now=now + timedelta(seconds=source.minimum_interval_seconds), ) assert lease_b is not None @pytest.mark.django_db def test_source_lease_reuses_expired_lease(db, source): now = timezone.now() SourceLease.objects.create( source=source, token="expired-worker", worker_id="expired-worker", expires_at=now - timedelta(minutes=1), ) lease = acquire_source_lease(source_id=source.pk, worker_token="recovery-worker", now=now) assert lease is not None assert lease.worker_id == "recovery-worker" @pytest.mark.django_db def test_source_lease_allows_origin_concurrency(db): shared_domain = "shared.example.org" source_a = Source.objects.create( name="Source A", source_type=Source.Type.EMPLOYER, base_url="https://shared.example.org/jobs/", domain=shared_domain, status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, max_concurrency=2, ) source_b = Source.objects.create( name="Source B", source_type=Source.Type.RSS, base_url="https://shared.example.org/careers/", domain=shared_domain, status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, max_concurrency=2, ) now = timezone.now() lease_a = acquire_source_lease(source_id=source_a.pk, worker_token="worker-a", now=now) lease_b = acquire_source_lease(source_id=source_b.pk, worker_token="worker-b", now=now) assert lease_a is not None assert lease_b is not None @pytest.mark.django_db def test_failure_backoff_uses_retry_after_and_bounds(): source = Source( name="Temp", source_type=Source.Type.EMPLOYER, base_url="https://retry.example.org/", domain="retry.example.org", status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, ) source.save() backoff = calculate_failure_backoff_seconds(source, failure_count=2, retry_after_seconds=120) assert backoff >= 120 assert backoff <= 3600 + 45