Repository navigation
Fix the tests that fail against staging on every run #2079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0ceae32
9c7dd80
259ac04
e983904
737d0b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,35 @@ def service_api_key() -> str: | |
| return service_api_key | ||
|
|
||
|
|
||
| # The API counts requests per user in a fixed 60 second window and every CI | ||
| # job uses the same API key, so a 429 is the other jobs' traffic and not a | ||
| # defect in the code under test. Reruns come too soon to outlast the window. | ||
| RATE_LIMIT_WINDOW_SECONDS = 60 | ||
| RATE_LIMIT_FALLBACK_WAIT_SECONDS = 20 | ||
| RATE_LIMIT_MAX_WAITS = 4 | ||
|
|
||
|
|
||
| def wait_out_rate_limit(send): | ||
| """Wraps `requests.Session.send` to resend a request answered with 429.""" | ||
|
|
||
| def send_and_wait(request, **kwargs): | ||
| for _ in range(RATE_LIMIT_MAX_WAITS): | ||
| response = send(request, **kwargs) | ||
| if response.status_code != requests.codes.too_many_requests: | ||
| return response | ||
| # Retry-After is the time left in the window, if the API sends it. | ||
| wait = RATE_LIMIT_FALLBACK_WAIT_SECONDS | ||
| retry_after = response.headers.get("Retry-After", "") | ||
| if retry_after.isdigit(): | ||
| wait = min(int(retry_after), RATE_LIMIT_WINDOW_SECONDS) + 1 | ||
| # Without a trace in the log a wait only looks like a slow test. | ||
| print(f"API rate limit reached, resending in {wait} seconds") | ||
| time.sleep(wait) | ||
| return send(request, **kwargs) | ||
|
|
||
| return send_and_wait | ||
|
|
||
|
|
||
| class IntegrationClient(Client): | ||
| def __init__(self, environ: str) -> None: | ||
| api_url = graphql_url(environ) | ||
|
|
@@ -173,6 +202,8 @@ def __init__(self, environ: str) -> None: | |
| rest_endpoint=rest_endpoint, | ||
| ) | ||
| self.queries = [] | ||
| # GraphQL and REST calls both leave through this session's send(). | ||
| self.connection.send = wait_out_rate_limit(self.connection.send) | ||
|
|
||
| def execute(self, query=None, params=None, check_naming=True, **kwargs): | ||
| if check_naming and query is not None: | ||
|
|
@@ -184,6 +215,21 @@ def execute(self, query=None, params=None, check_naming=True, **kwargs): | |
| kwargs["timeout"] = 30.0 | ||
| return super().execute(query, params, **kwargs) | ||
|
|
||
| def _create_project(self, input) -> Project: | ||
| # createProject returns the project even when the server failed to | ||
| # finish setting it up (seen when many are created at once). It then | ||
| # has no editor and connect_ontology cannot attach one: create again. | ||
| for _ in range(2): | ||
| project = super()._create_project(input) | ||
| if project.labeling_frontend() is not None: | ||
| return project | ||
| # The ID is what the API's logs can be searched for. | ||
| print(f"Project {project.uid} has no editor, creating it again") | ||
| project.delete() | ||
| # Jitter, so workers that failed together do not retry together. | ||
| time.sleep(randint(10, 30) / 10) | ||
| return super()._create_project(input) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing editor deletes healthy projectsMedium Severity
Reviewed by Cursor Bugbot for commit 737d0b6. Configure here. |
||
|
|
||
|
|
||
| class AdminClient(Client): | ||
| def __init__(self, env): | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rate-limit retries wake up together
Medium Severity
wait_out_rate_limitsleeps a fixed time on every 429, with no jitter. Every CI job uses the same API key, so jobs that hit the cap together wait the sameRetry-Afteror the same 20 seconds and resend together. That recreates the rate limit this wrapper is meant to ride out.Reviewed by Cursor Bugbot for commit 737d0b6. Configure here.