Skip to content

Pipeline Workers

The system runs four long-lived background workers plus a producer that enqueues work. Workers are independent processes consuming from RabbitMQ queues.

Producer

Located in workers/producer.py (generic) or workers/producers/<site>.py (custom).

The producer is responsible for enqueuing URLs to scrape. It connects to the pipeline, creates a job cursor, and publishes URL messages to url.queue.

graph LR
    P[Producer] -->|"generates URLs"| UQ[url.queue]
    UQ --> DW[dedup_worker]

Generic producer — used for simple sites where URLs are template-based. The urls or url_template in the profile defines the target URLs.

Custom producers — for complex sites requiring browser bootstrap sessions, dynamic pagination, or API token capture:

Producer Custom Logic
idealista.py Dynamic pagination, location variants, inline detail follows
g2.py Category-based listing URLs
hermes.py Category navigation
trustpilot.py Business/review page generation
booking_com.py Browser bootstrap → GraphQL token capture
facebook_marketplace.py Browser bootstrap → session cookie capture

Dedup Worker

(workers/dedup_worker.py)

Consumes from url.queue, deduplicates URLs using RedisBloom, and publishes unique URLs to fetch.queue.

# Dedup uses a Bloom filter for memory-efficient set membership
# False positives are possible but rare — trade-off for O(1) check
bloom = BloomDedup(redis_client, "dedup:my_job")
if not await bloom.check(url):
    await bloom.add(url)
    await publish(channel, "fetch.queue", message)

Fetch Worker

(workers/fetch_worker.py)

Consumes from fetch.queue, fetches URLs using the configured fetcher plugin, and publishes raw responses to raw.queue.

Key responsibilities: - Browser pool management — creates and reuses CloakBrowser sessions - Anti-bot evasion — patched Chromium fingerprints, residential proxies, warmup pages, automatic session rotation on block detection - Rate limiting — per-stage rate limiter to avoid triggering rate limits - Retry logic — configurable retry with backoff for transient failures - Block detection — heuristics to detect challenge/block pages and rotate IPs

graph LR
    FQ[fetch.queue] --> FW[fetch_worker]
    FW --> BP[Browser Pool]
    BP -->|"session"| CB[CloakBrowser / wreq]
    CB -->|"HTML/JSON"| RQ[raw.queue]

Two fetch worker replicas run in production, with per-stage concurrency and pool_size automatically divided by the replica count to keep the total browser fleet within configured limits.

Parse Worker

(workers/parse_worker.py)

Consumes from raw.queue, parses HTML/JSON using the configured parser plugin, and publishes structured items to parsed.queue.

graph LR
    RQ[raw.queue] --> PW[parse_worker]
    PW -->|"selectolax / jsonpath"| Items[structured items]
    Items --> PQ[parsed.queue]

The parse worker: 1. Looks up the stage's parser plugin from the profile registry 2. Calls parser.parse(raw_response) to extract structured data 3. Applies field validation (required fields, drop/flag strategy) 4. Handles inline fan-out (single page → multiple items) 5. Publishes each item batch to parsed.queue

Store Worker

(workers/store_worker.py)

Consumes from parsed.queue and persists items to configured storage backends.

graph LR
    PQ[parsed.queue] --> SW[store_worker]
    SW --> S3[Garage S3]
    SW --> PG[PostgreSQL]
    SW --> J[JSON / CSV / Markdown]

Features: - Batch writing — items are buffered and flushed on batch size or idle timeout - Multi-backend — items go to all configured storage targets simultaneously - Dedup at storagesource_url is the primary key in Postgres - Parquet format — S3 storage uses PyArrow for columnar output

Worker Communication Pattern

Workers communicate only through queues. No worker talks to another worker directly:

Producer ──▶ url.queue ──▶ Dedup ──▶ fetch.queue ──▶ Fetch ──▶ raw.queue ──▶ Parse ──▶ parsed.queue ──▶ Store

This means: - Any worker can be restarted independently - Workers can be scaled horizontally (fetch has 2 replicas) - Backpressure is natural — if a downstream worker is slow, its input queue fills up - Dead-letter queue catches messages that can't be processed