Skip to content

feat: the APScheduler jobs (scrape every 8h, discover every 24h) run with no distributed lock β€” if docker-compose scale is used or the scheduler container restarts mid-cycle, duplicate jobs fire simultaneously and create duplicate database recordsΒ #161

Description

@prince-pokharna

πŸ› Problem Statement

Arachnode's scheduler/tasks.py uses APScheduler to trigger:

  • Scrape cycle: every 8 hours
  • Contact discovery: every 24 hours (offset by 4 hours)

The README notes:

"(offset by 4 hours to avoid deadlock throttling)"

This manual offset is a fragile workaround. If:

  • The scheduler container restarts mid-cycle
  • A user accidentally runs two scheduler instances (e.g., during docker compose up --scale scheduler=2)
  • The container restarts exactly when a job is due

...multiple instances of the same job fire simultaneously. Both call the same POST /api/scrape β†’ aggregator endpoint β†’ PostgreSQL write, resulting in duplicate job records even with the deduplication logic.

πŸ’‘ Proposed Fix

Use Redis-based distributed locking via redlock-py to ensure only one instance of each scheduled job runs at a time:

# scheduler/tasks.py

from redis import Redis
from redlock import Redlock

redis_client = Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379)
dlm = Redlock([redis_client])

SCRAPE_LOCK_TTL = 8 * 60 * 60 * 1000  # 8 hours in ms (matches scrape interval)

async def run_scrape_cycle():
    lock = dlm.lock("scheduler:scrape_cycle", SCRAPE_LOCK_TTL)
    if not lock:
        print("[Scheduler] Scrape cycle already running β€” skipping duplicate")
        return

    try:
        print("[Scheduler] Acquired scrape lock β€” starting cycle")
        async with httpx.AsyncClient() as client:
            await client.post(f"{GATEWAY_URL}/api/scrape", json={
                "role": os.environ.get("JOBSEEKER_ROLE", "Backend Engineer"),
                "platforms": ["wellfound", "yc", "remotive"],
            }, timeout=300.0)
    finally:
        dlm.unlock(lock)
        print("[Scheduler] Released scrape lock")

Also add a SCHEDULER_INSTANCE_ID environment variable so logs from multiple instances can be distinguished:

INSTANCE_ID = os.environ.get("SCHEDULER_INSTANCE_ID", "scheduler-0")
print(f"[{INSTANCE_ID}] Starting scheduler...")

πŸ“ Files to Modify

File Change
scheduler/tasks.py Wrap each job with Redis distributed lock
scheduler/requirements.txt Add redlock-py
docker-compose.yml Add SCHEDULER_INSTANCE_ID env var
README.md Document that running multiple scheduler replicas is now safe

Suggested labels: bug, scheduler, reliability

I would like to work on this. Could you please assign it to me?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions