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:
2018-08-14 16:36:43 +02:00
parent 5d86b87f40
commit b64577df2e
8 changed files with 191 additions and 66 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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(

View 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."""

View 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