def start(self): # type: () -> None if self._running: raise Exception('PluginRunner is already running') self.logger('[Runner] Starting') python_executable = sys.executable if python_executable is None or len(python_executable) == 0: python_executable = '/usr/bin/python' self._proc = subprocess.Popen([ python_executable, 'runtime.py', 'start_plugin', self.plugin_path ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, cwd=self.runtime_path, close_fds=True) assert self._proc.stdout, 'Plugin stdout not available' self._process_running = True self._commands_executed = 0 self._commands_failed = 0 assert self._proc.stdin, 'Plugin stdin not defined' self._writer = PluginIPCWriter(stream=self._proc.stdin) self._reader = PluginIPCReader(stream=self._proc.stdout, logger=lambda message, ex: self.logger( '{0}: {1}'.format(message, ex)), command_receiver=self._process_command, name=self.name) self._reader.start() start_out = self._do_command('start', timeout=180) self.name = start_out['name'] self.version = start_out['version'] self.interfaces = start_out['interfaces'] self._decorators_in_use = start_out['decorators'] self._exposes = start_out['exposes'] self._metric_collectors = start_out['metric_collectors'] self._metric_receivers = start_out['metric_receivers'] exception = start_out.get('exception') if exception is not None: raise RuntimeError(exception) self._async_command_queue = Queue(1000) self._async_command_thread = BaseThread( name='plugincmd{0}'.format(self.plugin_path), target=self._perform_async_commands) self._async_command_thread.daemon = True self._async_command_thread.start() self._running = True if self._state_callback is not None: self._state_callback(self.name, PluginRunner.State.RUNNING) self.logger('[Runner] Started')
def __init__(self, path): # type: (str) -> None self._stopped = False self._path = path.rstrip('/') self._decorated_methods = { 'input_status': [], 'output_status': [], 'shutter_status': [], 'thermostat_status': [], 'thermostat_group_status': [], 'ventilation_status': [], 'receive_events': [], 'background_task': [], 'on_remove': [] } # type: Dict[str,List[Any]] self._name = None self._version = None self._interfaces = [] # type: List[Any] self._exposes = [] # type: List[Any] self._metric_definitions = [] # type: List[Any] self._metric_collectors = [] # type: List[Any] self._metric_receivers = [] # type: List[Any] self._plugin = None self._writer = PluginIPCWriter(os.fdopen(sys.stdout.fileno(), 'wb', 0)) self._reader = PluginIPCReader(os.fdopen(sys.stdin.fileno(), 'rb', 0), self._writer.log_exception) config = ConfigParser() config.read(constants.get_config_file()) try: http_port = int(config.get('OpenMotics', 'http_port')) except (NoSectionError, NoOptionError): http_port = 80 self._webinterface = WebInterfaceDispatcher(self._writer.log, port=http_port)
class PluginRuntime(object): SUPPORTED_DECORATOR_VERSIONS = { 'input_status': [1, 2], 'output_status': [1, 2], 'shutter_status': [1, 2, 3], 'thermostat_status': [1], 'thermostat_group_status': [1], 'ventilation_status': [1], 'receive_events': [1], 'background_task': [1], 'on_remove': [1] } def __init__(self, path): # type: (str) -> None self._stopped = False self._path = path.rstrip('/') self._decorated_methods = { 'input_status': [], 'output_status': [], 'shutter_status': [], 'thermostat_status': [], 'thermostat_group_status': [], 'ventilation_status': [], 'receive_events': [], 'background_task': [], 'on_remove': [] } # type: Dict[str,List[Any]] self._name = None self._version = None self._interfaces = [] # type: List[Any] self._exposes = [] # type: List[Any] self._metric_definitions = [] # type: List[Any] self._metric_collectors = [] # type: List[Any] self._metric_receivers = [] # type: List[Any] self._plugin = None self._writer = PluginIPCWriter(os.fdopen(sys.stdout.fileno(), 'wb', 0)) self._reader = PluginIPCReader(os.fdopen(sys.stdin.fileno(), 'rb', 0), self._writer.log_exception) config = ConfigParser() config.read(constants.get_config_file()) try: http_port = int(config.get('OpenMotics', 'http_port')) except (NoSectionError, NoOptionError): http_port = 80 self._webinterface = WebInterfaceDispatcher(self._writer.log, port=http_port) def _init_plugin(self): # type: () -> None plugin_root = os.path.dirname(self._path) plugin_dir = os.path.basename(self._path) # Add the plugin and it's eggs to the python path sys.path.insert(0, plugin_root) for egg_file in os.listdir(self._path): if egg_file.endswith('.egg'): sys.path.append(os.path.join(self._path, egg_file)) # Expose plugins.base to the plugin sys.modules['plugins'] = sys.modules['__main__'] sys.modules["plugins.base"] = base # Instanciate the plugin class plugin_class = get_plugin_class(plugin_dir) check_plugin(plugin_class) # Set the name, version, interfaces self._name = plugin_class.name self._version = plugin_class.version self._interfaces = plugin_class.interfaces # Initialze the plugin self._plugin = plugin_class(self._webinterface, self._writer.log) for decorator_name, decorated_methods in six.iteritems( self._decorated_methods): for decorated_method, decorator_version in get_special_methods( self._plugin, decorator_name): # only add if supported, raise if an unsupported version is found if decorator_version not in PluginRuntime.SUPPORTED_DECORATOR_VERSIONS[ decorator_name]: raise NotImplementedError( 'Decorator {} version {} is not supported'.format( decorator_name, decorator_version)) decorated_methods.append( decorated_method) # add the decorated method to the list # Set the exposed methods for decorated_method, _ in get_special_methods(self._plugin, 'om_expose'): self._exposes.append({ 'name': decorated_method.__name__, 'auth': decorated_method.om_expose['auth'], 'content_type': decorated_method.om_expose['content_type'] }) # Set the metric collectors for decorated_method, _ in get_special_methods(self._plugin, 'om_metric_data'): self._metric_collectors.append({ 'name': decorated_method.__name__, 'interval': decorated_method.om_metric_data['interval'] }) # Set the metric receivers for decorated_method, _ in get_special_methods(self._plugin, 'om_metric_receive'): self._metric_receivers.append({ 'name': decorated_method.__name__, 'source': decorated_method.om_metric_receive['source'], 'metric_type': decorated_method.om_metric_receive['metric_type'], 'interval': decorated_method.om_metric_receive['interval'] }) # Set the metric definitions if has_interface(plugin_class, 'metrics', '1.0'): if hasattr(plugin_class, 'metric_definitions'): self._metric_definitions = plugin_class.metric_definitions def _start_background_tasks(self): # type: () -> None """ Start all background tasks. """ for decorated_method in self._decorated_methods['background_task']: thread = BaseThread(name='plugin{}'.format( decorated_method.__name__), target=self._run_background_task, args=(decorated_method, )) thread.daemon = True thread.start() def get_decorators_in_use(self): registered_decorators = {} for decorator_name, decorated_methods in six.iteritems( self._decorated_methods): decorator_versions_in_use = set() for decorated_method in decorated_methods: decorator_version = getattr(decorated_method, decorator_name).get('version', 1) decorator_versions_in_use.add(decorator_version) registered_decorators[decorator_name] = list( decorator_versions_in_use) # something in the form of e.g. {'output_status': [1,2], 'input_status': [1]} where 1,2,... are the versions return registered_decorators def _run_background_task(self, task): # type: (Callable[[],None]) -> None running = True while running: try: task() running = False # Stop execution if the task returns without exception except Exception as exception: self._writer.log_exception('background task', exception) time.sleep(30) def process_stdin(self): # type: () -> None self._reader.start() while not self._stopped: command = self._reader.get(block=True) if command is None: continue action = command['action'] action_version = command['action_version'] response = {'cid': command['cid'], 'action': action} try: ret = None if action == 'start': ret = self._handle_start() elif action == 'stop': ret = self._handle_stop() elif action == 'input_status': ret = self._handle_input_status(command['event']) elif action == 'output_status': # v1 = state, v2 = event if action_version == 1: ret = self._handle_output_status(command['status'], data_type='status') else: ret = self._handle_output_status(command['event'], data_type='event') elif action == 'ventilation_status': ret = self._handle_ventilation_status(command['event']) elif action == 'thermostat_status': ret = self._handle_thermostat_status(command['event']) elif action == 'thermostat_group_status': ret = self._handle_thermostat_group_status( command['event']) elif action == 'shutter_status': # v1 = state as list, v2 = state as dict, v3 = event if action_version == 1: ret = self._handle_shutter_status(command['status'], data_type='status') elif action_version == 2: ret = self._handle_shutter_status( command['status'], data_type='status_dict') else: ret = self._handle_shutter_status(command['event'], data_type='event') elif action == 'receive_events': ret = self._handle_receive_events(command['code']) elif action == 'get_metric_definitions': ret = self._handle_get_metric_definitions() elif action == 'collect_metrics': ret = self._handle_collect_metrics(command['name']) elif action == 'distribute_metrics': ret = self._handle_distribute_metrics( command['name'], command['metrics']) elif action == 'request': ret = self._handle_request(command['method'], command['args'], command['kwargs']) elif action == 'remove_callback': ret = self._handle_remove_callback() elif action == 'ping': pass # noop else: raise RuntimeError('Unknown action: {0}'.format(action)) if ret is not None: response.update(ret) except Exception as exception: response['_exception'] = str(exception) self._writer.write(response) def _handle_start(self): # type: () -> Dict[str,Any] """ Handles the start command. Cover exceptions manually to make sure as much metadata is returned as possible. """ data = {} # type: Dict[str,Any] try: self._init_plugin() self._start_background_tasks() except Exception as exception: data['exception'] = str(exception) data.update({ 'name': self._name, 'version': self._version, 'decorators': self.get_decorators_in_use(), 'exposes': self._exposes, 'interfaces': self._interfaces, 'metric_collectors': self._metric_collectors, 'metric_receivers': self._metric_receivers }) return data def _handle_stop(self): def delayed_stop(): time.sleep(2) os._exit(0) stop_thread = BaseThread(name='pluginstop', target=delayed_stop) stop_thread.daemon = True stop_thread.start() self._stopped = True def _handle_input_status(self, data): event = GatewayEvent.deserialize(data) # get relevant event details input_id = event.data['id'] status = event.data['status'] for decorated_method in self._decorated_methods['input_status']: decorator_version = decorated_method.input_status.get('version', 1) if decorator_version == 1: # Backwards compatibility: only send rising edges of the input (no input releases) if status: self._writer.with_catch('input status', decorated_method, [(input_id, None)]) elif decorator_version == 2: # Version 2 will send ALL input status changes AND in a dict format self._writer.with_catch('input status', decorated_method, [{ 'input_id': input_id, 'status': status }]) else: error = NotImplementedError( 'Version {} is not supported for input status decorators'. format(decorator_version)) self._writer.log_exception('input status', error) def _handle_output_status(self, data, data_type='status'): event = GatewayEvent.deserialize( data) if data_type == 'event' else None for receiver in self._decorated_methods['output_status']: decorator_version = receiver.output_status.get('version', 1) if decorator_version not in PluginRuntime.SUPPORTED_DECORATOR_VERSIONS[ 'output_status']: error = NotImplementedError( 'Version {} is not supported for output status decorators'. format(decorator_version)) self._writer.log_exception('output status', error) else: if decorator_version == 1 and data_type == 'status': self._writer.with_catch('output status', receiver, [data]) elif decorator_version == 2 and event: self._writer.with_catch('output status', receiver, [event.data]) def _handle_ventilation_status(self, data): event = GatewayEvent.deserialize(data) for receiver in self._decorated_methods['ventilation_status']: self._writer.with_catch('ventilation status', receiver, [event.data]) def _handle_thermostat_status(self, data): event = GatewayEvent.deserialize(data) for receiver in self._decorated_methods['thermostat_status']: self._writer.with_catch('thermostat status', receiver, [event.data]) def _handle_thermostat_group_status(self, data): event = GatewayEvent.deserialize(data) for receiver in self._decorated_methods['thermostat_group_status']: self._writer.with_catch('thermostat group status', receiver, [event.data]) def _handle_shutter_status(self, data, data_type='status'): event = GatewayEvent.deserialize( data) if data_type == 'event' else None for receiver in self._decorated_methods['shutter_status']: decorator_version = receiver.shutter_status.get('version', 1) if decorator_version not in PluginRuntime.SUPPORTED_DECORATOR_VERSIONS[ 'shutter_status']: error = NotImplementedError( 'Version {} is not supported for shutter status decorators' .format(decorator_version)) self._writer.log_exception('shutter status', error) else: if decorator_version == 1 and data_type == 'status': self._writer.with_catch('shutter status', receiver, [data]) elif decorator_version == 2 and data_type == 'status_dict': self._writer.with_catch('shutter status', receiver, [data['status'], data['detail']]) elif decorator_version == 3 and event: self._writer.with_catch('shutter status', receiver, [event.data]) def _handle_receive_events(self, code): for receiver in self._decorated_methods['receive_events']: decorator_version = receiver.receive_events.get('version', 1) if decorator_version == 1: self._writer.with_catch('process event', receiver, [code]) else: error = NotImplementedError( 'Version {} is not supported for receive events decorators' .format(decorator_version)) self._writer.log_exception('receive events', error) def _handle_remove_callback(self): for decorated_method in self._decorated_methods['on_remove']: decorator_version = decorated_method.on_remove.get('version', 1) if decorator_version == 1: try: decorated_method() except Exception as exception: self._writer.log_exception('on remove', exception) else: error = NotImplementedError( 'Version {} is not supported for shutter status decorators' .format(decorator_version)) self._writer.log_exception('on remove', error) def _handle_get_metric_definitions(self): return {'metric_definitions': self._metric_definitions} def _handle_collect_metrics(self, name): metrics = [] collect = getattr(self._plugin, name) try: metrics.extend(list(collect())) except Exception as exception: self._writer.log_exception('collect metrics', exception) return {'metrics': metrics} def _handle_distribute_metrics(self, name, metrics): receive = getattr(self._plugin, name) for metric in metrics: self._writer.with_catch('distribute metric', receive, [metric]) def _handle_request(self, method, args, kwargs): func = getattr(self._plugin, method) requested_parameters = set( Toolbox.get_parameter_names(func)) - {'self'} difference = set(kwargs.keys()) - requested_parameters if difference: # Analog error message as the default CherryPy behavior return { 'success': False, 'exception': 'Unexpected query string parameters: {0}'.format( ', '.join(difference)) } difference = requested_parameters - set(kwargs.keys()) if difference: # Analog error message as the default CherryPy behavior return { 'success': False, 'exception': 'Missing parameters: {0}'.format(', '.join(difference)) } try: return {'success': True, 'response': func(*args, **kwargs)} except Exception as exception: return { 'success': False, 'exception': str(exception), 'stacktrace': traceback.format_exc() }
class PluginRunner(object): class State(object): RUNNING = 'RUNNING' STOPPED = 'STOPPED' def __init__(self, name, runtime_path, plugin_path, logger, command_timeout=5.0, state_callback=None): self.runtime_path = runtime_path self.plugin_path = plugin_path self.command_timeout = command_timeout self._logger = logger self._cid = 0 self._proc = None # type: Optional[subprocess.Popen[bytes]] self._running = False self._process_running = False self._command_lock = Lock() self._response_queue = Queue() # type: Queue[Dict[str,Any]] self._writer = None # type: Optional[PluginIPCWriter] self._reader = None # type: Optional[PluginIPCReader] self._state_callback = state_callback # type: Optional[Callable[[str, str], None]] self.name = name self.version = None self.interfaces = None self._decorators_in_use = {} self._exposes = [] self._metric_collectors = [] self._metric_receivers = [] self._async_command_thread = None self._async_command_queue = None # type: Optional[Queue[Optional[Dict[str, Any]]]] self._commands_executed = 0 self._commands_failed = 0 self.__collector_runs = {} # type: Dict[str,float] def start(self): # type: () -> None if self._running: raise Exception('PluginRunner is already running') self.logger('[Runner] Starting') python_executable = sys.executable if python_executable is None or len(python_executable) == 0: python_executable = '/usr/bin/python' self._proc = subprocess.Popen([ python_executable, 'runtime.py', 'start_plugin', self.plugin_path ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, cwd=self.runtime_path, close_fds=True) assert self._proc.stdout, 'Plugin stdout not available' self._process_running = True self._commands_executed = 0 self._commands_failed = 0 assert self._proc.stdin, 'Plugin stdin not defined' self._writer = PluginIPCWriter(stream=self._proc.stdin) self._reader = PluginIPCReader(stream=self._proc.stdout, logger=lambda message, ex: self.logger( '{0}: {1}'.format(message, ex)), command_receiver=self._process_command, name=self.name) self._reader.start() start_out = self._do_command('start', timeout=180) self.name = start_out['name'] self.version = start_out['version'] self.interfaces = start_out['interfaces'] self._decorators_in_use = start_out['decorators'] self._exposes = start_out['exposes'] self._metric_collectors = start_out['metric_collectors'] self._metric_receivers = start_out['metric_receivers'] exception = start_out.get('exception') if exception is not None: raise RuntimeError(exception) self._async_command_queue = Queue(1000) self._async_command_thread = BaseThread( name='plugincmd{0}'.format(self.plugin_path), target=self._perform_async_commands) self._async_command_thread.daemon = True self._async_command_thread.start() self._running = True if self._state_callback is not None: self._state_callback(self.name, PluginRunner.State.RUNNING) self.logger('[Runner] Started') def logger(self, message): # type: (str) -> None self._logger(message) logger_.info('Plugin {0} - {1}'.format(self.name, message)) def get_webservice(self, webinterface): # type: (WebInterface) -> Service return Service(self, webinterface) def is_running(self): # type: () -> bool return self._running def stop(self): # type: () -> None if self._process_running: self._running = False self.logger('[Runner] Sending stop command') try: self._do_command('stop') except Exception as exception: self.logger( '[Runner] Exception during stopping plugin: {0}'.format( exception)) time.sleep(0.1) if self._reader: self._reader.stop() self._process_running = False if self._async_command_queue is not None: self._async_command_queue.put( None) # Triggers an abort on the read thread if self._proc and self._proc.poll() is None: self.logger('[Runner] Terminating process') try: self._proc.terminate() except Exception as exception: self.logger( '[Runner] Exception during terminating plugin: {0}'. format(exception)) time.sleep(0.5) if self._proc.poll() is None: self.logger('[Runner] Killing process') try: self._proc.kill() except Exception as exception: self.logger( '[Runner] Exception during killing plugin: {0}'. format(exception)) if self._state_callback is not None: self._state_callback(self.name, PluginRunner.State.STOPPED) self.logger('[Runner] Stopped') def process_input_status(self, data, action_version=1): if action_version in [1, 2]: if action_version == 1: payload = {'status': data} else: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='input_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger('Input status version {} not supported.'.format( action_version)) def process_output_status(self, data, action_version=1): if action_version in [1, 2]: if action_version == 1: payload = {'status': data} else: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='output_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger('Output status version {} not supported.'.format( action_version)) def process_shutter_status(self, data, action_version=1): if action_version in [1, 2, 3]: if action_version == 1: payload = {'status': data} elif action_version == 2: status, detail = data payload = {'status': {'status': status, 'detail': detail}} else: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='shutter_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger('Shutter status version {} not supported.'.format( action_version)) def process_ventilation_status(self, data, action_version=1): if action_version in [1]: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='ventilation_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger('Ventilation status version {} not supported.'.format( action_version)) def process_thermostat_status(self, data, action_version=1): if action_version in [1]: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='thermostat_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger('Thermostat status version {} not supported.'.format( action_version)) def process_thermostat_group_status(self, data, action_version=1): if action_version in [1]: event_json = data.serialize() payload = {'event': event_json} self._do_async(action='thermostat_group_status', payload=payload, should_filter=True, action_version=action_version) else: self.logger( 'Thermostat group status version {} not supported.'.format( action_version)) def process_event(self, code): self._do_async('receive_events', {'code': code}, should_filter=True) def collect_metrics(self): for mc in self._metric_collectors: try: now = time.time() (name, interval) = (mc['name'], mc['interval']) if self.__collector_runs.get(name, 0.0) < now - interval: self.__collector_runs[name] = now metrics = self._do_command('collect_metrics', {'name': name})['metrics'] for metric in metrics: if metric is None: continue metric['source'] = self.name yield metric except Exception as exception: self.logger( '[Runner] Exception while collecting metrics {0}: {1}'. format(exception, traceback.format_exc())) def get_metric_receivers(self): return self._metric_receivers def distribute_metrics(self, method, metrics): self._do_async('distribute_metrics', { 'name': method, 'metrics': metrics }) def get_metric_definitions(self): return self._do_command('get_metric_definitions')['metric_definitions'] def request(self, method, args=None, kwargs=None): if args is None: args = [] if kwargs is None: kwargs = {} ret = self._do_command('request', { 'method': method, 'args': args, 'kwargs': kwargs }) if ret['success']: return ret['response'] elif 'stacktrace' in ret: raise Exception('{0}: {1}'.format(ret['exception'], ret['stacktrace'])) raise Exception(ret['exception']) def remove_callback(self): # type: () -> None self._do_command('remove_callback') def _process_command(self, response): # type: (Dict[str,Any]) -> None if not self._process_running: return assert self._proc, 'Plugin process not defined' exit_code = self._proc.poll() if exit_code is not None: self.logger( '[Runner] Stopped with exit code {0}'.format(exit_code)) self._process_running = False return if response['cid'] == 0: self._handle_async_response(response) elif response['cid'] == self._cid: self._response_queue.put(response) else: self.logger( '[Runner] Received message with unknown cid: {0}'.format( response)) def _handle_async_response(self, response): # type: (Dict[str,Any]) -> None if response['action'] == 'logs': self.logger(response['logs']) else: self.logger('[Runner] Unkown async message: {0}'.format(response)) def _do_async(self, action, payload, should_filter=False, action_version=1): # type: (str, Dict[str,Any], bool, int) -> None has_receiver = False for decorator_name, decorator_versions in six.iteritems( self._decorators_in_use): # the action version is linked to a specific decorator version has_receiver |= (action == decorator_name and action_version in decorator_versions) if not self._process_running or (should_filter and not has_receiver): return try: assert self._async_command_queue, 'Command Queue not defined' self._async_command_queue.put( { 'action': action, 'payload': payload, 'action_version': action_version }, block=False) except Full: self.logger('Async action cannot be queued, queue is full') def _perform_async_commands(self): # type: () -> None while self._process_running: try: # Give it a timeout in order to check whether the plugin is not stopped. assert self._async_command_queue, 'Command Queue not defined' command = self._async_command_queue.get(block=True, timeout=10) if command is None: continue # Used to exit this thread self._do_command(command['action'], payload=command['payload'], action_version=command['action_version']) except Empty: self._do_async('ping', {}) except Exception as exception: self.logger( '[Runner] Failed to perform async command: {0}'.format( exception)) def _do_command(self, action, payload=None, timeout=None, action_version=1): # type: (str, Dict[str,Any], Optional[float], int) -> Dict[str,Any] if payload is None: payload = {} self._commands_executed += 1 if timeout is None: timeout = self.command_timeout if not self._process_running: raise Exception('Plugin was stopped') with self._command_lock: try: command = self._create_command(action, payload, action_version) assert self._writer, 'Plugin stdin not defined' self._writer.write(command) except Exception: self._commands_failed += 1 raise try: response = self._response_queue.get(block=True, timeout=timeout) while response['cid'] != self._cid: response = self._response_queue.get(block=False) exception = response.get('_exception') if exception is not None: raise RuntimeError(exception) return response except Empty: metadata = '' if action == 'request': metadata = ' {0}'.format(payload['method']) if self._running: self.logger( '[Runner] No response within {0}s ({1}{2})'.format( timeout, action, metadata)) self._commands_failed += 1 raise Exception('Plugin did not respond') def _create_command(self, action, payload=None, action_version=1): # type: (str, Dict[str,Any], int) -> Dict[str,Any] if payload is None: payload = {} self._cid += 1 command = { 'cid': self._cid, 'action': action, 'action_version': action_version } command.update(payload) return command def error_score(self): # type: () -> float if self._commands_executed == 0: return 0.0 else: score = float(self._commands_failed) / self._commands_executed self._commands_failed = 0 self._commands_executed = 0 return score def get_queue_length(self): # type: () -> int if self._async_command_queue is None: return 0 return self._async_command_queue.qsize()