{"record":{"id":"e11522522aadd6f0","repo":"666ghj/MiroFish","slug":"github-graphql-repeated-an-edge-cursor","errorCode":null,"errorMessage":"GitHub GraphQL repeated an edge cursor","messagePattern":"GitHub GraphQL repeated an edge cursor","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":589,"sourceCode":"    after: str | None = None\n    seen_page_cursors: set[str] = set()\n    seen_edge_cursors: set[str] = set()\n    daily_increments: Counter[date] = Counter()\n    initial_total: int | None = None\n    page_number = 0\n\n    while True:\n        page = github.fetch_stargazer_page(after)\n        page_number += 1\n        if initial_total is None:\n            initial_total = page.total_count\n            pages_required = math.ceil(initial_total / PAGE_SIZE)\n            remaining_requests = max(0, pages_required - 1)\n            if page.rate_remaining < remaining_requests + RATE_LIMIT_RESERVE:\n                raise StarHistoryError(\"insufficient GitHub GraphQL rate limit for backfill\")\n        for edge in page.edges:\n            if edge.cursor in seen_edge_cursors:\n                raise StarHistoryError(\"GitHub GraphQL repeated an edge cursor\")\n            seen_edge_cursors.add(edge.cursor)\n            if edge.starred_at.date() < normalized_now.date():\n                daily_increments[edge.starred_at.date()] += 1\n\n        if page.end_cursor is not None:\n            if page.end_cursor in seen_page_cursors:\n                raise StarHistoryError(\"GitHub GraphQL repeated a page cursor\")\n            seen_page_cursors.add(page.end_cursor)\n        if not page.has_next_page:\n            break\n        if page.end_cursor is None:\n            raise StarHistoryError(\"GitHub GraphQL omitted the next page cursor\")\n        after = page.end_cursor\n        if page_number > 10_000:\n            raise StarHistoryError(\"GitHub GraphQL exceeded the page safety limit\")\n\n    if initial_total is None:\n        raise StarHistoryError(\"GitHub GraphQL returned no pages\")","sourceCodeStart":571,"sourceCodeEnd":607,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L571-L607","documentation":"Raised while paginating the GitHub GraphQL stargazer list during the one-time backfill (scripts/star_history.py:589). Each stargazer edge carries a unique pagination cursor; this script keeps a `seen_edge_cursors` set and refuses to continue if GitHub hands back an edge whose cursor was already delivered on an earlier page. It is an integrity guard: silently accepting a repeat would double-count stars in `daily_increments` and corrupt the reconstructed history.","triggerScenarios":"Calling the backfill that loops `github.fetch_stargazer_page(after)` (100 edges per page, PAGE_SIZE=100) on a repository whose stargazer list is concurrently changing, or when GitHub's GraphQL pagination returns overlapping pages. Concretely: page N returns edges [c1..c100], a later page returns an edge whose cursor is already in `seen_edge_cursors` -> raise before the `daily_increments[edge.starred_at.date()] += 1` line can double count.","commonSituations":"Backfill run on an actively starred repository (stars arrive between page fetches), GitHub eventual-consistency quirks on large `stargazers(first:100, after:...)` traversals, or a mock/test gateway that replays the same page twice. Typical for maintainers running the documented one-time authorized backfill via `gh api graphql`.","solutions":["Simply re-run the backfill from scratch: it is a fresh-start operation, and a transient pagination overlap usually disappears on a second run when star churn has settled.","Run the backfill when star churn is low (or immediately after announcing a freeze) so the list does not shift between page fetches.","If the error reproduces deterministically, capture the raw `gh api graphql` response for the failing page and inspect whether GitHub is returning duplicate edges/cursors; report it as an upstream pagination inconsistency.","If using a custom GitHubGateway implementation in tests, fix the fake gateway so each page yields distinct edge cursors (e.g. generate cursors from a monotonic counter)."],"exampleFix":"// test fake gateway that repeated cursors across pages\nclass FakeGateway:\n    def fetch_stargazer_page(self, after):\n        # before: every page reused cursor \"edge-0\"\n        # after: cursors derived from the edge index are unique per page\n        edges = tuple(\n            StargazerEdge(cursor=f\"edge-{page_index * 100 + i}\",\n                          starred_at=START + timedelta(days=page_index, minutes=i))\n            for i in range(100)\n        )\n        ...","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    state = run_backfill(gateway, clock)\nexcept StarHistoryError as exc:\n    if \"repeated an edge cursor\" in str(exc):\n        logger.warning(\"backfill saw duplicate edge cursor; retrying once: %s\", exc)\n        state = run_backfill(gateway, clock)  # fresh seen-sets each run\n    else:\n        raise","preventionTips":["Run the one-time backfill when the repository is not actively gaining/losing stars (avoid trending periods).","Treat backfill as idempotent-from-scratch: always restart the whole pagination, never resume mid-way from a saved cursor.","In test gateways, derive edge cursors from a global index so pages never overlap.","Log page_number alongside the error so a repeat at the same page points to upstream pagination rather than churn."],"tags":["github-api","graphql","pagination","backfill","data-integrity"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}