def __init__(self, settings): """ :param settings: The settings object, which must be an instance of `ServerSettings` or one of its subclasses :type settings: ServerSettings """ # Check subclassing setup if not self.service_name: raise AttributeError('Server subclass must set service_name') self._async_event_loop = None self._async_event_loop_thread = None if AsyncEventLoopThread: self._async_event_loop_thread = AsyncEventLoopThread() self._async_event_loop = self._async_event_loop_thread.loop # Store settings and extract transport self.settings = settings self.metrics = self.settings['metrics']['object']( **self.settings['metrics'].get('kwargs', {})) self.transport = self.settings['transport']['object']( self.service_name, self.metrics, **self.settings['transport'].get('kwargs', {})) # Set initial state self.shutting_down = False # Instantiate middleware self.middleware = [ m['object'](**m.get('kwargs', {})) for m in self.settings['middleware'] ] # Set up logger # noinspection PyTypeChecker self.logger = logging.getLogger('pysoa.server') # noinspection PyTypeChecker self.job_logger = logging.getLogger('pysoa.server.job') # Set these as the integer equivalents of the level names self.request_log_success_level = logging.getLevelName( self.settings['request_log_success_level']) self.request_log_error_level = logging.getLevelName( self.settings['request_log_error_level']) class DictWrapper(RecursivelyCensoredDictWrapper): SENSITIVE_FIELDS = frozenset( RecursivelyCensoredDictWrapper.SENSITIVE_FIELDS | settings['extra_fields_to_redact'], ) self.logging_dict_wrapper_class = DictWrapper self._default_status_action_class = None self._idle_timer = None self._heartbeat_file = None self._heartbeat_file_path = None self._heartbeat_file_last_update = 0
class TestAsyncEventLoopThread(unittest.TestCase): def setUp(self): super(TestAsyncEventLoopThread, self).setUp() self.thread = AsyncEventLoopThread([]) self.thread.start() def tearDown(self): self.thread.join() def test_loop_runs_in_another_thread(self): async def test_threads(thread): # noqa E999 assert thread is not threading.current_thread() future = self.thread.run_coroutine( test_threads(threading.current_thread())) future.result() def test_loop_executes_pending_tasks_before_close(self): async def test_execs_pending(): # noqa E999 await asyncio.sleep(1) return 1 def callback(_): assert future.done() assert future.result() == 1 future = self.thread.run_coroutine(test_execs_pending()) future.add_done_callback(callback) def test_join_stops_and_closes_loop(self): self.thread.join() assert not self.thread.loop.is_running() assert self.thread.loop.is_closed()
async def test_default_coroutine_middleware(): class SpecialError(Exception): pass context = {'i': 0} # noinspection PyCompatibility async def coroutine(): context['i'] += 1 if context['i'] > 2: return 'bar_returned_by_coroutine' raise SpecialError(context['i']) thread = AsyncEventLoopThread([DefaultCoroutineMiddleware()]) thread.start() with mock.patch('logging.Logger.exception') as mock_log_exception: future = thread.run_coroutine(coroutine()) await asyncio.sleep(0.01) with pytest.raises(SpecialError) as error_context: future.result() assert error_context.value.args[0] == 1 mock_log_exception.assert_called_once_with( 'Error occurred while awaiting coroutine in request.run_coroutine') mock_log_exception.reset_mock() future = thread.run_coroutine(coroutine()) await asyncio.sleep(0.01) with pytest.raises(SpecialError) as error_context: future.result() assert error_context.value.args[0] == 2 mock_log_exception.assert_called_once_with( 'Error occurred while awaiting coroutine in request.run_coroutine') mock_log_exception.reset_mock() future = thread.run_coroutine(coroutine()) await asyncio.sleep(0.01) assert future.result() == 'bar_returned_by_coroutine' assert mock_log_exception.call_count == 0 thread.join()
async def test_coroutine_middleware(): before_call_trace.clear() create_call_trace.clear() run_call_trace_pre.clear() run_call_trace_post.clear() var = contextvars.ContextVar( 'caller_var') # type: contextvars.ContextVar[str] var.set('yes man') test_context = { 'caller_var': None, 'middleware_var': None } # type: Dict[str, Any] # noinspection PyCompatibility async def coroutine(): run_call_trace_pre.append('target') test_context['caller_var'] = var.get('default_cv') for context_var in contextvars.copy_context().keys(): if context_var.name == 'middleware_var': test_context['middleware_var'] = context_var.get('default_mv') await asyncio.sleep(0.05) run_call_trace_post.append('target') return 'foo_coroutine_returned_this' thread = AsyncEventLoopThread([ SpecialCoroutineMiddleware(42), TracingCoroutineMiddleware(), ]) thread.start() future = thread.run_coroutine(coroutine()) await asyncio.sleep(0.01) assert future.result() == 'foo_coroutine_returned_this' thread.join() assert before_call_trace == [ 'SpecialCoroutineMiddleware', 'TracingCoroutineMiddleware' ] assert create_call_trace == [ 'TracingCoroutineMiddleware', 'SpecialCoroutineMiddleware' ] assert run_call_trace_pre == [ 'SpecialCoroutineMiddleware', 'TracingCoroutineMiddleware', 'target' ] assert run_call_trace_post == [ 'target', 'TracingCoroutineMiddleware', 'SpecialCoroutineMiddleware' ] assert test_context['caller_var'] == 'yes man' assert test_context['middleware_var'] == 42
async def test_default_coroutine_middleware(): class SpecialError(Exception): pass # noinspection PyCompatibility async def coroutine(): raise SpecialError() thread = AsyncEventLoopThread([DefaultCoroutineMiddleware()]) thread.start() with mock.patch('logging.Logger.exception') as mock_log_exception: thread.run_coroutine(coroutine()) await asyncio.sleep(0.2) thread.join() mock_log_exception.assert_called_once_with( 'Error occurred while awaiting coroutine in request.run_coroutine')
def setUp(self): super(TestAsyncEventLoopThread, self).setUp() self.thread = AsyncEventLoopThread([]) self.thread.start()
class Server(object): """ The base class from which all PySOA service servers inherit, and contains the code that does all of the heavy lifting for receiving and handling requests, passing those requests off to the relevant actions, and sending the actions' responses back to the caller. Required attributes that all concrete subclasses must provide: - `service_name`: A (unicode) string name of the service. - `action_class_map`: An object supporting `__contains__` and `__getitem__` (typically a `dict`) whose keys are action names and whose values are callable objects that return a callable action when called (such as subclasses of `Action` which, when "called" [constructed], yield a callable object [instance of the subclass]) """ settings_class = PolymorphicServerSettings use_django = False service_name = None action_class_map = {} def __init__(self, settings): """ :param settings: The settings object, which must be an instance of `ServerSettings` or one of its subclasses :type settings: ServerSettings """ # Check subclassing setup if not self.service_name: raise AttributeError('Server subclass must set service_name') self._async_event_loop = None self._async_event_loop_thread = None if AsyncEventLoopThread: self._async_event_loop_thread = AsyncEventLoopThread() self._async_event_loop = self._async_event_loop_thread.loop # Store settings and extract transport self.settings = settings self.metrics = self.settings['metrics']['object']( **self.settings['metrics'].get('kwargs', {})) self.transport = self.settings['transport']['object']( self.service_name, self.metrics, **self.settings['transport'].get('kwargs', {})) # Set initial state self.shutting_down = False # Instantiate middleware self.middleware = [ m['object'](**m.get('kwargs', {})) for m in self.settings['middleware'] ] # Set up logger # noinspection PyTypeChecker self.logger = logging.getLogger('pysoa.server') # noinspection PyTypeChecker self.job_logger = logging.getLogger('pysoa.server.job') # Set these as the integer equivalents of the level names self.request_log_success_level = logging.getLevelName( self.settings['request_log_success_level']) self.request_log_error_level = logging.getLevelName( self.settings['request_log_error_level']) class DictWrapper(RecursivelyCensoredDictWrapper): SENSITIVE_FIELDS = frozenset( RecursivelyCensoredDictWrapper.SENSITIVE_FIELDS | settings['extra_fields_to_redact'], ) self.logging_dict_wrapper_class = DictWrapper self._default_status_action_class = None self._idle_timer = None self._heartbeat_file = None self._heartbeat_file_path = None self._heartbeat_file_last_update = 0 def handle_next_request(self): """ Retrieves the next request from the transport, or returns if it times out (no request has been made), and then processes that request, sends its response, and returns when done. """ if not self._idle_timer: # This method may be called multiple times before receiving a request, so we only create and start a timer # if it's the first call or if the idle timer was stopped on the last call. self._idle_timer = self.metrics.timer( 'server.idle_time', resolution=TimerResolution.MICROSECONDS) self._idle_timer.start() # Get the next JobRequest try: request_id, meta, job_request = self.transport.receive_request_message( ) except MessageReceiveTimeout: # no new message, nothing to do self.perform_idle_actions() return # We are no longer idle, so stop the timer and reset for the next idle period self._idle_timer.stop() self._idle_timer = None try: PySOALogContextFilter.set_logging_request_context( request_id=request_id, **job_request.get('context', {})) except TypeError: # Non unicode keys in job_request['context'] will break keywording of a function call. # Try to recover by coercing the keys PySOALogContextFilter.set_logging_request_context( request_id=request_id, **{ six.text_type(k): v for k, v in six.iteritems(job_request['context']) }) request_for_logging = self.logging_dict_wrapper_class(job_request) self.job_logger.log(self.request_log_success_level, 'Job request: %s', request_for_logging) try: self.perform_pre_request_actions() # Process and run the Job job_response = self.process_job(job_request) # Prepare the JobResponse for sending by converting it to a message dict try: response_message = attr.asdict(job_response, dict_factory=UnicodeKeysDict) except Exception as e: self.metrics.counter( 'server.error.response_conversion_failure').increment() job_response = self.handle_job_exception( e, variables={'job_response': job_response}) response_message = attr.asdict(job_response, dict_factory=UnicodeKeysDict) response_for_logging = self.logging_dict_wrapper_class( response_message) # Send the response message try: if not job_request.get('control', {}).get( 'suppress_response', False): self.transport.send_response_message( request_id, meta, response_message) except MessageTooLarge as e: self.metrics.counter( 'server.error.response_too_large').increment() job_response = self.handle_job_error_code( ERROR_CODE_RESPONSE_TOO_LARGE, 'Could not send the response because it was too large', request_for_logging, response_for_logging, extra={ 'serialized_length_in_bytes': e.message_size_in_bytes }, ) self.transport.send_response_message( request_id, meta, attr.asdict(job_response, dict_factory=UnicodeKeysDict), ) except InvalidField: self.metrics.counter( 'server.error.response_not_serializable').increment() job_response = self.handle_job_error_code( ERROR_CODE_RESPONSE_NOT_SERIALIZABLE, 'Could not send the response because it failed to serialize', request_for_logging, response_for_logging, ) self.transport.send_response_message( request_id, meta, attr.asdict(job_response, dict_factory=UnicodeKeysDict), ) finally: if job_response.errors or any(a.errors for a in job_response.actions): if (self.request_log_error_level > self.request_log_success_level and self.job_logger.getEffectiveLevel() > self.request_log_success_level): # When we originally logged the request, it may have been hidden because the effective logging # level threshold was greater than the level at which we logged the request. So re-log the # request at the error level, if set higher. self.job_logger.log(self.request_log_error_level, 'Job request: %s', request_for_logging) self.job_logger.log(self.request_log_error_level, 'Job response: %s', response_for_logging) else: self.job_logger.log(self.request_log_success_level, 'Job response: %s', response_for_logging) finally: PySOALogContextFilter.clear_logging_request_context() self.perform_post_request_actions() def make_client(self, context): """ Gets a `Client` that will propagate the passed `context` in order to to pass it down to middleware or Actions. :return: A client configured with this server's `client_routing` settings :rtype: Client """ return Client(self.settings['client_routing'], context=context) @staticmethod def make_middleware_stack(middleware, base): """ Given a list of in-order middleware callable objects `middleware` and a base function `base`, chains them together so each middleware is fed the function below, and returns the top level ready to call. :param middleware: The middleware stack :type middleware: iterable[callable] :param base: The base callable that the lowest-order middleware wraps :type base: callable :return: The topmost middleware, which calls the next middleware ... which calls the lowest-order middleware, which calls the `base` callable. :rtype: callable """ for ware in reversed(middleware): base = ware(base) return base def process_job(self, job_request): """ Validate, execute, and run the job request, wrapping it with any applicable job middleware. :param job_request: The job request :type job_request: dict :return: A `JobResponse` object :rtype: JobResponse :raise: JobError """ try: # Validate JobRequest message validation_errors = [ Error( code=error.code, message=error.message, field=error.pointer, ) for error in (JobRequestSchema.errors(job_request) or []) ] if validation_errors: raise JobError(errors=validation_errors) # Add the client object in case a middleware wishes to use it job_request['client'] = self.make_client(job_request['context']) # Add the async event loop in case a middleware wishes to use it job_request['async_event_loop'] = self._async_event_loop if self._async_event_loop_thread: job_request[ 'run_coroutine'] = self._async_event_loop_thread.run_coroutine else: job_request['run_coroutine'] = None # Build set of middleware + job handler, then run job wrapper = self.make_middleware_stack( [m.job for m in self.middleware], self.execute_job, ) job_response = wrapper(job_request) if 'correlation_id' in job_request['context']: job_response.context['correlation_id'] = job_request[ 'context']['correlation_id'] except JobError as e: self.metrics.counter('server.error.job_error').increment() job_response = JobResponse(errors=e.errors, ) except Exception as e: # Send an error response if no middleware caught this. # Formatting the error might itself error, so try to catch that self.metrics.counter('server.error.unhandled_error').increment() return self.handle_job_exception(e) return job_response def handle_job_exception(self, exception, variables=None): """ Makes and returns a last-ditch error response. :param exception: The exception that happened :type exception: Exception :param variables: A dictionary of context-relevant variables to include in the error response :type variables: dict :return: A `JobResponse` object :rtype: JobResponse """ # Get the error and traceback if we can # noinspection PyBroadException try: error_str, traceback_str = six.text_type( exception), traceback.format_exc() except Exception: self.metrics.counter( 'server.error.error_formatting_failure').increment() error_str, traceback_str = 'Error formatting error', traceback.format_exc( ) # Log what happened self.logger.exception(exception) if not isinstance(traceback_str, six.text_type): try: # Try to traceback_str = traceback_str.decode('utf-8') except UnicodeDecodeError: traceback_str = 'UnicodeDecodeError: Traceback could not be decoded' # Make a bare bones job response error_dict = { 'code': ERROR_CODE_SERVER_ERROR, 'message': 'Internal server error: %s' % error_str, 'traceback': traceback_str, } if variables is not None: # noinspection PyBroadException try: error_dict['variables'] = { key: repr(value) for key, value in variables.items() } except Exception: self.metrics.counter( 'server.error.variable_formatting_failure').increment() error_dict['variables'] = 'Error formatting variables' return JobResponse(errors=[error_dict]) def handle_job_error_code(self, code, message, request_for_logging, response_for_logging, extra=None): log_extra = { 'data': { 'request': request_for_logging, 'response': response_for_logging } } if extra: log_extra['data'].update(extra) self.logger.error( message, exc_info=True, extra=log_extra, ) return JobResponse(errors=[Error(code=code, message=message)]) def execute_job(self, job_request): """ Processes and runs the action requests contained in the job and returns a `JobResponse`. :param job_request: The job request :type job_request: dict :return: A `JobResponse` object :rtype: JobResponse """ # Run the Job's Actions job_response = JobResponse() job_switches = RequestSwitchSet(job_request['context']['switches']) for i, raw_action_request in enumerate(job_request['actions']): action_request = EnrichedActionRequest( action=raw_action_request['action'], body=raw_action_request.get('body', None), switches=job_switches, context=job_request['context'], control=job_request['control'], client=job_request['client'], async_event_loop=job_request['async_event_loop'], run_coroutine=job_request['run_coroutine'], ) action_in_class_map = action_request.action in self.action_class_map if action_in_class_map or action_request.action in ('status', 'introspect'): # Get action to run if action_in_class_map: action = self.action_class_map[action_request.action]( self.settings) elif action_request.action == 'introspect': from pysoa.server.action.introspection import IntrospectionAction action = IntrospectionAction(server=self) else: if not self._default_status_action_class: from pysoa.server.action.status import make_default_status_action_class self._default_status_action_class = make_default_status_action_class( self.__class__) action = self._default_status_action_class(self.settings) # Wrap it in middleware wrapper = self.make_middleware_stack( [m.action for m in self.middleware], action, ) # Execute the middleware stack try: action_response = wrapper(action_request) except ActionError as e: # Error: an error was thrown while running the Action (or Action middleware) action_response = ActionResponse( action=action_request.action, errors=e.errors, ) else: # Error: Action not found. action_response = ActionResponse( action=action_request.action, errors=[ Error( code=ERROR_CODE_UNKNOWN, message= 'The action "{}" was not found on this server.'. format(action_request.action), field='action', ) ], ) job_response.actions.append(action_response) if (action_response.errors and not job_request['control'].get( 'continue_on_error', False)): # Quit running Actions if an error occurred and continue_on_error is False break return job_response def handle_shutdown_signal(self, *_): """ Handles the reception of a shutdown signal. """ if self.shutting_down: self.logger.warning('Received double interrupt, forcing shutdown') sys.exit(1) else: self.logger.warning('Received interrupt, initiating shutdown') self.shutting_down = True def harakiri(self, *_): """ Handles the reception of a timeout signal indicating that a request has been processing for too long, as defined by the Harakiri settings. """ if self.shutting_down: self.logger.warning( 'Graceful shutdown failed after {}s. Exiting now!'.format( self.settings['harakiri']['shutdown_grace'])) sys.exit(1) else: self.logger.warning( 'No activity during {}s, triggering harakiri with grace {}s'. format( self.settings['harakiri']['timeout'], self.settings['harakiri']['shutdown_grace'], )) self.shutting_down = True signal.alarm(self.settings['harakiri']['shutdown_grace']) def setup(self): """ Runs just before the server starts, if you need to do one-time loads or cache warming. Call super().setup() if you override. See the documentation for `Server.main` for full details on the chain of `Server` method calls. """ def _close_old_django_connections(self): if self.use_django: if not getattr(django_settings, 'DATABASES'): # No database connections are configured, so we have nothing to do return try: # noinspection PyCallingNonCallable if django_get_autocommit(): self.logger.debug('Cleaning Django connections') # noinspection PyCallingNonCallable django_close_old_connections() except DatabaseError: # Sometimes old connections won't close because they have already been interrupted (timed out, server # moved, etc.). There's no reason to interrupt server processes for this problem. We can continue on # without issue. pass except BaseException as e: # `get_autocommit` fails under PyTest without `pytest.mark.django_db`, so ignore that specific error. try: from _pytest.outcomes import Failed if not isinstance(e, Failed): raise e except ImportError: # But if we can't import PyTest, then it can't be that error, so raise raise e def _close_django_caches(self, shutdown=False): if self.use_django and django_caches: if shutdown: self.logger.info('Closing all Django caches') for cache in django_caches.all(): cache.close(for_shutdown=shutdown) def _create_heartbeat_file(self): if self.settings['heartbeat_file']: self.logger.info('Creating heartbeat file') self._heartbeat_file_path = os.path.abspath( self.settings['heartbeat_file'].replace( '{{pid}}', six.text_type(os.getpid())), ) self._heartbeat_file = codecs.open(self._heartbeat_file_path, 'wb', encoding='utf-8') self._update_heartbeat_file() def _delete_heartbeat_file(self): if self._heartbeat_file: self.logger.info('Closing and removing heartbeat file') # noinspection PyBroadException try: self._heartbeat_file.close() except Exception: self.logger.warning('Error while closing heartbeat file', exc_info=True) finally: # noinspection PyBroadException try: os.remove(self._heartbeat_file_path) except Exception: self.logger.warning('Error while removing heartbeat file', exc_info=True) def _update_heartbeat_file(self): if self._heartbeat_file and time.time( ) - self._heartbeat_file_last_update > 2.5: # Only update the heartbeat file if one is configured and it has been at least 2.5 seconds since the last # update. This prevents us from dragging down service performance by constantly updating the file system. self._heartbeat_file.seek(0) self._heartbeat_file.write(six.text_type(time.time())) self._heartbeat_file.flush() self._heartbeat_file_last_update = time.time() def perform_pre_request_actions(self): """ Runs just before the server accepts a new request. Call super().perform_pre_request_actions() if you override. Be sure your purpose for overriding isn't better met with middleware. See the documentation for `Server.main` for full details on the chain of `Server` method calls. """ if self.use_django: if getattr(django_settings, 'DATABASES'): self.logger.debug('Resetting Django query log') # noinspection PyCallingNonCallable django_reset_queries() self._close_old_django_connections() def perform_post_request_actions(self): """ Runs just after the server processes a request. Call super().perform_post_request_actions() if you override. Be sure your purpose for overriding isn't better met with middleware. See the documentation for `Server.main` for full details on the chain of `Server` method calls. """ self._close_old_django_connections() self._close_django_caches() self._update_heartbeat_file() def perform_idle_actions(self): """ Runs periodically when the server is idle, if it has been too long since it last received a request. Call super().perform_idle_actions() if you override. See the documentation for `Server.main` for full details on the chain of `Server` method calls. """ self._close_old_django_connections() self._update_heartbeat_file() def run(self): """ Starts the server run loop and returns after the server shuts down due to a shutdown-request, Harakiri signal, or unhandled exception. See the documentation for `Server.main` for full details on the chain of `Server` method calls. """ self.logger.info( 'Service "{service}" server starting up, pysoa version {pysoa}, listening on transport {transport}.' .format( service=self.service_name, pysoa=pysoa.version.__version__, transport=self.transport, )) self.setup() self.metrics.commit() if self._async_event_loop_thread: self._async_event_loop_thread.start() self._create_heartbeat_file() signal.signal(signal.SIGINT, self.handle_shutdown_signal) signal.signal(signal.SIGTERM, self.handle_shutdown_signal) signal.signal(signal.SIGALRM, self.harakiri) # noinspection PyBroadException try: while not self.shutting_down: # reset harakiri timeout signal.alarm(self.settings['harakiri']['timeout']) # Get, process, and execute the next JobRequest self.handle_next_request() self.metrics.commit() except MessageReceiveError: self.logger.exception( 'Error receiving message from transport; shutting down') except Exception: self.metrics.counter('server.error.unknown').increment() self.logger.exception('Unhandled server error; shutting down') finally: self.metrics.commit() self.logger.info('Server shutting down') if self._async_event_loop_thread: self._async_event_loop_thread.join() self._close_django_caches(shutdown=True) self._delete_heartbeat_file() self.logger.info('Server shutdown complete') # noinspection PyUnusedLocal @classmethod def initialize(cls, settings): """ Called just before the `Server` class is instantiated, and passed the settings dict. Can be used to perform settings manipulation, server class patching (such as for performance tracing operations), and more. Use with great care and caution. Overriding methods must call `super` and return `cls` or a new/modified `cls`, which will be used to instantiate the server. See the documentation for `Server.main` for full details on the chain of `Server` method calls. :return: The server class or a new/modified server class :rtype: type """ return cls @classmethod def main(cls): """ Command-line entry point for running a PySOA server. The chain of method calls is as follows:: cls.main | -> cls.initialize => new_cls -> new_cls.__init__ => self -> self.run | -> self.setup -> loop: self.handle_next_request while not self.shutting_down | -> transport.receive_request_message -> self.perform_idle_actions (if no request) -> self.perform_pre_request_actions -> self.process_job | -> middleware(self.execute_job) -> transport.send_response_message -> self.perform_post_request_actions """ parser = argparse.ArgumentParser( description='Server for the {} SOA service'.format( cls.service_name), ) parser.add_argument( '-d', '--daemon', action='store_true', help='run the server process as a daemon', ) if not cls.use_django: # If Django mode is turned on, we use the Django settings framework to get our settings, so the caller # needs to set DJANGO_SETTINGS_MODULE. Otherwise, the caller must pass in the -s/--settings argument. parser.add_argument( '-s', '--settings', help='The settings module to use', required=True, ) cmd_options, _ = parser.parse_known_args(sys.argv[1:]) # Load settings from the given file (or use Django and grab from its settings) if cls.use_django: # noinspection PyUnresolvedReferences if not django_settings: raise ImportError( 'Could not import Django. You must install Django if you enable Django support in your service.' ) try: settings = cls.settings_class( django_settings.SOA_SERVER_SETTINGS) except AttributeError: raise ValueError( 'Cannot find `SOA_SERVER_SETTINGS` in the Django settings.' ) else: try: settings_module = importlib.import_module(cmd_options.settings) except ImportError as e: raise ValueError('Cannot import settings module `%s`: %s' % (cmd_options.settings, e)) try: settings_dict = getattr(settings_module, 'SOA_SERVER_SETTINGS') except AttributeError: try: settings_dict = getattr(settings_module, 'settings') except AttributeError: raise ValueError( "Cannot find `SOA_SERVER_SETTINGS` or `settings` variable in settings module `{}`." .format(cmd_options.settings, )) settings = cls.settings_class(settings_dict) PySOALogContextFilter.set_service_name(cls.service_name) # Set up logging logging.config.dictConfig(settings['logging']) # Optionally daemonize if cmd_options.daemon: pid = os.fork() if pid > 0: print('PID={}'.format(pid)) sys.exit() # Set up server and signal handling server = cls.initialize(settings)(settings) # Start server event loop server.run()
def __init__(self, settings, forked_process_id=None): """ :param settings: The settings object, which must be an instance of `ServerSettings` or one of its subclasses :type settings: ServerSettings :param forked_process_id: If multiple processes are forked by the same parent process, this will be set to a unique, deterministic (incremental) ID which can be used in logging, the heartbeat file, etc. For example, if the `--fork` argument is used with the value 5 (creating five child processes), this argument will have the values 1, 2, 3, 4, and 5 across the five respective child processes. :type forked_process_id: int """ # Check subclassing setup if not self.service_name: raise AttributeError('Server subclass must set service_name') # Store settings and extract transport self.settings = settings self.metrics = self.settings['metrics']['object'](**self.settings['metrics'].get('kwargs', {})) self.transport = self.settings['transport']['object']( self.service_name, self.metrics, **self.settings['transport'].get('kwargs', {}) ) self._async_event_loop = None self._async_event_loop_thread = None if AsyncEventLoopThread: self._async_event_loop_thread = AsyncEventLoopThread([ m['object'](**m.get('kwargs', {})) for m in self.settings['coroutine_middleware'] ]) self._async_event_loop = self._async_event_loop_thread.loop # Set initial state self.shutting_down = False self._shutdown_lock = threading.Lock() self._last_signal = 0 self._last_signal_received = 0 # Instantiate middleware self.middleware = [ m['object'](**m.get('kwargs', {})) for m in self.settings['middleware'] ] # Set up logger # noinspection PyTypeChecker self.logger = logging.getLogger('pysoa.server') # noinspection PyTypeChecker self.job_logger = logging.getLogger('pysoa.server.job') # Set these as the integer equivalents of the level names self.request_log_success_level = logging.getLevelName(self.settings['request_log_success_level']) self.request_log_error_level = logging.getLevelName(self.settings['request_log_error_level']) class DictWrapper(RecursivelyCensoredDictWrapper): SENSITIVE_FIELDS = frozenset( RecursivelyCensoredDictWrapper.SENSITIVE_FIELDS | settings['extra_fields_to_redact'], ) self.logging_dict_wrapper_class = DictWrapper self._default_status_action_class = None self._idle_timer = None self._heartbeat_file = None self._heartbeat_file_path = None self._heartbeat_file_last_update = 0 self._forked_process_id = forked_process_id
def setUp(self): super(AsyncEventLoopThreadTests, self).setUp() self.thread = AsyncEventLoopThread() self.thread.start()