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:
@@ -346,13 +346,22 @@ class BENCHMARK_PT_main(Panel):
|
|||||||
sub.separator()
|
sub.separator()
|
||||||
|
|
||||||
sub = col.row()
|
sub = col.row()
|
||||||
sub.enabled = not G.results_submitted
|
sub.enabled = G.state != G.State.submitting
|
||||||
sub.scale_y = 2.25
|
sub.scale_y = 2.25
|
||||||
if G.submission_exception:
|
text = "SHARE ONLINE"
|
||||||
text = "Retry Submission"
|
if G.results_submitted and G.state != G.State.submitting:
|
||||||
|
if G.results_url:
|
||||||
|
# If we have a results URL, open it upon clicking the button
|
||||||
|
sub.operator("wm.url_open", text="Shared!").url = G.results_url
|
||||||
|
else:
|
||||||
|
sub.enabled = False
|
||||||
|
sub.operator("benchmark.share", text=text)
|
||||||
else:
|
else:
|
||||||
text = "SHARE ONLINE"
|
if G.state == G.State.submitting:
|
||||||
sub.operator("benchmark.share", text=text)
|
text = "Submitting..."
|
||||||
|
elif G.submission_exception:
|
||||||
|
text = "Retry Submission"
|
||||||
|
sub.operator("benchmark.share", text=text)
|
||||||
|
|
||||||
sub = col.row()
|
sub = col.row()
|
||||||
subsub = sub.split()
|
subsub = sub.split()
|
||||||
@@ -368,18 +377,17 @@ class BENCHMARK_PT_main(Panel):
|
|||||||
split.label()
|
split.label()
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
screen_index = 0
|
|
||||||
|
|
||||||
with G.progress_lock:
|
with G.progress_lock:
|
||||||
if G.result_dict:
|
state = G.state
|
||||||
screen_index = 2
|
|
||||||
elif G.result_stats or G.progress_status:
|
|
||||||
screen_index = 1
|
|
||||||
|
|
||||||
if screen_index == 0:
|
draw_funcs = {
|
||||||
self.draw_welcome(context)
|
G.State.welcome: self.draw_welcome,
|
||||||
elif screen_index == 2:
|
G.State.complete: self.draw_submit,
|
||||||
self.draw_submit(context)
|
G.State.submitting: self.draw_submit,
|
||||||
|
}
|
||||||
|
func = draw_funcs.get(state, None)
|
||||||
|
if func:
|
||||||
|
func(context)
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -488,8 +496,10 @@ class BENCHMARK_OT_run_base(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
G.result_stats += "{}: {}".format(name_stat['name'],
|
G.result_stats += "{}: {}".format(name_stat['name'],
|
||||||
stat["result"])
|
stat["result"])
|
||||||
|
G.state = G.State.complete
|
||||||
else:
|
else:
|
||||||
G.result_stats = ""
|
G.result_stats = ""
|
||||||
|
G.state = G.State.welcome
|
||||||
# TOGO(sergey): Use some more nice picture for the final slide.
|
# TOGO(sergey): Use some more nice picture for the final slide.
|
||||||
G.background_image_path = ""
|
G.background_image_path = ""
|
||||||
# Tag for nice redraw
|
# Tag for nice redraw
|
||||||
@@ -513,9 +523,11 @@ class BENCHMARK_OT_run_base(bpy.types.Operator):
|
|||||||
return {'PASS_THROUGH'}
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
G.cancel = False
|
with G.progress_lock:
|
||||||
G.result_platform = ""
|
G.cancel = False
|
||||||
G.progress_status = "Initializing..."
|
G.result_platform = ""
|
||||||
|
G.progress_status = "Initializing..."
|
||||||
|
G.state = G.State.running
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
|
|
||||||
compute_device = context.scene.compute_device
|
compute_device = context.scene.compute_device
|
||||||
@@ -630,30 +642,44 @@ class BENCHMARK_OT_share(bpy.types.Operator):
|
|||||||
bl_idname = "benchmark.share"
|
bl_idname = "benchmark.share"
|
||||||
bl_label = "Share Benchmark Result"
|
bl_label = "Share Benchmark Result"
|
||||||
|
|
||||||
def execute(self, context):
|
timer = None
|
||||||
|
thread = None
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if event.type == 'TIMER':
|
||||||
|
if self.thread.is_alive():
|
||||||
|
context.area.tag_redraw()
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
else:
|
||||||
|
self.done(context)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
from benchmark import submission
|
from benchmark import submission
|
||||||
|
|
||||||
make_buttons_default()
|
make_buttons_default()
|
||||||
print('Submitting benchmark')
|
self.thread = submission.submit_benchmark_bgthread(G.result_dict)
|
||||||
G.submission_exception = None
|
|
||||||
try:
|
# Create timer to query thread status
|
||||||
submission.submit_benchmark(G.result_dict)
|
wm = context.window_manager
|
||||||
except submission.CommunicationError as cex:
|
self.timer = wm.event_timer_add(0.1, context.window)
|
||||||
logger.ERROR(f'Error {cex.status_code} submitting benchmark: {cex.message}')
|
|
||||||
if cex.json:
|
# Register self as modal.
|
||||||
logger.ERROR(f'Response JSON: {cex.json}')
|
context.window_manager.modal_handler_add(self)
|
||||||
else:
|
return {'RUNNING_MODAL'}
|
||||||
logger.ERROR(f'Response body: {cex.body}')
|
|
||||||
G.submission_exception = cex
|
def done(self, context):
|
||||||
return {'CANCELLED'}
|
|
||||||
except Exception as ex:
|
|
||||||
logger.ERROR(f'error submitting benchmark: {ex}')
|
|
||||||
G.submission_exception = ex
|
|
||||||
return {'CANCELLED'}
|
|
||||||
print('Submission done')
|
|
||||||
make_buttons_green()
|
make_buttons_green()
|
||||||
G.results_submitted = True
|
|
||||||
return {'FINISHED'}
|
if self.timer:
|
||||||
|
wm = context.window_manager
|
||||||
|
wm.event_timer_remove(self.timer)
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join()
|
||||||
|
|
||||||
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
class BENCHMARK_OT_opendata_link(bpy.types.Operator):
|
class BENCHMARK_OT_opendata_link(bpy.types.Operator):
|
||||||
|
@@ -5,7 +5,7 @@ import blf
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from ..foundation import util
|
from ..foundation import util
|
||||||
from ..submission.client import CommunicationError
|
from ..submission import exceptions
|
||||||
|
|
||||||
from . import G
|
from . import G
|
||||||
|
|
||||||
@@ -231,8 +231,16 @@ def _after_submission_text() -> str:
|
|||||||
return f'Unable to connect to the Open Data platform. ' \
|
return f'Unable to connect to the Open Data platform. ' \
|
||||||
f'Please check your internet connection and try again.'
|
f'Please check your internet connection and try again.'
|
||||||
|
|
||||||
|
if isinstance(ex, requests.exceptions.Timeout):
|
||||||
|
return f'There was a timeout communicating with the Open Data platform. ' \
|
||||||
|
f'Please check your internet connection and try again.'
|
||||||
|
|
||||||
|
if isinstance(ex, exceptions.TokenTimeoutError):
|
||||||
|
return f'There was a timeout waiting for a Client Authentication token. ' \
|
||||||
|
f'This is fine, just try again.'
|
||||||
|
|
||||||
# If not our own exception class, show generic message.
|
# If not our own exception class, show generic message.
|
||||||
if not isinstance(ex, CommunicationError):
|
if not isinstance(ex, exceptions.CommunicationError):
|
||||||
return f'Error submitting your results: {ex}'
|
return f'Error submitting your results: {ex}'
|
||||||
|
|
||||||
# Return proper message based on the HTTP status code of the response.
|
# Return proper message based on the HTTP status code of the response.
|
||||||
|
@@ -1,9 +1,18 @@
|
|||||||
|
import enum
|
||||||
import threading
|
import threading
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
|
||||||
class G:
|
class G:
|
||||||
"""Global state of the Benchmark Client."""
|
"""Global state of the Benchmark Client."""
|
||||||
|
|
||||||
|
class State(enum.Enum):
|
||||||
|
welcome = 1
|
||||||
|
running = 2
|
||||||
|
complete = 3
|
||||||
|
submitting = 4
|
||||||
|
|
||||||
|
state = State.welcome
|
||||||
result_platform = ''
|
result_platform = ''
|
||||||
progress_status = ''
|
progress_status = ''
|
||||||
result_stats = ''
|
result_stats = ''
|
||||||
@@ -15,6 +24,7 @@ class G:
|
|||||||
cached_system_info = {}
|
cached_system_info = {}
|
||||||
cached_compute_devices = []
|
cached_compute_devices = []
|
||||||
results_submitted = False
|
results_submitted = False
|
||||||
|
results_url = ''
|
||||||
|
|
||||||
images = {}
|
images = {}
|
||||||
current_progress = 0.0
|
current_progress = 0.0
|
||||||
@@ -25,6 +35,7 @@ class G:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def reset(cls):
|
def reset(cls):
|
||||||
"""Reset the global state."""
|
"""Reset the global state."""
|
||||||
|
cls.state = G.State.welcome
|
||||||
cls.result_platform = ''
|
cls.result_platform = ''
|
||||||
cls.progress_status = ''
|
cls.progress_status = ''
|
||||||
cls.result_stats = ''
|
cls.result_stats = ''
|
||||||
@@ -32,4 +43,5 @@ class G:
|
|||||||
cls.background_image_path = ""
|
cls.background_image_path = ""
|
||||||
cls.scene_status = {}
|
cls.scene_status = {}
|
||||||
cls.results_submitted = False
|
cls.results_submitted = False
|
||||||
|
cls.results_url = ''
|
||||||
cls.submission_exception = None
|
cls.submission_exception = None
|
||||||
|
@@ -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):
|
def submit_benchmark(benchmark_data: dict):
|
||||||
@@ -22,11 +76,16 @@ def submit_benchmark(benchmark_data: dict):
|
|||||||
bc = BenchmarkClient(mydata_url)
|
bc = BenchmarkClient(mydata_url)
|
||||||
|
|
||||||
# Make sure we have a token; can start the browser to get one.
|
# 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)
|
result = bc.submit_benchmark(benchmark_data)
|
||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
# If we get a location from the MyData server, show it in a browser.
|
# If we get a location from the MyData server, show it in a browser.
|
||||||
if result.location:
|
if result.location:
|
||||||
|
with G.progress_lock:
|
||||||
|
G.results_url = result.location
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open_new_tab(result.location)
|
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)
|
self.log.debug('Finding free port starting at %s', local_addr)
|
||||||
return sockutil.find_free_port(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."""
|
"""Starts the HTTP server, waits for the Token."""
|
||||||
|
|
||||||
if self.auth_token is None:
|
if self.auth_token is None:
|
||||||
|
@@ -7,27 +7,11 @@ import urllib.parse
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from . import timeouts, exceptions
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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:
|
class SubmissionResult:
|
||||||
"""Metadata of the submitted benchmark.
|
"""Metadata of the submitted benchmark.
|
||||||
|
|
||||||
@@ -46,7 +30,6 @@ class SubmissionResult:
|
|||||||
|
|
||||||
|
|
||||||
class BenchmarkClient:
|
class BenchmarkClient:
|
||||||
default_timeout = 30 # seconds
|
|
||||||
|
|
||||||
def __init__(self, mydata_server: str) -> None:
|
def __init__(self, mydata_server: str) -> None:
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
@@ -126,7 +109,7 @@ class BenchmarkClient:
|
|||||||
log.debug('validating token at %s', self.url_verify_token)
|
log.debug('validating token at %s', self.url_verify_token)
|
||||||
resp = self.session.get(self.url_verify_token,
|
resp = self.session.get(self.url_verify_token,
|
||||||
headers={'Authorization': f'Bearer {self.auth_token}'},
|
headers={'Authorization': f'Bearer {self.auth_token}'},
|
||||||
timeout=self.default_timeout)
|
timeout=timeouts.verify)
|
||||||
token_ok = resp.status_code in {200, 204}
|
token_ok = resp.status_code in {200, 204}
|
||||||
if not token_ok:
|
if not token_ok:
|
||||||
log.info('Client token is no longer valid, will obtain another one.')
|
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):
|
if not webbrowser.open_new_tab(url):
|
||||||
raise SystemError(f'Unable to open a browser to visit {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()
|
self._stop_http_server()
|
||||||
|
|
||||||
if self.auth_token:
|
if self.auth_token:
|
||||||
@@ -179,10 +162,10 @@ class BenchmarkClient:
|
|||||||
resp = self.session.post(self.url_submit,
|
resp = self.session.post(self.url_submit,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={'Authorization': f'Bearer {self.auth_token}'},
|
headers={'Authorization': f'Bearer {self.auth_token}'},
|
||||||
timeout=self.default_timeout)
|
timeout=timeouts.submit)
|
||||||
if resp.status_code != 201:
|
if resp.status_code != 201:
|
||||||
log.error('Bad status code %d received: %s', resp.status_code, resp.text)
|
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()
|
result = resp.json()
|
||||||
return SubmissionResult(
|
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