Initial work to return more comprehensive error information when there is a WorkerPool exception.

pull/2908/head
derekpierre 2022-04-07 11:32:34 -04:00
parent 77361e42ab
commit 154a4ac47b
3 changed files with 66 additions and 47 deletions

View File

@ -514,6 +514,7 @@ Some common returned status codes you may encounter are:
- ``400 BAD REQUEST`` -- The server cannot or will not process the request due to something that is perceived to - ``400 BAD REQUEST`` -- The server cannot or will not process the request due to something that is perceived to
be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
- ``401 UNAUTHORIZED`` -- Authentication is required and the request has failed to provide valid authentication credentials. - ``401 UNAUTHORIZED`` -- Authentication is required and the request has failed to provide valid authentication credentials.
- ``404 NOT FOUND`` -- Request could not be completed because requested resources could not be found.
- ``500 INTERNAL SERVER ERROR`` -- The server encountered an unexpected condition that prevented it from - ``500 INTERNAL SERVER ERROR`` -- The server encountered an unexpected condition that prevented it from
fulfilling the request. fulfilling the request.

View File

@ -29,6 +29,7 @@ from hendrix.deploy.base import HendrixDeploy
from hendrix.deploy.tls import HendrixDeployTLS from hendrix.deploy.tls import HendrixDeployTLS
from twisted.internet import reactor, stdio from twisted.internet import reactor, stdio
from nucypher.cli.processes import JSONRPCLineReceiver
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter, WebEmitter from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter, WebEmitter
from nucypher.control.interfaces import ControlInterface from nucypher.control.interfaces import ControlInterface
@ -250,6 +251,7 @@ class WebController(InterfaceControlServer):
_captured_status_codes = {200: 'OK', _captured_status_codes = {200: 'OK',
400: 'BAD REQUEST', 400: 'BAD REQUEST',
404: 'NOT FOUND',
500: 'INTERNAL SERVER ERROR'} 500: 'INTERNAL SERVER ERROR'}
def test_client(self): def test_client(self):
@ -302,8 +304,27 @@ class WebController(InterfaceControlServer):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.handle_request(*args, **kwargs) return self.handle_request(*args, **kwargs)
def handle_request(self, method_name, control_request, *args, **kwargs) -> Response: @staticmethod
def json_response_from_worker_pool_exception(exception):
json_response = {}
if isinstance(exception, WorkerPool.TimedOut):
message_prefix = f"Execution timed out after {exception.timeout}s"
else:
message_prefix = f"Execution failed - no more values to try"
json_response['failure_message'] = f"{message_prefix}; " \
f"{len(exception.failures)} concurrent failures recorded"
if exception.failures:
failures = []
for value, exc_info in exception.failures.items():
failure = {'value': value}
_, exception, tb = exc_info
failure['error'] = str(exception)
failures.append(failure)
json_response['failures'] = failures
return json_response
def handle_request(self, method_name, control_request, *args, **kwargs) -> Response:
_400_exceptions = (SpecificationError, _400_exceptions = (SpecificationError,
TypeError, TypeError,
JSONDecodeError, JSONDecodeError,
@ -336,47 +357,26 @@ class WebController(InterfaceControlServer):
error_message=WebController._captured_status_codes[__exception_code]) error_message=WebController._captured_status_codes[__exception_code])
# #
# Server Errors # Execution Errors
# #
except SpecificationError as e: except WorkerPoolException as e:
__exception_code = 500 # special case since WorkerPoolException contains multiple stack traces
# - not ideal for returning from REST endpoints
__exception_code = 404
if self.crash_on_error: if self.crash_on_error:
raise raise
return self.emitter.exception(
e=e, json_response_from_exception = self.json_response_from_worker_pool_exception(e)
log_level='critical', return self.emitter.exception_with_response(
response_code=__exception_code, json_error_response=json_response_from_exception,
error_message=WebController._captured_status_codes[__exception_code]) e=RuntimeError(json_response_from_exception['failure_message']),
error_message=WebController._captured_status_codes[__exception_code],
log_level='warn',
response_code=__exception_code)
# #
# Unhandled Server Errors # Unhandled Server Errors
# #
except WorkerPoolException as e:
# special case since WorkerPoolException contain stack traces - not ideal for returning from REST endpoints
__exception_code = 500
if self.crash_on_error:
raise
if isinstance(e, WorkerPool.TimedOut):
message_prefix = f"Execution timed out after {e.timeout}s"
else:
message_prefix = f"Execution failed - no more values to try"
# get random failure for context
if e.failures:
value = list(e.failures)[0]
_, exception, _ = e.failures[value]
msg = f"{message_prefix} ({len(e.failures)} concurrent failures recorded); " \
f"for example, for {value}: {exception}"
else:
msg = message_prefix
return self.emitter.exception(
e=RuntimeError(msg),
log_level='warn',
response_code=__exception_code,
error_message=WebController._captured_status_codes[__exception_code])
except Exception as e: except Exception as e:
__exception_code = 500 __exception_code = 500
if self.crash_on_error: if self.crash_on_error:
@ -392,4 +392,4 @@ class WebController(InterfaceControlServer):
# #
else: else:
self.log.debug(f"{method_name} [200 - OK]") self.log.debug(f"{method_name} [200 - OK]")
return self.emitter.respond(response=response) return self.emitter.respond(json_response=response)

View File

@ -243,6 +243,14 @@ class WebEmitter:
self.log = Logger('web-emitter') self.log = Logger('web-emitter')
def _log_exception(self, e, error_message, log_level, response_code):
exception = f"{type(e).__name__}: {str(e)}" if str(e) else type(e).__name__
message = f"{self} [{str(response_code)} - {error_message}] | ERROR: {exception}"
logger = getattr(self.log, log_level)
# See #724 / 2156
message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>")
logger(message_cleaned_for_logger)
@staticmethod @staticmethod
def assemble_response(response: dict) -> dict: def assemble_response(response: dict) -> dict:
response_data = {'result': response, response_data = {'result': response,
@ -255,25 +263,35 @@ class WebEmitter:
log_level: str = 'info', log_level: str = 'info',
response_code: int = 500): response_code: int = 500):
exception = f"{type(e).__name__}: {str(e)}" if str(e) else type(e).__name__ self._log_exception(e, error_message, log_level, response_code)
message = f"{self} [{str(response_code)} - {error_message}] | ERROR: {exception}"
logger = getattr(self.log, log_level)
# See #724 / 2156
message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>")
logger(message_cleaned_for_logger)
if self.crash_on_error: if self.crash_on_error:
raise e raise e
response_message = str(e) or type(e).__name__ response_message = str(e) or type(e).__name__
return self.sink(response_message, status=response_code) return self.sink(response_message, status=response_code)
def respond(self, response) -> Response: def exception_with_response(self,
assembled_response = self.assemble_response(response=response) json_error_response,
e,
error_message: str,
log_level: str = 'info',
response_code: int = 500):
self._log_exception(e, error_message, log_level, response_code)
if self.crash_on_error:
raise e
assembled_response = self.assemble_response(response=json_error_response)
serialized_response = WebEmitter.transport_serializer(assembled_response) serialized_response = WebEmitter.transport_serializer(assembled_response)
# ---------- HTTP OUTPUT json_response = self.sink(response=serialized_response, status=response_code, content_type="application/json")
response = self.sink(response=serialized_response, status=HTTPStatus.OK, content_type="application/json") return json_response
return response
def respond(self, json_response) -> Response:
assembled_response = self.assemble_response(response=json_response)
serialized_response = WebEmitter.transport_serializer(assembled_response)
json_response = self.sink(response=serialized_response, status=HTTPStatus.OK, content_type="application/json")
return json_response
def get_stream(self, *args, **kwargs): def get_stream(self, *args, **kwargs):
return null_stream() return null_stream()