Skip to content

fix(github-poller): drop page-1 ETag sentinel on paginated endpoints#81

Merged
LakshmanTurlapati merged 2 commits into
mainfrom
fix/github-poller-page1-etag
May 26, 2026
Merged

fix(github-poller): drop page-1 ETag sentinel on paginated endpoints#81
LakshmanTurlapati merged 2 commits into
mainfrom
fix/github-poller-page1-etag

Conversation

@LakshmanTurlapati

Copy link
Copy Markdown
Collaborator

Summary

  • Server-side GitHub stats poller silently strands paginated entries past page 1 for any oldest-first endpoint, because it uses page 1's ETag as a sentinel for the whole dataset. Visible failure: /stats stars graph stuck at 100 in prod while the repo grew past 110.
  • Root cause is in showcase/server/src/telemetry/github-poller.js — for endpoints GitHub sorts newest-first (commits, releases, etc.) the trick works because page 1 mutates on every new entry; for oldest-first endpoints (stargazers default, forks?sort=oldest) page 1 freezes at 100 entries forever and the ETag never changes.
  • Fix: never send If-None-Match on paginated endpoints; always re-walk all pages. Non-paginated repo-summary keeps the ETag round-trip (no pagination wrinkle there). Cost: ~14 extra GitHub req/hr across the 6 paginated endpoints, well under the 5000 req/hr authenticated cap.
  • Also pre-empts the same latent bug on forks (currently 9 forks; would have hit the moment count crossed 100).

Investigation

Full root-cause report in .planning/debug/stars-graph-stuck-at-100.md (not part of this PR — internal debug artifact).

Key evidence:

  • curl .../api/public-stats/github/stars | jq length100 exactly
  • curl https://api.github.com/repos/LakshmanTurlapati/FSB/stargazers?page=1&per_page=100 → 100 entries with identical first/last bookends to the stale cache
  • curl .../api/public-stats/github/stargazers?page=2&per_page=100 → 14 entries (the missing ones)
  • Verified the fix end-to-end with an in-process mock-fetch simulation: pre-fix with a stale page-1 ETag fed in → touchGithubCacheRow only (cache stays at 100); post-fix → upsertGithubCacheRow with 114 entries.

Test plan

  • Deploy to Fly
  • Within ~5 min (one poller tick): curl -s https://full-selfbrowsing.com/api/public-stats/github/stars | jq length returns ≥ live stargazers_count (currently 114) and continues to grow as new stars come in
  • repo-summary endpoint unchanged: curl -s https://full-selfbrowsing.com/api/public-stats/github/repo-summary | jq .stargazers_count still matches live
  • /stats page in browser shows current star count (not 100)
  • No GitHub rate-limit spike in server logs over the next hour

The poller's "send cached page-1 ETag, on 304 skip the walk" trick is
only valid for endpoints GitHub sorts newest-first. For oldest-first
endpoints (stargazers default, forks?sort=oldest), once page 1 fills to
100 entries it becomes immutable and the ETag freezes -- the poller
then treats every subsequent 304 as "nothing changed" and the cache
silently strands all entries on pages 2+. Visible failure: /stats stars
graph stuck at 100 in production while the repo grew past 110.

Fix: never send If-None-Match on paginated endpoints; always walk all
pages. Non-paginated `repo-summary` keeps the ETag round-trip (no
pagination wrinkle there). Cost: ~14 extra GitHub req/hr across the 6
paginated endpoints, well under the 5000 req/hr authenticated cap.

Verified with in-process mock-fetch simulation: pre-fix with a stale
page-1 ETag fed in -> touchGithubCacheRow only (cache stuck at 100);
post-fix -> upsertGithubCacheRow with 114 entries (matches live).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c71ac48c3a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const firstUrl = `${base}?${cfg.query}${cfg.query ? '&' : ''}page=1&per_page=${PER_PAGE}`;

const firstResp = await fetchOnePage(firstUrl, cfg.accept, existingEtag, fetchImpl);
const firstResp = await fetchOnePage(firstUrl, cfg.accept, null, fetchImpl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard always-walk pagination behind authenticated mode

Passing null for If-None-Match on paginated endpoints forces full polling every 5 minutes, which guarantees at least 84 unauthenticated REST calls/hour (6 paginated endpoints + repo-summary across 12 ticks) before extra pages are counted. In environments without GITHUB_TOKEN, this deterministically exceeds GitHub’s 60/hr unauthenticated limit, after which the page=1 fetches return non-200 and this function skips cache updates, leaving stats stale. This is a behavior regression from the previous conditional-request path for tokenless deployments.

Useful? React with 👍 / 👎.

Addresses Codex P1 on PR #81: the previous fix dropped If-None-Match on
all paginated endpoints, which would have pushed tokenless deployments
past the 60 req/hr unauthenticated cap (7 endpoints x 12 ticks/hr = 84
baseline first-page requests, all 200s).

GitHub's rate-limit policy doesn't count 304 responses against the
primary limit, so the original ETag round-trip was effectively free for
newest-first endpoints (commits, issues, pulls, releases) -- and it's
still correct there because new entries land on page 1, so page 1's
ETag is a valid whole-dataset freshness sentinel.

Only `stars` (stargazers default sort = oldest-first) and
`forks?sort=oldest` need the always-walk treatment, because page 1
becomes immutable once it fills to 100 entries. Marked those two with
`oldestFirst: true` in the ENDPOINTS config; pollEndpoint reads the
flag and only sends If-None-Match when it's false/absent.

Net cost vs. pre-fix: ~36 extra req/hr (stars walks 2 pages currently,
forks walks 1) -- still inside the 60 req/hr per-IP unauthenticated cap
and trivial against the 5000 req/hr authenticated cap. Newest-first
endpoints retain their original tokenless-friendly behavior.
@LakshmanTurlapati

Copy link
Copy Markdown
Collaborator Author

@codex review — addressed the P1 in 21d04b8. Only the oldest-first endpoints (stars + forks?sort=oldest) now opt out of the ETag sentinel via oldestFirst: true; the four newest-first paginated endpoints (commits, issues, pulls, releases) keep the ETag round-trip, so tokenless deployments stay within the 60 req/hr per-IP cap (304 responses don't count against the rate limit). Net cost vs. pre-fix: ~36 req/hr from the two oldest-first endpoints, comfortably under 60/hr.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@LakshmanTurlapati LakshmanTurlapati merged commit fc21f2e into main May 26, 2026
8 checks passed
@LakshmanTurlapati LakshmanTurlapati deleted the fix/github-poller-page1-etag branch May 29, 2026 01:01
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