Discovery โ Under the Hood
Statement-count economics, the fingerprint's collision geometry, and the provider-poisoning bug that shipped and got fixed.
In this deep dive
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.
| Operation | Cost | Why |
|---|---|---|
| Fetch k list URLs | O(max latency), parallel | Promise.allSettled โ independent sources, one failure doesn't kill the rest |
| Fingerprint one item | O(len) + SHA-256 | Regex normalization passes over short strings โ microseconds |
| Dedup lookup | O(log n) | job_postings_user_fingerprint_idx, one probe per item |
| Ingest n items | O(n) ยท ~5 stmts ยท RTT | 500 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 polls | Bounded polling with explicit terminal states; slow actors time out cleanly |
| Instagram captions | +1 AI call per post | Serial extraction; 15 posts โ 15 boundary-guarded model calls โ why the task's maxDuration is 300 s |
| Progress flush | 1 update / 50 items | Amortized 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
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.
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.
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
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
runningrun 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. ASELECT โฆ FOR UPDATEor 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
failedwith the error โ but a hard process kill (OOM, platform eviction) leavesrunningforever. 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:
postedAtlives 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:
runStatusmaps (error โง zero parsed) โfailed, (error โจ failures) โpartial, elsecompletedโ and the UI renderspartialas 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
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.
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.
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.
Quarantine's formal ancestor: where messages that can't be processed go to be seen, not lost.
The primitive for Exercise 2 โ constraints that apply only to rows in a given state.
Cron tasks, maxDuration, and retry semantics โ the platform contract the idempotency keys defend against.