feat(crawler): schedule periodic re-validation for fan-out child publishers (#4850)#4873
Merged
Merged
Conversation
…ishers (#4850) recordChildPublisherFromManager (PR #4840) created publishers rows for each child synthesized from a manager file's publisher_properties[] selector but never scheduled them for refresh. A manager-asserted child stamped last_validated=NOW() at fan-out time stayed at that timestamp indefinitely. Fix: also enqueue the child into manager_revalidation_queue (existing infra from migration 471) with next_attempt_after = NOW + 24h. The existing worker drains at 50/5min, so 6,800 cafemedia children spread across ~12h after first becoming due. ON CONFLICT DO NOTHING preserves any existing backoff state — re-enqueue from a later fan-out doesn't reset a child that 404'd recently. What the worker does per child: crawlSingleDomain(child) runs the full discovery cascade — tries the child's own /.well-known first, falls back to ads.txt MANAGERDOMAIN. Children with their own file get promoted to discovery_method='direct' (gives the divergence-detector use case real data). 404s record failed-fetch and the row deletes; re-enqueue happens on next cafemedia rotation. Tests pin: 24h initial delay; idempotent re-enqueue preserves backoff; self-attribute case skips both publisher and queue writes. Resolves #4850.
4 tasks
Contributor
There was a problem hiding this comment.
LGTM. Follow-ups noted below. Right shape — reuses migration 471's queue rather than spinning a new table, and the 24h initial delay handles the cafemedia thundering-herd at the right layer.
Things I checked
- Column list on the new INSERT (
server/src/db/publisher-db.ts:281) matchesenqueueManagerRevalidationat:542exactly —(publisher_domain, manager_domain, enqueued_at, next_attempt_after, attempts, last_attempted_at, last_error). - Migration 471 declares
publisher_domain TEXT PRIMARY KEY, soON CONFLICT (publisher_domain) DO NOTHINGis well-defined against the canonicalized string being passed. - Idempotency test at
crawler-publisher-properties-fanout.test.ts:126exercises the DO NOTHING path correctly — seeds NOW+3d/attempts=2, re-calls, asserts state preserved. - Self-attribute early return at
publisher-db.ts:248skips both writes; the queue-side assertion lives in the separate test at:159(count = 0 for MANAGER == MANAGER). - Caller wired at
server/src/crawler.ts:1078insidefanOutPublisherPropertiesAuthorizations, with manager-self drop already applied one line earlier — defense-in-depth, both layers reject self-attribute. - Empty-frontmatter changeset matches repo convention (other
.changeset/*.mdfiles do the same). Not a missing-changeset block. - No tenant/auth surface —
publishersandmanager_revalidation_queueare global registry tables.
Follow-ups (non-blocking — file as issues)
- Wrap the two INSERTs in
BEGIN/COMMIT.publisher-db.ts:251-285shares agetClient()handle across both INSERTs but never opens a transaction, unlikeupsertAdagentsCacheat:325. If the queue INSERT fails after the publishers INSERT commits (connection drop mid-write), the child row lands with no refresh schedule. Self-healing on the next fan-out (the queue INSERT retries via DO NOTHING and the publishers ON CONFLICT branch stays correct), so this isn't load-bearing — but matching the file's own transaction convention removes the orphan window entirely. - Extend
clearFixturesto covermanager_revalidation_queue.crawler-publisher-properties-fanout.test.ts:43cleanspublishersandagent_publisher_authorizationsbut not the queue. The new tests at:123and:156rely on inlineDELETEfor cleanup — if either throws before that line, subsequent test runs against the same DB inherit a stale row that fails theattempts: 0assertion. - Post-deploy probe. The test plan's unchecked item — confirming ~6,800 cafemedia children land with
next_attempt_afterclustered 24h out — is the right validation to actually run once this ships. Worth a calendar reminder, not a merge blocker.
Approving.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves #4850.
recordChildPublisherFromManager(PR #4840) createdpublishersrows for fan-out children but never scheduled them for refresh — a manager-asserted child stampedlast_validated = NOW()at fan-out time stayed at that timestamp indefinitely. This wires children into the existingmanager_revalidation_queueworker.Fix
Two-line addition to
recordChildPublisherFromManager:ON CONFLICT DO NOTHING— preserves existing backoff (idempotent re-enqueue from subsequent fan-outs doesn't reset)manager_revalidation_queuefrom migration 471) — same shape, same worker, samecrawlSingleDomainactionWhat the worker does per child
crawlSingleDomain(child)runs the full discovery cascade:/.well-known/adagents.json(direct). If present, the child gets promoted todiscovery_method='direct'with the cached blob. The divergence detector (filed in SDK companion tickets adcp-client-python#749 part 3, etc.) now has real data to compare manager's inline claim against child's own declaration.ads.txt MANAGERDOMAIN. Child's row stays as-is, queue row deletes.recordFailedAdagentsFetchupdateslast_http_status. Queue row deletes — re-enqueue happens on next cafemedia rotation, no infinite loop.Out of scope
markRevalidationFailedschedule (1h / 6h / 24h / 72h).Test plan
manager_revalidation_queueafter a cafemedia crawl, confirm ~6,800 rows withnext_attempt_afterclustered ~24h out🤖 Generated with Claude Code