โš™๏ธ Deep Dive

Discovery โ€” Under the Hood

Statement-count economics, the fingerprint's collision geometry, and the provider-poisoning bug that shipped and got fixed.

What a run costs, statement by statement

Ingestion (ingestConnectorRun, packages/db/src/discovery.ts:180) processes items sequentially. Per parsed item: company upsert, fingerprint lookup, posting insert-or-update, source-item upsert, signal insert โ€” ~5 statements, each a full HTTP round trip on the Neon driver.

OperationCostWhy
Fetch k list URLsO(max latency), parallelPromise.allSettled โ€” independent sources, one failure doesn't kill the rest
Fingerprint one itemO(len) + SHA-256Regex normalization passes over short strings โ€” microseconds
Dedup lookupO(log n)job_postings_user_fingerprint_idx, one probe per item
Ingest n itemsO(n) ยท ~5 stmts ยท RTT500 items โ‰ˆ 2,500 round trips โ‰ˆ 40โ€“75 s at 15โ€“30 ms RTT โ€” hence the 300 s task budget and the caps
Apify actor runโ‰ค120 s, 1 s pollsBounded polling with explicit terminal states; slow actors time out cleanly
Instagram captions+1 AI call per postSerial extraction; 15 posts โ‰ˆ 15 boundary-guarded model calls โ€” why the task's maxDuration is 300 s
Progress flush1 update / 50 itemsAmortized honesty: the sources screen never shows 0/0/0 during a long run

The sequential per-item loop is the obvious optimization target (batch the inserts, or pipeline over a pooled connection) โ€” and the code chose caps instead. That's a legitimate engineering answer: when the bound is what matters, limiting n is cheaper and safer than making each n faster. Batching changes failure semantics too โ€” a batch insert that dies mid-way is harder to reason about than "we stopped cleanly at item 350 of 500."

The decision: content fingerprints with badge-on-disagreement

Chosen
SHA-256 of normalized (company, title, location, season)

Deterministic, explainable, testable โ€” the unit tests enumerate exact collision cases.

Survives reposts under new URLs (fingerprint matches, freshness resets, history grows).

Sensitive to title phrasing: "SWE" vs "Software Engineering" won't collide โ€” missed dedup by design.

Alternative
Apply-URL equality

Trivial, zero false merges.

Fails on the common case: the same job on three boards has three URLs; reposts get new URLs weekly.

Alternative
Embedding similarity

Catches paraphrases the hash misses.

A threshold to tune forever, per-item model cost, and an unexplainable merge when it's wrong โ€” the worst property for a "trust your discovery" screen.

The geometry to internalize: normalization defines equivalence classes. Every stripped token (seasons, "Intern", "Inc") widens the class โ€” more true merges, more risk of false ones. The recipe strips exactly the tokens that vary across listings of the same job and keeps everything that distinguishes actual roles. When a class is wrong in the dangerous direction (same fingerprint, different apply URL or track), hasDuplicateDisagreement refuses to merge and raises a badge instead. Dedup keys are product decisions wearing a hash function's clothes.

Also chosen: the raw/normalized split. source_items keeps what the source said; job_postings keeps what Wera concluded. Ingestion is thus re-runnable and auditable โ€” the same principle as event logs and staging tables in data engineering: never overwrite your inputs with your conclusions.

A shipped bug, dissected โ€” plus the races that remain

โš ๏ธ
The provider-poisoning bug (fixed in 4425c79)

ingestConnectorRun upserts a registry row for the connector โ€” and its conflict clause overwrote provider with the connector's internal string (github-public-list). But saved sources are dispatched by provider, and github-public-list isn't a dispatchable provider. Net effect: running a saved source once removed it from all future scheduled runs. The fix threads the saved row's provider through ingestion (see the comment at source-ingestion.ts:115). The lesson has a name: two meanings, one table โ€” registry rows (created by runs) and config rows (created by users) shared source_connectors, and an upsert written for one meaning clobbered the other. When a table serves two masters, every conflict clause is a place they can fight.

  • The running-run lock is advisory: before starting, ingestion fails any running run older than 15 minutes, then rejects if one is still active. But the check and the insert aren't atomic โ€” two simultaneous manual runs can slip through the window. A SELECT โ€ฆ FOR UPDATE or a partial unique index on (user_id, connector_key) WHERE status = 'running' would close it; at one user the race needs two of your own tabs. Noticing that it's a check-then-act is the skill.
  • Crash mid-ingest: the catch block marks the run failed with the error โ€” but a hard process kill (OOM, platform eviction) leaves running forever. That's what the 15-minute stale sweep at the start of the next run is for: self-healing state machines beat cron janitors you have to remember to build.
  • One bad timestamp โ‰  one dead query: postedAt lives in jsonb; casting it in SQL (::timestamptz) would abort the whole inbox query on the first malformed value. The code reads it as text and parses per-row in JS (discovery.ts:582) โ€” blast-radius engineering at the column level.
  • Partial success is a status, not a lie: runStatus maps (error โˆง zero parsed) โ†’ failed, (error โˆจ failures) โ†’ partial, else completed โ€” and the UI renders partial as a warning, never a green check. Honest statuses are a failure-mode defense: they keep humans in the loop while numbers drift.

Make it yours

๐Ÿ›  Exercise 1
Write the RSS connector for real

Implement the university-feed connector from the Overview quiz: inputSchema (feed URL), rawItemSchema (one entry), run() with fetchWithTimeout, normalize(). Feed it a deliberately malformed fixture and assert the batch survives with one quarantined item.

Hint

Copy the Greenhouse connector's shape. The test you want already exists as a pattern: look at how discovery tests use a "Broken Fixture" row.

๐Ÿ›  Exercise 2
Close the running-run race

Design the partial unique index that makes "one running run per connector per user" a database guarantee, and rewrite the start-of-run logic to rely on the constraint instead of the check. What happens to the stale-sweep logic โ€” still needed?

Hint

CREATE UNIQUE INDEX โ€ฆ ON source_runs (user_id, connector_key) WHERE status = 'running'. The insert now fails atomically โ€” but yes, you still need the sweep, because a crashed run holds the "lock" forever otherwise. Constraints prevent races; sweeps heal crashes. Different jobs.

๐Ÿ›  Exercise 3
Measure your own fingerprint recipe

Take 50 real postings from two job boards (or the repo's test fixtures) and compute pairwise fingerprint collisions. Build the confusion matrix: true merges, missed merges, false merges. Now change one normalization rule (e.g., stop stripping locations' "US" variants) and re-measure. You're doing precision/recall tuning on a hash function.

Hint

Every normalization rule trades recall (more merges) against precision (fewer wrong merges). The badge mechanism is what lets the recipe run at less-than-perfect precision safely.

๐Ÿ“–
Dead Letter Channel โ€” Enterprise Integration Patterns

Quarantine's formal ancestor: where messages that can't be processed go to be seen, not lost.

๐Ÿ“–
Partial Indexes โ€” PostgreSQL manual

The primitive for Exercise 2 โ€” constraints that apply only to rows in a given state.

๐Ÿ“–
Trigger.dev โ€” scheduled tasks

Cron tasks, maxDuration, and retry semantics โ€” the platform contract the idempotency keys defend against.