Repository navigation
Job-wait loop and login can be stopped in JupyterLite; a stale token re-authenticates [AI-written] #380
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
Job-wait loop and login can be stopped in JupyterLite; a stale token re-authenticates [AI-written] #380
Changes from all commits
095c712
f63fbdf
42d7052
1acb4a2
2321863
ba85f79
f2e1b1b
c8bda7f
3fce4da
1f82b93
0907baf
6582f49
27c08c0
c98adfc
2af535e
8dfdd64
633e99e
5ca0f2e
d8e90f4
9a45cce
894e58e
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 |
|---|---|---|
| @@ -1,10 +1,23 @@ | ||
| import asyncio | ||
| import json | ||
| import re | ||
| import urllib.parse | ||
| import urllib.request | ||
| from typing import Any, Dict, Iterable, List, Optional, Union | ||
| from typing import Any, Awaitable, Dict, Iterable, List, Optional, Union | ||
|
|
||
| import requests | ||
| from mat3ra.api_client import APIClient, JobEndpoints | ||
|
|
||
| from ....auth import reauthenticate | ||
| from ....primitive.environment import is_pyodide_environment | ||
|
|
||
| try: | ||
| from pyodide.http import pyfetch # type: ignore | ||
| except ImportError: | ||
| pyfetch = None | ||
|
|
||
| MATERIALS_SET_ENTITY_CLASS = "Material" | ||
| DEFAULT_STATUS_TIMEOUT_SECONDS = 30 | ||
|
|
||
|
|
||
| def save_files(job_id: str, job_endpoint: JobEndpoints, filename_on_cloud: str, filename_on_disk: str) -> None: | ||
|
|
@@ -25,18 +38,55 @@ def save_files(job_id: str, job_endpoint: JobEndpoints, filename_on_cloud: str, | |
| outp.write(server_response.read()) | ||
|
|
||
|
|
||
| def get_jobs_statuses_by_ids(endpoint: JobEndpoints, job_ids: List[str]) -> List[str]: | ||
| async def _list_jobs_with_fetch(endpoint: JobEndpoints, query: dict, projection: dict, abort_signal: Any) -> List[dict]: | ||
| """ | ||
| `endpoint.list` through the browser's fetch, which leaves the event loop free while the request is in flight. | ||
| Raises `requests.HTTPError` on an error status. | ||
| """ | ||
| parameters = urllib.parse.urlencode({"query": json.dumps(query), "projection": json.dumps(projection)}) | ||
| url = urllib.parse.urljoin(endpoint.conn.preamble, f"{endpoint.name}?{parameters}") | ||
| response = await pyfetch(url, headers={**endpoint.headers, **endpoint.auth.get_headers()}, signal=abort_signal) | ||
| if not response.ok: | ||
| error_response = requests.Response() | ||
| error_response.status_code = response.status | ||
| raise requests.HTTPError(f"Error {response.status}.", response=error_response) | ||
| return (await response.json())["data"] | ||
|
|
||
|
|
||
| async def get_jobs_statuses_by_ids_async( | ||
| endpoint: JobEndpoints, | ||
| job_ids: List[str], | ||
| timeout: float = DEFAULT_STATUS_TIMEOUT_SECONDS, | ||
| abort_signal: Any = None, | ||
| ) -> List[str]: | ||
| """ | ||
| Gets jobs statues by their IDs. | ||
| Gets jobs statuses by their IDs without blocking the event loop: through the browser's fetch in pyodide, | ||
| in a worker thread otherwise. A rejected access token (401) is replaced through the device login once. | ||
|
|
||
| Args: | ||
| endpoint (JobEndpoints): Job endpoint object from the Exabyte API Client | ||
| job_ids (list): list of job IDs to get the status for | ||
| timeout (float): seconds to wait for the response before raising asyncio.TimeoutError | ||
| abort_signal: JS AbortSignal that aborts the fetch in pyodide | ||
|
|
||
| Returns: | ||
| list: list of job statuses | ||
| """ | ||
| jobs = endpoint.list({"_id": {"$in": job_ids}}, {"fields": {"status": 1}}) | ||
| query = {"_id": {"$in": job_ids}} | ||
| projection = {"fields": {"status": 1}} | ||
|
|
||
| def request_jobs() -> Awaitable[List[dict]]: | ||
| if is_pyodide_environment(): | ||
| return _list_jobs_with_fetch(endpoint, query, projection, abort_signal) | ||
| return asyncio.get_running_loop().run_in_executor(None, endpoint.list, query, projection) | ||
|
|
||
| try: | ||
| jobs = await asyncio.wait_for(request_jobs(), timeout) | ||
| except requests.HTTPError as error: | ||
| if error.response.status_code != 401 or not endpoint.auth.access_token: | ||
| raise | ||
| await reauthenticate(endpoint.auth) | ||
| jobs = await asyncio.wait_for(request_jobs(), timeout) | ||
|
Comment on lines
+83
to
+89
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. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win One slow status request ends the whole job wait.
🤖 Prompt for AI Agents |
||
| return [job["status"] for job in jobs] | ||
|
|
||
|
|
||
|
|
||
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.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Token validation can block and does not set a timeout.
_authenticate_oidc_with_cacheis an async function. It callsAPIClient.authenticate(...).list_accounts()directly, and that call is synchronous. In native Python, a slow platform stalls the event loop, and the call has no timeout of its own in this code.authenticatenow makes this call on every non-host run, so a hung platform leavesauthenticate()stuck until the API client's own timeout, if it has one. Run the check in an executor and wrap it inasyncio.wait_for, the same way the job-status request does.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Learnings