Skip to content

fix: token_spend calculation incorrect with concurrent webhooks#1081

Merged
myakove merged 1 commit intomainfrom
fix/issue-970-token-spend-concurrency
Apr 30, 2026
Merged

fix: token_spend calculation incorrect with concurrent webhooks#1081
myakove merged 1 commit intomainfrom
fix/issue-970-token-spend-concurrency

Conversation

@myakove
Copy link
Copy Markdown
Collaborator

@myakove myakove commented Apr 30, 2026

Problem

CountingRequester was broken in two ways:

  1. withLazy() bypass: PyGithub's Github.get_repo() calls requester.withLazy() which returns a NEW raw Requester, completely bypassing the CountingRequester wrapper. Only Github-level calls (get_rate_limit) were counted — all Repository-level API calls (labels, check runs, comments) were invisible. A PR that ran for 2m50s doing dozens of operations reported only 1 API call.

  2. Shared counter between concurrent webhooks: If a Github instance were reused, multiple webhooks would share the same CountingRequester and corrupt each other's token_spend.

Fix

  • Override withLazy() in CountingRequester to return a new wrapper sharing the same counter (_shared_count list + _thread_lock)
  • Always create a fresh CountingRequester per webhook — unwrap any existing wrapper to get the real requester
  • Remove initial_wrapper_count — each webhook starts at 0, no delta needed

Also includes

  • Single-token optimization: Skip rate limit comparison loop when only 1 token is configured (saves 1 API call per webhook)
  • WEBHOOK_SERVER_DEV_MODE env var for uvicorn auto-reload during development
  • Remove hasattr(_Github__requester) guard — PyGithub >=2.4.0 guaranteed, direct access per AGENTS.md fail-fast principle

Verified on live dev server

Metric Before fix After fix
pull_request API calls 1 ❌ 31 ✅
Rate limit math 3934-3933=1 3340-3309=31 ✅
Rate limit capacity logged

Testing

  • All 39 related tests pass
  • ruff check/format clean
  • Peer-reviewed by Cursor (2 rounds)

Closes #970

Summary by CodeRabbit

  • New Features

    • Dev-mode server reload via environment toggle; worker/process behavior adjusted at runtime.
    • Shared request-counting wrappers for consistent API metrics.
  • Bug Fixes

    • Improved single-token API validation and short-circuit handling.
    • More accurate token/rate-limit accounting using absolute request counts; prevents cross-instance counter sharing.
  • Tests

    • Expanded coverage for metrics, single-token flows, init/destructor edge cases and concurrent webhooks.
  • Documentation

    • Updated token-spend description in webhook context.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4fc4cb51-4263-4be3-ba54-d3764aa9f1be

📥 Commits

Reviewing files that changed from the base of the PR and between 41d31e6 and e180859.

📒 Files selected for processing (8)
  • docs/docker-deployment.md
  • entrypoint.py
  • webhook_server/libs/github_api.py
  • webhook_server/tests/conftest.py
  • webhook_server/tests/test_github_api_metrics.py
  • webhook_server/tests/test_helpers.py
  • webhook_server/utils/context.py
  • webhook_server/utils/helpers.py

Walkthrough

Dev-mode toggles for Uvicorn were added and GitHub request-counting was reworked: requester wrappers now support shared counters/locks and are rewrapped per webhook to track absolute API call counts; helpers short-circuit single-token selection; tests and a context docstring were updated accordingly.

Changes

Cohort / File(s) Summary
Entrypoint / dev mode
entrypoint.py
Read WEBHOOK_SERVER_DEV_MODE env var, normalize to boolean _dev_mode; pass reload=_dev_mode to uvicorn and omit workers in dev mode; refactor uvicorn kwargs into a typed dict.
CountingRequester & webhook metrics
webhook_server/libs/github_api.py
CountingRequester accepts optional shared counter/lock, exposes count as a property backed by shared state, adds withLazy() to preserve shared state for lazily-derived requesters, and increments the shared counter under the shared lock. GithubWebhook unwraps any existing wrapper and installs a new per-webhook CountingRequester; token_spend and final rate-limit metrics use the wrapper’s absolute .count.
Helpers: single-token fast path
webhook_server/utils/helpers.py
get_api_with_highest_rate_limit short-circuits when exactly one (api, token) tuple exists: validates token by resolving get_user().login, checks rate.remaining for sentinel invalid token, logs rate, and returns the single (api, token, api_user) without iterating.
Context docstring
webhook_server/utils/context.py
Updated WebhookContext.token_spend docstring to state it represents API call counts captured by the webhook’s requester wrapper (documentation only).
Tests & fixtures
webhook_server/tests/test_github_api_metrics.py, webhook_server/tests/test_helpers.py, webhook_server/tests/conftest.py
Tests updated/added to reflect shared counter semantics and per-webhook wrapper behavior (withLazy() sharing, per-webhook counts, concurrent-webhooks regression), added single-token behavior tests, and fixture github_webhook now returns a mocked API with rate.remaining = 5000.

Sequence Diagram(s)

sequenceDiagram
    participant Webhook as rect rgba(200,230,255,0.5) Webhook
    participant Requester as rect rgba(200,255,200,0.5) CountingRequester
    participant PyGithub as rect rgba(255,230,200,0.5) PyGithub/API
    participant Metrics as rect rgba(255,200,200,0.5) WebhookContext
    Webhook->>PyGithub: receive API client (may be wrapped)
    Webhook->>Requester: unwrap existing wrapper -> create new per-webhook CountingRequester
    Webhook->>PyGithub: perform API call via Requester
    Requester->>Requester: increment shared_count under shared_lock
    Requester->>PyGithub: forward actual HTTP request
    PyGithub-->>Requester: response
    Requester-->>Webhook: return response
    Webhook->>Metrics: record requester.count as token_spend / final_count
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rnetser
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes include scope creep: WEBHOOK_SERVER_DEV_MODE env var and uvicorn reload feature are unrelated to the concurrent webhook token_spend bug fix. Move WEBHOOK_SERVER_DEV_MODE feature (entrypoint.py, docker-deployment.md docs) to a separate PR to keep this PR focused on fixing issue #970.
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main bug fix: correcting token_spend calculation for concurrent webhooks sharing the same GitHub token.
Linked Issues check ✅ Passed All coding requirements from issue #970 are met: CountingRequester now supports shared state via withLazy(), fresh wrappers are created per webhook by unwrapping existing ones, and initial_wrapper_count baseline tracking is removed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-970-token-spend-concurrency

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@myakove-bot
Copy link
Copy Markdown
Collaborator

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Pre-commit Checks: pre-commit runs automatically if .pre-commit-config.yaml exists
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest python-module-install - Test Python package installation
  • /retest pre-commit - Run pre-commit hooks and checks
  • /retest conventional-title - Validate commit message format
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. LGTM Count: Minimum 1 /lgtm from reviewers
  3. Status Checks: All required status checks must pass
  4. No Blockers: No wip, hold, has-conflicts labels and PR must be mergeable (no conflicts)
  5. Verified: PR must be marked as verified

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove
  • rnetser

Reviewers:

  • myakove
  • rnetser
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge
AI Features
  • Conventional Title: Mode: fix (claude/claude-opus-4-6[1m])
  • Cherry-Pick Conflict Resolution: Enabled (claude/claude-opus-4-6[1m])
  • Test Oracle: Triggers: approved (claude/claude-opus-4-6[1m]); /test-oracle can be used anytime

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is removed on new commits unless the push is detected as a clean rebase
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@entrypoint.py`:
- Around line 62-64: The call currently passes both workers=int(_max_workers)
and reload=_dev_mode (using _max_workers and _dev_mode), which is invalid for
uvicorn; change the invocation so that the workers parameter is only included
when reload is False (i.e., when _dev_mode is falsy). Locate the call that
supplies workers=int(_max_workers) and reload=_dev_mode and add conditional
logic to omit workers when _dev_mode is true so only reload is used in dev and
workers is used in production.

In `@webhook_server/libs/github_api.py`:
- Around line 217-224: The docstring for WebhookContext.token_spend is now
misleading because the code assigns token_spend = self.requester_wrapper.count
(absolute calls counted by this wrapper) instead of a rate-limit delta; update
the docstring in webhook_server/utils/context.py for class WebhookContext
(token_spend) to state it represents "API calls counted by this webhook's
requester_wrapper (absolute count)" or similar wording, and mention that
final_rate_limit is computed heuristically as initial_rate_limit -
requester_wrapper.count to keep docs consistent with the logic in github_api.py
(references: WebhookContext.token_spend, requester_wrapper.count,
final_rate_limit).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b553f696-9b6e-4000-a2b5-ae2b212695a8

📥 Commits

Reviewing files that changed from the base of the PR and between 13b4936 and 30c9817.

📒 Files selected for processing (5)
  • entrypoint.py
  • webhook_server/libs/github_api.py
  • webhook_server/tests/test_github_api_metrics.py
  • webhook_server/tests/test_helpers.py
  • webhook_server/utils/helpers.py

Comment thread entrypoint.py Outdated
Comment thread webhook_server/libs/github_api.py
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 30, 2026
@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webhook_server/tests/test_github_api_metrics.py (1)

145-180: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

HIGH: This still doesn't lock in the issue #970 regression.

This test proves the old wrapper is replaced, but it never exercises two GithubWebhook instances against the same mock_github_api. The bug was cross-webhook count contamination, so please add a direct two-instance test that performs requests through both webhook-local wrappers/repositories and asserts the counts stay independent.

🧪 Suggested regression shape
gh1 = GithubWebhook(minimal_hook_data, minimal_headers, logger)
gh2 = GithubWebhook(minimal_hook_data, minimal_headers, logger)

gh1.requester_wrapper.requestJsonAndCheck("a")
gh1.requester_wrapper.requestJsonAndCheck("b")
gh2.requester_wrapper.requestJsonAndCheck("c")

assert gh1.requester_wrapper.count == 2
assert gh2.requester_wrapper.count == 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webhook_server/tests/test_github_api_metrics.py` around lines 145 - 180,
Update the test to reproduce the cross-webhook-count regression by creating two
GithubWebhook instances against the same mock_github_api and exercising their
local wrappers independently: in test_init_unwraps_existing_wrapper (or a new
test), instantiate gh1 = GithubWebhook(...) and gh2 = GithubWebhook(...) using
the same mock_get_api return (mock_github_api with existing inner requester),
call gh1.requester_wrapper.requestJsonAndCheck("a"),
gh1.requester_wrapper.requestJsonAndCheck("b"), then
gh2.requester_wrapper.requestJsonAndCheck("c"), and finally assert
gh1.requester_wrapper.count == 2 and gh2.requester_wrapper.count == 1; ensure
the mock inner_requester used by mock_github_api supports the called method so
the CountingRequester increments independently for each wrapper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@webhook_server/tests/test_github_api_metrics.py`:
- Around line 311-320: The test test_del_cleanup_without_clone_dir should call
the destructor directly to make failures deterministic: create the
partially-initialized instance via gh = GithubWebhook.__new__(GithubWebhook) and
then invoke GithubWebhook.__del__(gh) (rather than using del gh) so any
exceptions in GithubWebhook.__del__ are raised in the test; update the
assertion/expectation accordingly to ensure __del__ handles the missing
clone_repo_dir without raising.

In `@webhook_server/utils/helpers.py`:
- Around line 495-510: The single-token fast path still makes a live GitHub call
via _api.get_rate_limit(); remove that call and instead use the cached
_api.rate_limiting data (or skip rate-limit logging) before returning. In the
block handling apis_and_tokens == 1, avoid calling _api.get_rate_limit() and
either construct a minimal rate_limit object from _api.rate_limiting (then call
log_rate_limit(rate_limit=..., api_user=_api_user)) or simply omit the call to
log_rate_limit, keeping the existing checks that read _api.rate_limiting and the
final logger.info and return of _api, _token, _api_user.

---

Outside diff comments:
In `@webhook_server/tests/test_github_api_metrics.py`:
- Around line 145-180: Update the test to reproduce the cross-webhook-count
regression by creating two GithubWebhook instances against the same
mock_github_api and exercising their local wrappers independently: in
test_init_unwraps_existing_wrapper (or a new test), instantiate gh1 =
GithubWebhook(...) and gh2 = GithubWebhook(...) using the same mock_get_api
return (mock_github_api with existing inner requester), call
gh1.requester_wrapper.requestJsonAndCheck("a"),
gh1.requester_wrapper.requestJsonAndCheck("b"), then
gh2.requester_wrapper.requestJsonAndCheck("c"), and finally assert
gh1.requester_wrapper.count == 2 and gh2.requester_wrapper.count == 1; ensure
the mock inner_requester used by mock_github_api supports the called method so
the CountingRequester increments independently for each wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ecbe836-85e4-458a-ae2d-ec1ccf228763

📥 Commits

Reviewing files that changed from the base of the PR and between 30c9817 and fc45543.

📒 Files selected for processing (7)
  • entrypoint.py
  • webhook_server/libs/github_api.py
  • webhook_server/tests/conftest.py
  • webhook_server/tests/test_github_api_metrics.py
  • webhook_server/tests/test_helpers.py
  • webhook_server/utils/context.py
  • webhook_server/utils/helpers.py

Comment thread webhook_server/tests/test_github_api_metrics.py Outdated
Comment thread webhook_server/utils/helpers.py
@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai[bot]

The following review comments were reviewed and a decision was made:

webhook_server/tests/test_github_api_metrics.py:145 (outside diff) — HIGH: This still doesn't lock in the issue #970 regression.

Addressed: Fixed: Added test_concurrent_webhooks_have_independent_counts regression test proving two GithubWebhook instances have independent counts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Tip

For best results, initiate chat on the files or code changes.

@myakove Thanks for confirming! Adding test_concurrent_webhooks_have_independent_counts is exactly the right regression lock — it directly proves that two concurrent GithubWebhook instances maintain independent counters, which is the core invariant broken by issue #970. This will catch any future regressions that re-introduce shared state between webhook instances.


🧠 Learnings used
Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1071
File: webhook_server/libs/handlers/pull_request_handler.py:224-226
Timestamp: 2026-04-14T10:11:43.829Z
Learning: In myk-org/github-webhook-server, retrying non-idempotent GitHub write operations (e.g., `create_issue_comment`, `create_issue`, `create_review_request`) through `github_api_call()` in `webhook_server/utils/github_retry.py` is an accepted and documented tradeoff. The risk of duplicate comments or issues on retry after a transient 5xx error is knowingly accepted in exchange for resilience against transient GitHub API failures. Do NOT flag this pattern as a bug or suggest adding idempotency markers — the decision is intentional and documented in a comment in `github_retry.py` above the retry loop.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1071
File: webhook_server/utils/github_retry.py:96-98
Timestamp: 2026-04-14T10:10:35.508Z
Learning: In myk-org/github-webhook-server, the `github_api_call()` wrapper in `webhook_server/utils/github_retry.py` intentionally retries ALL GitHub API calls including non-idempotent/mutating ones (e.g., `create_issue_comment`, `create_issue`, `create_review_request`, `create_check_run`, `merge`) on transient 5xx errors. This is an accepted and documented tradeoff — rare duplicate side effects are considered preferable to hard failures. Do NOT flag this as an issue or suggest splitting read/write retry policies; the maintainer explicitly rejected that approach as adding unnecessary complexity for minimal benefit.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 612
File: webhook_server_container/libs/github_api.py:925-926
Timestamp: 2024-10-29T10:42:50.163Z
Learning: In `webhook_server_container/libs/github_api.py`, the method `self._keep_approved_by_approvers_after_rebase()` must be called after removing labels when synchronizing a pull request. Therefore, it should be placed outside the `ThreadPoolExecutor` to ensure it runs sequentially after label removal.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1025
File: webhook_server/libs/handlers/runner_handler.py:952-977
Timestamp: 2026-03-12T13:44:41.853Z
Learning: In myk-org/github-webhook-server (webhook_server/**/*.py), PyGithub properties `.number`, `.title`, and `.body` are cached from the webhook payload and do NOT trigger blocking API calls. Therefore, these properties do NOT need to be wrapped with `asyncio.to_thread()` in async methods. Only PyGithub properties or methods that make live API calls (e.g., `.user.login`, `.base.ref`, `get_pull()`, `create_issue_comment()`, etc.) require wrapping. This is documented in CLAUDE.md as "Safe operations: Properties from webhook payload (.number, .title, .body)".

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 612
File: webhook_server_container/libs/github_api.py:2089-2100
Timestamp: 2024-10-29T08:09:57.157Z
Learning: In `webhook_server_container/libs/github_api.py`, when the function `_keep_approved_by_approvers_after_rebase` is called, existing approval labels have already been cleared after pushing new changes, so there's no need to check for existing approvals within this function.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1002
File: webhook_server/tests/test_pull_request_review_handler.py:298-344
Timestamp: 2026-02-28T12:05:25.906Z
Learning: In the myk-org/github-webhook-server repository's test-oracle integration (webhook_server/libs/test_oracle.py and webhook_server/libs/handlers/*.py), the 'approved' trigger refers to the `/approve` **command** (user-initiated comment command), NOT GitHub's review approval state. GitHub review approvals without the `/approve` command should NOT trigger the test oracle. This distinction is critical: review state "approved" alone does not trigger oracle; only the explicit `/approve` command does.

Learnt from: CR
Repo: myk-org/github-webhook-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-14T12:01:31.249Z
Learning: Applies to webhook_server/tests/**/*.py : Test files should mock `asyncio.to_thread` when testing functions that use `github_api_call()` internally

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1025
File: webhook_server/libs/handlers/runner_handler.py:1031-1033
Timestamp: 2026-03-12T13:44:36.341Z
Learning: In webhook_server/libs/handlers/runner_handler.py and other handlers, PyGithub properties such as `.number`, `.title`, and `.body` on PullRequest objects are safe to access directly in async code without asyncio.to_thread(), as they are cached from the webhook payload or from a previously fetched object (e.g., via get_pull()). Per CLAUDE.md: 'Safe operations (don't block): Properties from webhook payload (.number, .title, .body)'. Only actual API method calls (e.g., get_pull(), create_issue_comment(), add_to_labels()) need to be wrapped in asyncio.to_thread().

Learnt from: CR
Repo: myk-org/github-webhook-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-14T12:01:31.249Z
Learning: Applies to webhook_server/**/*.py : Maintain 90% code coverage minimum; new code without tests fails CI

Learnt from: CR
Repo: myk-org/github-webhook-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-14T12:01:31.249Z
Learning: Applies to webhook_server/libs/handlers/**/*.py : CRITICAL: Wrap ALL PyGithub method calls and property accesses with `github_api_call()` from `webhook_server.utils.github_retry` to prevent blocking the event loop and ensure retry protection for transient GitHub API failures

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 586
File: webhook_server_container/libs/github_api.py:1947-1956
Timestamp: 2024-10-08T09:19:56.185Z
Learning: In `webhook_server_container/libs/github_api.py`, the indentation style used in the `set_pull_request_automerge` method is acceptable as per the project's coding standards.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 778
File: webhook_server/libs/pull_request_handler.py:327-330
Timestamp: 2025-05-13T12:06:27.297Z
Learning: In the GitHub webhook server, synchronous GitHub API calls (like create_issue_comment, add_to_assignees, etc.) in async methods should be awaited using asyncio.to_thread or loop.run_in_executor to prevent blocking the event loop.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 778
File: webhook_server/libs/pull_request_handler.py:327-330
Timestamp: 2025-05-13T12:06:27.297Z
Learning: In the GitHub webhook server, synchronous GitHub API calls (like create_issue_comment, add_to_assignees, etc.) in async methods should be awaited using asyncio.to_thread or loop.run_in_executor to prevent blocking the event loop.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 1036
File: webhook_server/libs/github_api.py:609-630
Timestamp: 2026-03-17T14:45:44.692Z
Learning: In myk-org/github-webhook-server, in the `status` event handler inside `process()` in `webhook_server/libs/github_api.py`, only `pending` state is skipped. Terminal states `success`, `failure`, and `error` all trigger `check_if_can_be_merged()` re-evaluation so that the `can-be-merged` check always reflects the current status check outcome. Do NOT flag `failure`/`error` states as incorrectly triggering re-evaluation — this is intentional by design.

Learnt from: myakove
Repo: myk-org/github-webhook-server PR: 0
File: :0-0
Timestamp: 2025-10-28T16:09:08.689Z
Learning: For this repository, prioritize speed and minimizing API calls in reviews and suggestions: reuse webhook payload data, batch GraphQL queries, cache IDs (labels/users), and avoid N+1 patterns.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@entrypoint.py`:
- Line 19: Add documentation for the new runtime env var WEBHOOK_SERVER_DEV_MODE
(referenced by the _dev_mode variable in entrypoint.py) to the runtime
environment variables list in docs/docker-deployment.md: describe accepted
values ("1", "true", "yes" case-insensitive) and default behavior when unset,
explain its effect (enables webhook server dev mode) and give an example usage
line for docker/compose and exported shell variable form.

In `@webhook_server/tests/test_helpers.py`:
- Around line 172-180: The test includes hard-coded token-like literals (e.g.,
"token1") that trigger S105; refactor by extracting these literals into named
constants at the top of the test module (e.g., TEST_TOKEN_1) and append the
allowlist pragma comment "# pragma: allowlist secret" to each constant, then
update usages in get_api_with_highest_rate_limit() test and the other affected
block (around the assertions at lines 191-196) to reference those constants
instead of inline string literals; ensure constants are clearly named and used
wherever the token-like values appear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 42d7f1a2-4461-4548-aa7b-ef83c3c94090

📥 Commits

Reviewing files that changed from the base of the PR and between fc45543 and 41d31e6.

📒 Files selected for processing (7)
  • entrypoint.py
  • webhook_server/libs/github_api.py
  • webhook_server/tests/conftest.py
  • webhook_server/tests/test_github_api_metrics.py
  • webhook_server/tests/test_helpers.py
  • webhook_server/utils/context.py
  • webhook_server/utils/helpers.py

Comment thread entrypoint.py
Comment thread webhook_server/tests/test_helpers.py
CountingRequester was broken in two ways:

1. withLazy() bypass: PyGithub's Github.get_repo() calls
   requester.withLazy() which returns a NEW raw Requester,
   completely bypassing the CountingRequester wrapper. Only
   Github-level calls (get_rate_limit) were counted — all
   Repository-level API calls (labels, check runs, comments)
   were invisible. Fixed by overriding withLazy() to propagate
   the shared counter.

2. Shared counter between concurrent webhooks: if a Github
   instance were reused, multiple webhooks would share the same
   CountingRequester and corrupt each other's token_spend.
   Fixed by always creating a fresh CountingRequester per webhook.

Also includes:
- Single-token optimization: skip rate limit comparison when only
  one token is configured
- WEBHOOK_SERVER_DEV_MODE env var for uvicorn auto-reload
- Remove hasattr guard on _Github__requester (PyGithub >=2.4.0
  guaranteed)

Closes #970
@myakove myakove force-pushed the fix/issue-970-token-spend-concurrency branch from 41d31e6 to e180859 Compare April 30, 2026 14:08
@myakove
Copy link
Copy Markdown
Collaborator Author

myakove commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@myakove myakove merged commit c039880 into main Apr 30, 2026
7 of 9 checks passed
@myakove myakove deleted the fix/issue-970-token-spend-concurrency branch April 30, 2026 15:06
@myakove-bot
Copy link
Copy Markdown
Collaborator

New container for ghcr.io/myk-org/github-webhook-server:latest published

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: token_spend calculation incorrect when concurrent webhooks share the same GitHub token

2 participants