Skip to content

perf(crawlers): GraphQL metadata batch + tree-call cache (rate-limit ⑤+⑥b) - #64

Merged
yya007 merged 13 commits into
masterfrom
perf/ratelimit-graphql-tree-cache
Jun 22, 2026
Merged

perf(crawlers): GraphQL metadata batch + tree-call cache (rate-limit ⑤+⑥b)#64
yya007 merged 13 commits into
masterfrom
perf/ratelimit-graphql-tree-cache

Conversation

@yya007

@yya007 yya007 commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Implements rate-limit options ⑤ (GraphQL metadata batch) and ⑥b (tree-call cache) from docs/crawler-rate-limit-strategy.md, building on ①②③ (#63). Reference integration is the topic crawler. Built test-first via subagent-driven development; per-task reviewed plus 8 rounds of codex review.

  • ⑥b — Cache the Trees call. find_skill_md_paths_cached reuses the cached {path: blob_sha} map when a repo's (pushed_at, default_branch) is unchanged, skipping the recursive Trees API call. pushed_at comes free from the ②-cached metadata. This is what makes a warm run approach zero metered calls per unchanged repo. Keyed on default_branch too, since a default-branch change re-resolves HEAD to a different tree without bumping pushed_at; empty/failed lookups are never cached (so a transient Trees failure can't pin a repo to "empty").
  • ⑤ — Batch metadata via GraphQL. fetch_repo_metadata_batch fetches stars/pushedAt/defaultBranch/topics for up to 100 repos in one GraphQL POST (separate 5k-point/hr pool) instead of one REST call each. Uses GraphQL variables (no injection), tolerates partial errors, omits null repos, chunks >100. The topic crawler pre-filters discovered repos through the skip checks, then lazily batches metadata in chunks of 100 as the loop consumes them (so a small --limit doesn't over-fetch), falling back to the per-repo cached REST path for any repo GraphQL omits.

Composition: GraphQL gives pushed_at+default_branch for every repo in one call → feeds ⑥b's tree-skip → feeds ③'s content cache.

④ was dropped (deliberately)

Option ④ (date-filtered incremental topic discovery via pushed:>last-run) was implemented, then removed after codex review found it fundamentally unsound: a repo that adds a skill topic without a new commit keeps its old pushed_at, so pushed:>since filters it out and the advancing watermark makes it permanently undiscoverable — and GitHub search has no "topic-added-since" qualifier to fix it. Topic discovery now searches fully each run (correct; repo-search at 30/min was never the bottleneck — the wins are ⑤/⑥b and the ①②③ caches). ④ returns to the backlog noting incremental topic discovery needs a different mechanism (e.g. enumerate-and-diff, not a date watermark). ⑦/⑧ were also deferred (low/niche).

Verification

  • pytest571 passed, 20 deselected; ruff check crawlers/ tests/crawlers/ clean; tests do no network/disk (verified no data/crawl_state/topic.json leak).
  • TDD throughout. New tests: GraphQL parse/null-omit/partial-errors/non-200/chunking/counting; batch-used-vs-REST-fallback; batch excludes already-covered/filtered repos; lazy-chunk respects --limit; tree-cache hit/miss/stale-pushed_at/changed-default-branch/empty-not-cached.
  • 8 codex-review rounds; all P1/P2 findings resolved (the unfixable one drove the ④ removal).

Risk & rollback

Moderate — touches crawlers/base.py (new helpers, no signature change to existing public fns) and the topic crawler loop. Additive; per-repo REST path remains as fallback. Only the topic crawler is wired. Rollback = revert. No index/runtime/schema change.

🤖 Generated with Claude Code

yya007 and others added 13 commits June 22, 2026 02:25
find_skill_md_paths_cached skips the recursive Trees API call when a
repo's pushed_at is unchanged since it was last walked, reusing the
cached {path: blob_sha} map. Wired through the topic crawler. Makes a
warm run approach zero metered calls per unchanged repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on 4)

In incremental/discover mode, append pushed:>last-run to each topic
search query so re-runs only surface repos changed since the previous
crawl, instead of re-paginating every query in full every time. Last-run
timestamp is persisted via crawl_state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k I/O

run() now always touches crawl_state; mock load/save_crawl_state in the
existing run tests so they no longer write data/crawl_state/topic.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fetch_repo_metadata_batch fetches stars/pushedAt/defaultBranch/topics for
up to 100 repos in one GraphQL POST (separate 5k-point/hr pool) instead of
one REST call each. The topic crawler bulk-fetches discovered repos and
falls back to the per-repo cached REST path for any repo GraphQL omits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A discovery that returns no repos (transient rate-limit / empty search)
must not push last_discovery_at forward, or the next incremental run
would silently skip repos changed in the gap. Guard the crawl_state save
on a non-empty discovery; add a regression test. Also tightens the
discovery-state test to a positive assertion and removes unused mocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n clean sweep

Codex review (P2 x2): (1) the GraphQL metadata batch fetched every
discovered repo before the already-covered/filter-cache skips ran, wasting
quota in CI (--data-dir data/raw skips most). Batch only the to-process set.
(2) advancing last_discovery_at on any non-empty discovery could skip repos
left unprocessed by a --limit truncation or a per-repo failure; advance only
after a complete, clean sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
codex review (P2): _discover_topic_repos swallows a per-query search
RuntimeError and returns a partial list; advancing last_discovery_at then
skips repos from the failed query next run. _discover_topic_repos now
returns (repos, discovery_complete) and the watermark only advances when
discovery completed cleanly (in addition to the existing
not-truncated/not-failed guards).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
codex review: (P1) discover mode date-filters discovery to only new repos
but left resume=False, so write_jsonl truncated the existing topic corpus
to the new subset — enable resume/append for discover like incremental.
(P2) the GraphQL metadata batch eagerly fetched every to_process repo before
the --limit check; fetch lazily in chunks of 100 as the loop consumes repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sweep

codex review: (P1) hitting the 1000-repo discovery cap left
discovery_complete=True, so a partial capped discovery advanced the
watermark and skipped repos beyond the cap — mark a capped result
incomplete. (P3) a *complete* discovery that finds nothing new now advances
the watermark (the discovery_complete flag already gates transient failures),
so quiet periods don't re-scan the same window forever.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rite

codex review: (P2) find_skill_md_paths_cached cached a {} result, but
find_skill_md_paths returns {} for both "no SKILL.md" and a transient Trees
failure — pinning a repo that has skills to empty forever. Only cache
non-empty results (genuine empties are handled by the filter cache).
(P2) the discovery watermark was saved before write_jsonl; a failed/killed
write would advance past unwritten repos. Save it only after a successful write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rtial

codex review: (P2) a timed-out topic search returns HTTP 200 with
incomplete_results=true and a partial page; mark discovery incomplete so the
watermark won't advance past the omitted repos. (P2) --limit hit inside a
repo's inner SKILL.md loop left truncated=False (only the outer loop set it),
so the watermark could advance with that repo's remaining skills unwritten;
set truncated in the inner break too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e cache on default branch

Option 4's pushed:>since filter is unsound for topic discovery: a repo that
adds a skill topic without a new commit keeps its old pushed_at, so it is
filtered out and the advancing watermark makes it permanently undiscoverable
(GitHub search has no topic-added-since qualifier). Remove the date filter and
the whole watermark/crawl_state machinery; topic discovery searches fully each
run (cheap via the downstream metadata/tree/content caches, not via narrowed
discovery). Keep options 5 (GraphQL batch) and 6b (tree cache), and fix 6b to
key on (pushed_at, default_branch) so a default-branch change isn't served
stale paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yya007
yya007 merged commit 2dc1411 into master Jun 22, 2026
1 check passed
@yya007
yya007 deleted the perf/ratelimit-graphql-tree-cache branch June 22, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant