Run submission in a separate thread, and more explicit state
Also more explicit timeouts and an overall better handling of errors.
This commit is contained in:
@@ -1,4 +1,58 @@
|
||||
from .client import CommunicationError
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from ..space import G
|
||||
from . import exceptions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def submit_benchmark_bgthread(benchmark_data: dict) -> threading.Thread:
|
||||
"""Submit benchmark data in a background thread.
|
||||
|
||||
This will update G.xxx to reflect the state of the submission.
|
||||
"""
|
||||
|
||||
thread = threading.Thread(target=_submit_and_update_g, args=(benchmark_data,))
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
def _submit_and_update_g(benchmark_data: dict) -> None:
|
||||
with G.progress_lock:
|
||||
G.submission_exception = None
|
||||
G.state = G.State.submitting
|
||||
|
||||
def set_exception(exception):
|
||||
with G.progress_lock:
|
||||
G.state = G.State.complete
|
||||
G.submission_exception = exception
|
||||
|
||||
try:
|
||||
submit_benchmark(benchmark_data)
|
||||
|
||||
except exceptions.CommunicationError as ex:
|
||||
log.error('Error %d submitting benchmark: %s', ex.status_code, ex.message)
|
||||
if ex.json:
|
||||
log.error('Response JSON:', ex.json)
|
||||
else:
|
||||
log.error('Response body: %s', ex.body)
|
||||
set_exception(ex)
|
||||
return
|
||||
|
||||
except exceptions.TokenTimeoutError as ex:
|
||||
log.warning('Timeout waiting for a client token. Just try submitting again.')
|
||||
set_exception(ex)
|
||||
return
|
||||
|
||||
except Exception as ex:
|
||||
log.error('error submitting benchmark: %s', ex)
|
||||
set_exception(ex)
|
||||
return
|
||||
|
||||
with G.progress_lock:
|
||||
G.state = G.State.complete
|
||||
G.results_submitted = True
|
||||
|
||||
|
||||
def submit_benchmark(benchmark_data: dict):
|
||||
@@ -22,11 +76,16 @@ def submit_benchmark(benchmark_data: dict):
|
||||
bc = BenchmarkClient(mydata_url)
|
||||
|
||||
# Make sure we have a token; can start the browser to get one.
|
||||
bc.load_auth_token()
|
||||
token = bc.load_auth_token()
|
||||
if not token:
|
||||
raise exceptions.TokenTimeoutError()
|
||||
|
||||
result = bc.submit_benchmark(benchmark_data)
|
||||
print(result)
|
||||
|
||||
# If we get a location from the MyData server, show it in a browser.
|
||||
if result.location:
|
||||
with G.progress_lock:
|
||||
G.results_url = result.location
|
||||
import webbrowser
|
||||
webbrowser.open_new_tab(result.location)
|
||||
|
@@ -50,7 +50,7 @@ class TokenHTTPServer(http.server.HTTPServer):
|
||||
self.log.debug('Finding free port starting at %s', local_addr)
|
||||
return sockutil.find_free_port(local_addr)
|
||||
|
||||
def wait_for_token(self, timeout=None):
|
||||
def wait_for_token(self, timeout: float):
|
||||
"""Starts the HTTP server, waits for the Token."""
|
||||
|
||||
if self.auth_token is None:
|
||||
|
@@ -7,27 +7,11 @@ import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
from . import timeouts, exceptions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommunicationError(requests.exceptions.BaseHTTPError):
|
||||
"""Raised when we get an invalid status code form the MyData server."""
|
||||
|
||||
def __init__(self, message: str, response: requests.Response):
|
||||
self.message = message
|
||||
self.status_code = response.status_code
|
||||
self.body = response.text
|
||||
|
||||
if response.headers.get('Content-Type', '') == 'application/json':
|
||||
self.json = response.json()
|
||||
else:
|
||||
self.json = None
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message}; ' \
|
||||
f'status_code={self.status_code}; json={self.json}; body={self.body}'
|
||||
|
||||
|
||||
class SubmissionResult:
|
||||
"""Metadata of the submitted benchmark.
|
||||
|
||||
@@ -46,7 +30,6 @@ class SubmissionResult:
|
||||
|
||||
|
||||
class BenchmarkClient:
|
||||
default_timeout = 30 # seconds
|
||||
|
||||
def __init__(self, mydata_server: str) -> None:
|
||||
from requests.adapters import HTTPAdapter
|
||||
@@ -126,7 +109,7 @@ class BenchmarkClient:
|
||||
log.debug('validating token at %s', self.url_verify_token)
|
||||
resp = self.session.get(self.url_verify_token,
|
||||
headers={'Authorization': f'Bearer {self.auth_token}'},
|
||||
timeout=self.default_timeout)
|
||||
timeout=timeouts.verify)
|
||||
token_ok = resp.status_code in {200, 204}
|
||||
if not token_ok:
|
||||
log.info('Client token is no longer valid, will obtain another one.')
|
||||
@@ -158,7 +141,7 @@ class BenchmarkClient:
|
||||
if not webbrowser.open_new_tab(url):
|
||||
raise SystemError(f'Unable to open a browser to visit {url}')
|
||||
|
||||
self.auth_token = self.auth_http_server.wait_for_token()
|
||||
self.auth_token = self.auth_http_server.wait_for_token(timeout=timeouts.wait_for_token)
|
||||
self._stop_http_server()
|
||||
|
||||
if self.auth_token:
|
||||
@@ -179,10 +162,10 @@ class BenchmarkClient:
|
||||
resp = self.session.post(self.url_submit,
|
||||
json=payload,
|
||||
headers={'Authorization': f'Bearer {self.auth_token}'},
|
||||
timeout=self.default_timeout)
|
||||
timeout=timeouts.submit)
|
||||
if resp.status_code != 201:
|
||||
log.error('Bad status code %d received: %s', resp.status_code, resp.text)
|
||||
raise CommunicationError(f'Bad status code received', resp)
|
||||
raise exceptions.CommunicationError(f'Bad status code received', resp)
|
||||
|
||||
result = resp.json()
|
||||
return SubmissionResult(
|
||||
|
23
benchmark/submission/exceptions.py
Normal file
23
benchmark/submission/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import requests
|
||||
|
||||
|
||||
class CommunicationError(requests.exceptions.BaseHTTPError):
|
||||
"""Raised when we get an invalid status code form the MyData server."""
|
||||
|
||||
def __init__(self, message: str, response: requests.Response):
|
||||
self.message = message
|
||||
self.status_code = response.status_code
|
||||
self.body = response.text
|
||||
|
||||
if response.headers.get('Content-Type', '') == 'application/json':
|
||||
self.json = response.json()
|
||||
else:
|
||||
self.json = None
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message}; ' \
|
||||
f'status_code={self.status_code}; json={self.json}; body={self.body}'
|
||||
|
||||
|
||||
class TokenTimeoutError(Exception):
|
||||
"""Raised when there was a timeout waiting for a client token."""
|
14
benchmark/submission/timeouts.py
Normal file
14
benchmark/submission/timeouts.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Timeouts for HTTP traffic, all in seconds."""
|
||||
|
||||
submit = 30
|
||||
verify = 10
|
||||
|
||||
# This one is tricky, as the user may need to take the time to register a new
|
||||
# Blender ID (which includes confirmation of their email address). We should
|
||||
# not show too scary messages when it comes to timeout errors when waiting for
|
||||
# a token, and just expect it to time out when a new Blender ID account is
|
||||
# registered.
|
||||
#
|
||||
# It should be long enough for a normal flow, though, as after this timeout
|
||||
# the temp HTTP server on localhost:$RANDOM is down again.
|
||||
wait_for_token = 15
|
Reference in New Issue
Block a user