Log API Calls and the Rate Limit Left on Each macOS Admission Check

The calls per check grow with the number of active runs, and the token's
hourly limit is shared by the whole repository, so each check now logs how
many calls it made and what the last response says is left.
This commit is contained in:
Hanif Koh
2026-10-11 23:55:43 +08:00
parent 24cefcba62
commit fd7e8fd5a9
2 changed files with 68 additions and 4 deletions
+32 -4
View File
@@ -131,6 +131,10 @@ class Api:
def __init__(self, token, url="https://api.github.com"):
self.token = token
self.url = url.rstrip("/")
# The token's hourly limit is shared by every workflow in the repository,
# so what the last response says is left shows their use too.
self.calls = 0
self.remaining = self.limit = "?"
def get(self, path, **params):
query = urllib.parse.urlencode(params)
@@ -139,8 +143,22 @@ class Api:
"Authorization": f"Bearer {self.token}",
"X-GitHub-Api-Version": "2022-11-28",
})
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
self.calls += 1
try:
with urllib.request.urlopen(request, timeout=30) as response:
self.note_rate_limit(response.headers)
return json.load(response)
except urllib.error.HTTPError as error:
# A rate-limited request fails, and its headers say so.
self.note_rate_limit(error.headers)
raise
def note_rate_limit(self, headers):
self.remaining = headers.get("X-RateLimit-Remaining", "?")
self.limit = headers.get("X-RateLimit-Limit", "?")
def usage(self, calls_before):
return f" API calls this check: {self.calls - calls_before}, rate limit left: {self.remaining} of {self.limit}"
def list_runs(api, repo, workflow, **params):
@@ -227,9 +245,19 @@ def main():
api = Api(os.environ["GH_TOKEN"], os.environ.get("GITHUB_API_URL", "https://api.github.com"))
run_id = int(os.environ["GITHUB_RUN_ID"])
limit = int(os.environ.get("MACOS_RUNNER_LIMIT") or 5)
def measure_now():
calls_before = api.calls
try:
total, lines = measure(api, repo, workflow, run_id, datetime.datetime.now(datetime.timezone.utc),
priority=os.environ.get("PRIORITY") == "true")
except Exception:
print(api.usage(calls_before).strip())
raise
return total, lines + [api.usage(calls_before)]
reason = wait(
lambda: measure(api, repo, workflow, run_id, datetime.datetime.now(datetime.timezone.utc),
priority=os.environ.get("PRIORITY") == "true"),
measure_now,
limit,
wait_minutes=int(os.environ.get("WAIT_MINUTES") or 240),
poll_seconds=int(os.environ.get("POLL_SECONDS") or 180),
+36
View File
@@ -9,6 +9,7 @@ import os
import sys
import unittest
import urllib.error
from unittest import mock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -289,6 +290,41 @@ class MeasureTest(unittest.TestCase):
self.assertFalse(any("event" in params for _, params in api.calls))
class ApiTest(unittest.TestCase):
class Response:
def __init__(self, headers):
self.headers = headers
def __enter__(self):
return self
def __exit__(self, *_):
return False
def read(self):
return b"{}"
def test_reports_calls_and_the_rate_limit_left(self):
api = admission.Api("token")
responses = [self.Response({}),
self.Response({"X-RateLimit-Remaining": "812", "X-RateLimit-Limit": "1000"})]
with mock.patch.object(admission.urllib.request, "urlopen", side_effect=responses):
api.get("first")
calls_before = api.calls
api.get("second")
self.assertEqual(api.usage(0), " API calls this check: 2, rate limit left: 812 of 1000")
self.assertEqual(api.usage(calls_before), " API calls this check: 1, rate limit left: 812 of 1000")
def test_reads_the_rate_limit_from_a_failed_request(self):
api = admission.Api("token")
headers = {"X-RateLimit-Remaining": "0", "X-RateLimit-Limit": "1000"}
error = urllib.error.HTTPError("url", 403, "Forbidden", headers, None)
with mock.patch.object(admission.urllib.request, "urlopen", side_effect=error), \
self.assertRaises(urllib.error.HTTPError):
api.get("runs")
self.assertEqual(api.usage(0), " API calls this check: 1, rate limit left: 0 of 1000")
class WaitTest(unittest.TestCase):
def run_wait(self, results, limit=5, wait_minutes=60, poll_seconds=120):
results = iter(results)