class ZmqDriver(Driver): def __init__(self, engine): super().__init__(engine) self._engine = engine self._stream = None self._exit = False self._updates = None def start(self, endpoint): self._stream = Stream(endpoint) startup_state = self._register() # Validators version 1.1 send startup info with the registration # response; newer versions will send an activation message with the # startup info if startup_state is None: startup_state = self._wait_until_active() self._updates = Queue() driver_thread = Thread( target=self._driver_loop) driver_thread.start() try: self._engine.start( self._updates, ZmqService( stream=self._stream, timeout=SERVICE_TIMEOUT), startup_state) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught engine exception") self.stop() driver_thread.join() def _driver_loop(self): try: future = self._stream.receive() while True: if self._exit: self._engine.stop() break try: message = future.result(1) future = self._stream.receive() except concurrent.futures.TimeoutError: continue try: result = self._process(message) # if message was a ping ignore if result[0] == Message.PING_REQUEST: continue self._updates.put(result) except exceptions.ReceiveError as err: LOGGER.warning("%s", err) continue except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught driver exception") def stop(self): self._exit = True self._engine.stop() self._stream.close() def _register(self): self._stream.wait_for_ready() request = consensus_pb2.ConsensusRegisterRequest( name=self._engine.name(), version=self._engine.version(), ) for (name, version) in self._engine.additional_protocols(): protocol = request.additional_protocols.add() protocol.name = name protocol.version = version while True: future = self._stream.send( message_type=Message.CONSENSUS_REGISTER_REQUEST, content=request.SerializeToString()) response = consensus_pb2.ConsensusRegisterResponse() response.ParseFromString(future.result(REGISTER_TIMEOUT).content) if ( response.status == consensus_pb2.ConsensusRegisterResponse.NOT_READY ): continue if response.status == consensus_pb2.ConsensusRegisterResponse.OK: if ( response.HasField('chain_head') and response.HasField('local_peer_info') ): return StartupState( response.chain_head, response.peers, response.local_peer_info) return None raise exceptions.ReceiveError( 'Registration failed with status {}'.format(response.status)) def _wait_until_active(self): future = self._stream.receive() while True: try: message = future.result(1) except concurrent.futures.TimeoutError: continue if ( message.message_type == Message.CONSENSUS_NOTIFY_ENGINE_ACTIVATED ): notification = \ consensus_pb2.ConsensusNotifyEngineActivated() notification.ParseFromString(message.content) startup_state = StartupState( notification.chain_head, notification.peers, notification.local_peer_info) LOGGER.info( 'Received activation message with startup state: %s', startup_state) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck() .SerializeToString()) return startup_state LOGGER.warning('Received message type %s while waiting for \ activation message', message.message_type) future = self._stream.receive() def _process(self, message): type_tag = message.message_type if type_tag == Message.CONSENSUS_NOTIFY_PEER_CONNECTED: notification = consensus_pb2.ConsensusNotifyPeerConnected() notification.ParseFromString(message.content) data = notification.peer_info elif type_tag == Message.CONSENSUS_NOTIFY_PEER_DISCONNECTED: notification = consensus_pb2.ConsensusNotifyPeerDisconnected() notification.ParseFromString(message.content) data = notification.peer_id elif type_tag == Message.CONSENSUS_NOTIFY_PEER_MESSAGE: notification = consensus_pb2.ConsensusNotifyPeerMessage() notification.ParseFromString(message.content) header = consensus_pb2.ConsensusPeerMessageHeader() header.ParseFromString(notification.message.header) peer_message = PeerMessage( header=header, header_bytes=notification.message.header, header_signature=notification.message.header_signature, content=notification.message.content) data = peer_message, notification.sender_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_NEW: notification = consensus_pb2.ConsensusNotifyBlockNew() notification.ParseFromString(message.content) data = notification.block elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_VALID: notification = consensus_pb2.ConsensusNotifyBlockValid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_INVALID: notification = consensus_pb2.ConsensusNotifyBlockInvalid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_COMMIT: notification = consensus_pb2.ConsensusNotifyBlockCommit() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_ENGINE_DEACTIVATED: self.stop() data = None elif type_tag == Message.PING_REQUEST: data = None else: raise exceptions.ReceiveError( 'Received unexpected message type: {}'.format(type_tag)) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck().SerializeToString()) return type_tag, data
class TransactionProcessor(object): def __init__(self, url): self._stream = Stream(url) self._url = url self._handlers = [] @property def zmq_id(self): return self._stream.zmq_id def add_handler(self, handler): """Add a transaction family handler :param handler: """ self._handlers.append(handler) def _matches(self, handler, header): return header.family_name == handler.family_name \ and header.family_version in handler.family_versions \ and header.payload_encoding in handler.encodings def _find_handler(self, header): """Find a handler for a particular (family_name, family_versions, payload_encoding) :param header transaction_pb2.TransactionHeader: :return: handler """ try: return next(handler for handler in self._handlers if self._matches(handler, header)) except StopIteration: LOGGER.debug("Missing handler for header: %s", header) return None def _register_requests(self): """Returns all of the TpRegisterRequests for handlers :return (list): list of TpRegisterRequests """ return itertools.chain.from_iterable( # flattens the nested list [[ TpRegisterRequest(family=n, version=v, encoding=e, namespaces=h.namespaces) for n, v, e in itertools.product( [h.family_name], h.family_versions, h.encodings) ] for h in self._handlers]) def _unregister_request(self): """Returns a single TP_UnregisterRequest that requests that the validator stop sending transactions for previously registered handlers. :return (processor_pb2.TpUnregisterRequest): """ return TpUnregisterRequest() def _process(self, msg): if msg.message_type != Message.TP_PROCESS_REQUEST: LOGGER.debug( "Transaction Processor recieved invalid message type. " "Message type should be TP_PROCESS_REQUEST," " but is %s", Message.MessageType.Name(msg.message_type)) return request = TpProcessRequest() request.ParseFromString(msg.content) state = State(self._stream, request.context_id) header = TransactionHeader() header.ParseFromString(request.header) try: if not self._stream.is_ready(): raise ValidatorConnectionError() handler = self._find_handler(header) if handler is None: return handler.apply(request, state) self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.OK).SerializeToString()) except InvalidTransaction as it: LOGGER.warning("Invalid Transaction %s", it) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INVALID_TRANSACTION, message=str(it), extended_data=it.extended_data).SerializeToString()) except ValidatorConnectionError as vce: # TP_PROCESS_REQUEST has made it through the # handler.apply and an INVALID_TRANSACTION would have been # sent back but the validator has disconnected and so it # doesn't care about the response. LOGGER.warning("during invalid transaction response: %s", vce) except InternalError as ie: LOGGER.warning("internal error: %s", ie) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INTERNAL_ERROR, message=str(ie), extended_data=ie.extended_data).SerializeToString()) except ValidatorConnectionError as vce: # Same as the prior except block, but an internal error has # happened, but because of the disconnect the validator # probably doesn't care about the response. LOGGER.warning("during internal error response: %s", vce) except ValidatorConnectionError as vce: # Somewhere within handler.apply a future resolved with an # error status that the validator has disconnected. There is # nothing left to do but reconnect. LOGGER.warning( "during handler.apply a future was resolved " "with error status: %s", vce) def _process_future(self, future, timeout=None, sigint=False): try: msg = future.result(timeout) except CancelledError: # This error is raised when Task.cancel is called on # disconnect from the validator in stream.py, for # this future. return if msg is RECONNECT_EVENT: if sigint is False: LOGGER.info("reregistering with validator") self._stream.wait_for_ready() self._register() else: LOGGER.debug('received message of type: %s', Message.MessageType.Name(msg.message_type)) if msg.message_type == Message.TP_PING: self._stream.send_back( message_type=Message.TP_PING_RESPONSE, correlation_id=msg.correlation_id, content=TpPingResponse( status=TpPingResponse.OK).SerializeToString()) return self._process(msg) def _register(self): futures = [] for message in self._register_requests(): self._stream.wait_for_ready() future = self._stream.send( message_type=Message.TP_REGISTER_REQUEST, content=message.SerializeToString()) futures.append(future) for future in futures: resp = TpRegisterResponse() try: resp.ParseFromString(future.result().content) LOGGER.info("register attempt: %s", TpRegisterResponse.Status.Name(resp.status)) except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on registration: %s", vce) def _unregister(self): message = self._unregister_request() self._stream.wait_for_ready() future = self._stream.send(message_type=Message.TP_UNREGISTER_REQUEST, content=message.SerializeToString()) response = TpUnregisterResponse() try: response.ParseFromString(future.result(1).content) LOGGER.info("unregister attempt: %s", TpUnregisterResponse.Status.Name(response.status)) except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on unregistration: %s", vce) def start(self): fut = None try: self._register() while True: # During long running processing this # is where the transaction processor will # spend most of its time fut = self._stream.receive() self._process_future(fut) except KeyboardInterrupt: try: # tell the validator to not send any more messages self._unregister() while True: if fut is not None: # process futures as long as the tp has them, # if the TP_PROCESS_REQUEST doesn't come from # zeromq->asyncio in 1 second raise a # concurrent.futures.TimeOutError and be done. self._process_future(fut, 1, sigint=True) fut = self._stream.receive() except concurrent.futures.TimeoutError: # Where the tp will usually exit after # a KeyboardInterrupt. Caused by the 1 second # timeout in _process_future. pass except FutureTimeoutError: # If the validator is not able to respond to the # unregister request, exit. pass def stop(self): self._stream.close()
class ZmqDriver(Driver): def __init__(self, engine): super().__init__(engine) self._engine = engine self._stream = None self._exit = False self._updates = None def start(self, endpoint): self._stream = Stream(endpoint) (chain_head, peers) = self._register() self._updates = Queue() engine_thread = Thread( target=self._engine.start, args=(self._updates, ZmqService(stream=self._stream, timeout=SERVICE_TIMEOUT, name=self._engine.name(), version=self._engine.version()), chain_head, peers)) engine_thread.start() while True: if self._exit: self._engine.stop() engine_thread.join() break try: message = self._stream.receive().result(0.1) except concurrent.futures.TimeoutError: continue result = self._process(message) self._updates.put(result) def stop(self): self._exit = True self._stream.close() def _register(self): self._stream.wait_for_ready() request = consensus_pb2.ConsensusRegisterRequest( name=self._engine.name(), version=self._engine.version(), ).SerializeToString() while True: future = self._stream.send( message_type=Message.CONSENSUS_REGISTER_REQUEST, content=request) response = consensus_pb2.ConsensusRegisterResponse() response.ParseFromString(future.result(REGISTER_TIMEOUT).content) if (response.status == consensus_pb2.ConsensusRegisterResponse.NOT_READY): continue if response.status == consensus_pb2.ConsensusRegisterResponse.OK: return (response.chain_head, response.peers) raise exceptions.ReceiveError( 'Registration failed with status {}'.format(response.status)) def _process(self, message): type_tag = message.message_type if type_tag == Message.CONSENSUS_NOTIFY_PEER_CONNECTED: notification = consensus_pb2.ConsensusNotifyPeerConnected() notification.ParseFromString(message.content) data = notification.peer_info elif type_tag == Message.CONSENSUS_NOTIFY_PEER_DISCONNECTED: notification = consensus_pb2.ConsensusNotifyPeerDisconnected() notification.ParseFromString(message.content) data = notification.peer_id elif type_tag == Message.CONSENSUS_NOTIFY_PEER_MESSAGE: notification = consensus_pb2.ConsensusNotifyPeerMessage() notification.ParseFromString(message.content) data = notification.message, notification.sender_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_NEW: notification = consensus_pb2.ConsensusNotifyBlockNew() notification.ParseFromString(message.content) data = notification.block elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_VALID: notification = consensus_pb2.ConsensusNotifyBlockValid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_INVALID: notification = consensus_pb2.ConsensusNotifyBlockInvalid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_COMMIT: notification = consensus_pb2.ConsensusNotifyBlockCommit() notification.ParseFromString(message.content) data = notification.block_id else: raise exceptions.ReceiveError( 'Received unexpected message type: {}'.format(type_tag)) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck().SerializeToString()) return type_tag, data
class ZmqDriver(Driver): def __init__(self, engine): super().__init__(engine) self._engine = engine self._stream = None self._exit = False self._updates = None def start(self, endpoint): self._stream = Stream(endpoint) startup_state = self._register() # Validators version 1.1 send startup info with the registration # response; newer versions will send an activation message with the # startup info if startup_state is None: startup_state = self._wait_until_active() self._updates = Queue() driver_thread = Thread( target=self._driver_loop) driver_thread.start() try: self._engine.start( self._updates, ZmqService( stream=self._stream, timeout=SERVICE_TIMEOUT), startup_state) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught engine exception") self.stop() driver_thread.join() def _driver_loop(self): try: future = self._stream.receive() while True: if self._exit: self._engine.stop() break try: message = future.result(1) future = self._stream.receive() except concurrent.futures.TimeoutError: continue result = self._process(message) self._updates.put(result) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught driver exception") def stop(self): self._exit = True self._engine.stop() self._stream.close() def _register(self): self._stream.wait_for_ready() request = consensus_pb2.ConsensusRegisterRequest( name=self._engine.name(), version=self._engine.version(), ).SerializeToString() while True: future = self._stream.send( message_type=Message.CONSENSUS_REGISTER_REQUEST, content=request) response = consensus_pb2.ConsensusRegisterResponse() response.ParseFromString(future.result(REGISTER_TIMEOUT).content) if ( response.status == consensus_pb2.ConsensusRegisterResponse.NOT_READY ): continue if response.status == consensus_pb2.ConsensusRegisterResponse.OK: if ( response.HasField('chain_head') and response.HasField('local_peer_info') ): return StartupState( response.chain_head, response.peers, response.local_peer_info) return None raise exceptions.ReceiveError( 'Registration failed with status {}'.format(response.status)) def _wait_until_active(self): future = self._stream.receive() while True: try: message = future.result(1) except concurrent.futures.TimeoutError: continue if ( message.message_type == Message.CONSENSUS_NOTIFY_ENGINE_ACTIVATED ): notification = \ consensus_pb2.ConsensusNotifyEngineActivated() notification.ParseFromString(message.content) startup_state = StartupState( notification.chain_head, notification.peers, notification.local_peer_info) LOGGER.info( 'Received activation message with startup state: %s', startup_state) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck() .SerializeToString()) return startup_state LOGGER.warning('Received message type %s while waiting for \ activation message', message.message_type) future = self._stream.receive() def _process(self, message): type_tag = message.message_type if type_tag == Message.CONSENSUS_NOTIFY_PEER_CONNECTED: notification = consensus_pb2.ConsensusNotifyPeerConnected() notification.ParseFromString(message.content) data = notification.peer_info elif type_tag == Message.CONSENSUS_NOTIFY_PEER_DISCONNECTED: notification = consensus_pb2.ConsensusNotifyPeerDisconnected() notification.ParseFromString(message.content) data = notification.peer_id elif type_tag == Message.CONSENSUS_NOTIFY_PEER_MESSAGE: notification = consensus_pb2.ConsensusNotifyPeerMessage() notification.ParseFromString(message.content) header = consensus_pb2.ConsensusPeerMessageHeader() header.ParseFromString(notification.message.header) peer_message = PeerMessage( header=header, header_bytes=notification.message.header, header_signature=notification.message.header_signature, content=notification.message.content) data = peer_message, notification.sender_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_NEW: notification = consensus_pb2.ConsensusNotifyBlockNew() notification.ParseFromString(message.content) data = notification.block elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_VALID: notification = consensus_pb2.ConsensusNotifyBlockValid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_INVALID: notification = consensus_pb2.ConsensusNotifyBlockInvalid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_COMMIT: notification = consensus_pb2.ConsensusNotifyBlockCommit() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_ENGINE_DEACTIVATED: self.stop() data = None else: raise exceptions.ReceiveError( 'Received unexpected message type: {}'.format(type_tag)) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck().SerializeToString()) return type_tag, data
class TransactionProcessor: """TransactionProcessor is a generic class for communicating with a validator and routing transaction processing requests to a registered handler. It uses ZMQ and channels to handle requests concurrently. """ def __init__(self, url): """ Args: url (string): The URL of the validator """ self._stream = Stream(url) self._url = url self._handlers = [] @property def zmq_id(self): return self._stream.zmq_id def add_handler(self, handler): """Adds a transaction family handler Args: handler (TransactionHandler): the handler to be added """ self._handlers.append(handler) def _matches(self, handler, header): return header.family_name == handler.family_name \ and header.family_version in handler.family_versions def _find_handler(self, header): """Find a handler for a particular (family_name, family_versions) :param header transaction_pb2.TransactionHeader: :return: handler """ try: return next( handler for handler in self._handlers if self._matches(handler, header)) except StopIteration: LOGGER.debug("Missing handler for header: %s", header) return None def _register_requests(self): """Returns all of the TpRegisterRequests for handlers :return (list): list of TpRegisterRequests """ return itertools.chain.from_iterable( # flattens the nested list [ [TpRegisterRequest( family=n, version=v, namespaces=h.namespaces) for n, v in itertools.product( [h.family_name], h.family_versions,)] for h in self._handlers]) def _unregister_request(self): """Returns a single TP_UnregisterRequest that requests that the validator stop sending transactions for previously registered handlers. :return (processor_pb2.TpUnregisterRequest): """ return TpUnregisterRequest() def _process(self, msg): if msg.message_type != Message.TP_PROCESS_REQUEST: LOGGER.debug( "Transaction Processor recieved invalid message type. " "Message type should be TP_PROCESS_REQUEST," " but is %s", Message.MessageType.Name(msg.message_type)) return request = TpProcessRequest() request.ParseFromString(msg.content) state = Context(self._stream, request.context_id) header = request.header try: if not self._stream.is_ready(): raise ValidatorConnectionError() handler = self._find_handler(header) if handler is None: return handler.apply(request, state) self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.OK ).SerializeToString()) except InvalidTransaction as it: LOGGER.warning("Invalid Transaction %s", it) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INVALID_TRANSACTION, message=str(it), extended_data=it.extended_data ).SerializeToString()) except ValidatorConnectionError as vce: # TP_PROCESS_REQUEST has made it through the # handler.apply and an INVALID_TRANSACTION would have been # sent back but the validator has disconnected and so it # doesn't care about the response. LOGGER.warning("during invalid transaction response: %s", vce) except InternalError as ie: LOGGER.warning("internal error: %s", ie) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INTERNAL_ERROR, message=str(ie), extended_data=ie.extended_data ).SerializeToString()) except ValidatorConnectionError as vce: # Same as the prior except block, but an internal error has # happened, but because of the disconnect the validator # probably doesn't care about the response. LOGGER.warning("during internal error response: %s", vce) except ValidatorConnectionError as vce: # Somewhere within handler.apply a future resolved with an # error status that the validator has disconnected. There is # nothing left to do but reconnect. LOGGER.warning("during handler.apply a future was resolved " "with error status: %s", vce) except AuthorizationException as ae: LOGGER.warning("AuthorizationException: %s", ae) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INVALID_TRANSACTION, message=str(ae), ).SerializeToString()) except ValidatorConnectionError as vce: # TP_PROCESS_REQUEST has made it through the # handler.apply and an INVALID_TRANSACTION would have been # sent back but the validator has disconnected and so it # doesn't care about the response. LOGGER.warning("during invalid transaction response: %s", vce) def _process_future(self, future, timeout=None, sigint=False): try: msg = future.result(timeout) except CancelledError: # This error is raised when Task.cancel is called on # disconnect from the validator in stream.py, for # this future. return if msg is RECONNECT_EVENT: if sigint is False: LOGGER.info("reregistering with validator") self._stream.wait_for_ready() self._register() else: LOGGER.debug( 'received message of type: %s', Message.MessageType.Name(msg.message_type)) if msg.message_type == Message.PING_REQUEST: self._stream.send_back( message_type=Message.PING_RESPONSE, correlation_id=msg.correlation_id, content=PingResponse().SerializeToString()) return self._process(msg) def _register(self): futures = [] for message in self._register_requests(): self._stream.wait_for_ready() future = self._stream.send( message_type=Message.TP_REGISTER_REQUEST, content=message.SerializeToString()) futures.append(future) for future in futures: resp = TpRegisterResponse() try: resp.ParseFromString(future.result().content) LOGGER.info("register attempt: %s", TpRegisterResponse.Status.Name(resp.status)) except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on registration: %s", vce) def _unregister(self): message = self._unregister_request() self._stream.wait_for_ready() future = self._stream.send( message_type=Message.TP_UNREGISTER_REQUEST, content=message.SerializeToString()) response = TpUnregisterResponse() try: response.ParseFromString(future.result(1).content) LOGGER.info("unregister attempt: %s", TpUnregisterResponse.Status.Name(response.status)) except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on unregistration: %s", vce) def start(self): """Connects the transaction processor to a validator and starts listening for requests and routing them to an appropriate transaction handler. """ fut = None try: self._register() while True: # During long running processing this # is where the transaction processor will # spend most of its time fut = self._stream.receive() self._process_future(fut) except KeyboardInterrupt: try: # tell the validator to not send any more messages self._unregister() while True: if fut is not None: # process futures as long as the tp has them, # if the TP_PROCESS_REQUEST doesn't come from # zeromq->asyncio in 1 second raise a # concurrent.futures.TimeOutError and be done. self._process_future(fut, 1, sigint=True) fut = self._stream.receive() except concurrent.futures.TimeoutError: # Where the tp will usually exit after # a KeyboardInterrupt. Caused by the 1 second # timeout in _process_future. pass except FutureTimeoutError: # If the validator is not able to respond to the # unregister request, exit. pass def stop(self): """Closes the connection between the TransactionProcessor and the validator. """ self._stream.close()
class SyncProcessor: future: Optional["Future"] = None _stream: Stream def __init__(self, url=None): self._stream = Stream(url or VALIDATOR_URL) self._handlers = [] def add_handler(self, handler: Type["HandlerBase"]): if handler not in self._handlers: LOG.info(f"Add handler {handler.__name__}") self._handlers.append(handler) def start(self): LOG.info("Start SyncProcessor") future = None try: self._register() while True: future = self._stream.receive() self._process_future(future) except KeyboardInterrupt: LOG.warning("break") try: # tell the validator to not send any more messages self._unregister() while True: if future is not None: # process futures as long as the tp has them, # if the TP_PROCESS_REQUEST doesn't come from # zeromq->asyncio in 1 second raise a # concurrent.futures.TimeOutError and be done. self._process_future(future, 1, sigint=True) future = self._stream.receive() except ConcurrentTimeoutError: # Where the tp will usually exit after # a KeyboardInterrupt. Caused by the 1 second # timeout in _process_future. pass except FutureTimeoutError: # If the validator is not able to respond to the # unregister request, exit. pass def _unregister(self): LOG.warning("unregister") pass def _register(self): LOG.debug(f"Register handlers") self._stream.wait_for_ready() future = self._stream.send( message_type=Message.CLIENT_EVENTS_SUBSCRIBE_REQUEST, content=ClientEventsSubscribeRequest( last_known_block_ids=[NULL_BLOCK_ID], subscriptions=[ EventSubscription(event_type="sawtooth/block-commit"), EventSubscription( event_type="sawtooth/state-delta", filters=[ EventFilter( key="address", match_string=f"^{Namespaces.GA_NAMESPACE}.*", filter_type=EventFilter.REGEX_ANY, ) ], ), ], ).SerializeToString(), ) resp = TpRegisterResponse() try: resp.ParseFromString(future.result().content) LOG.info("Register response: %s", TpRegisterResponse.Status.Name(resp.status)) except ValidatorConnectionError as vce: LOG.info("During waiting for response on registration: %s", vce) except Exception as e: LOG.info("During waiting for response on registration: %s", e) def _process_future(self, future: "Future", timeout=None, sigint=False) -> None: try: msg = future.result(timeout) except CancelledError: # This error is raised when Task.cancel is called on # disconnect from the validator in stream.py, for # this future. LOG.error("Cancelled") return if msg is RECONNECT_EVENT: if sigint is False: LOG.info("Reregistering with validator") self._stream.wait_for_ready() self._register() else: LOG.debug(f"Received message of type: " f"{Message.MessageType.Name(msg.message_type)}") if msg.message_type == Message.PING_REQUEST: self._stream.send_back( message_type=Message.PING_RESPONSE, correlation_id=msg.correlation_id, content=PingResponse().SerializeToString(), ) return elif msg.message_type == Message.CLIENT_EVENTS: self._process(msg) return LOG.warning(f"SyncProcessor got wrong message type: " f"{Message.MessageType.Name(msg.message_type)}") def _process(self, msg: Message) -> None: event_list = EventList() event_list.ParseFromString(msg.content) events = event_list.events raw_block = next( (e for e in events if e.event_type == "sawtooth/block-commit"), []) block = {b.key: b.value for b in raw_block.attributes} LOG.debug(f"Block: {block}") event = next( (e for e in events if e.event_type == "sawtooth/state-delta"), None) if event: state_changes = StateChangeList() state_changes.ParseFromString(event.data) changes = list(state_changes.state_changes) LOG.debug(f"Found {len(changes)} state changes") self._handle_changes(block, changes) def _handle_changes(self, block: dict, changes: list): # r.table(Tbl.BLOCKS).insert(block).run() for change in changes: for handler in self._handlers: if change.address.startswith(handler.prefix): handler.process(change)
class ZmqDriver(Driver): def __init__(self, engine): super().__init__(engine) self._engine = engine self._stream = None self._exit = False self._updates = None def start(self, endpoint): self._stream = Stream(endpoint) startup_state = self._register() self._updates = Queue() driver_thread = Thread( target=self._driver_loop) driver_thread.start() try: self._engine.start( self._updates, ZmqService( stream=self._stream, timeout=SERVICE_TIMEOUT, name=self._engine.name(), version=self._engine.version()), startup_state) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught engine exception") self.stop() driver_thread.join() def _driver_loop(self): try: future = self._stream.receive() while True: if self._exit: self._engine.stop() break try: message = future.result(1) future = self._stream.receive() except concurrent.futures.TimeoutError: continue result = self._process(message) self._updates.put(result) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught driver exception") def stop(self): self._exit = True self._engine.stop() self._stream.close() def _register(self): self._stream.wait_for_ready() request = consensus_pb2.ConsensusRegisterRequest( name=self._engine.name(), version=self._engine.version(), ).SerializeToString() while True: future = self._stream.send( message_type=Message.CONSENSUS_REGISTER_REQUEST, content=request) response = consensus_pb2.ConsensusRegisterResponse() response.ParseFromString(future.result(REGISTER_TIMEOUT).content) if ( response.status == consensus_pb2.ConsensusRegisterResponse.NOT_READY ): continue if response.status == consensus_pb2.ConsensusRegisterResponse.OK: return StartupState( response.chain_head, response.peers, response.local_peer_info) raise exceptions.ReceiveError( 'Registration failed with status {}'.format(response.status)) def _process(self, message): type_tag = message.message_type if type_tag == Message.CONSENSUS_NOTIFY_PEER_CONNECTED: notification = consensus_pb2.ConsensusNotifyPeerConnected() notification.ParseFromString(message.content) data = notification.peer_info elif type_tag == Message.CONSENSUS_NOTIFY_PEER_DISCONNECTED: notification = consensus_pb2.ConsensusNotifyPeerDisconnected() notification.ParseFromString(message.content) data = notification.peer_id elif type_tag == Message.CONSENSUS_NOTIFY_PEER_MESSAGE: notification = consensus_pb2.ConsensusNotifyPeerMessage() notification.ParseFromString(message.content) data = notification.message, notification.sender_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_NEW: notification = consensus_pb2.ConsensusNotifyBlockNew() notification.ParseFromString(message.content) data = notification.block elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_VALID: notification = consensus_pb2.ConsensusNotifyBlockValid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_INVALID: notification = consensus_pb2.ConsensusNotifyBlockInvalid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_COMMIT: notification = consensus_pb2.ConsensusNotifyBlockCommit() notification.ParseFromString(message.content) data = notification.block_id else: raise exceptions.ReceiveError( 'Received unexpected message type: {}'.format(type_tag)) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck().SerializeToString()) return type_tag, data
class TransactionProcessor: """TransactionProcessor is a generic class for communicating with a validator and routing transaction processing requests to a registered handler. It uses ZMQ and channels to handle requests concurrently. """ # This is the version used by SDK to match if validator supports feature # it requested during registration. It should only be incremented when # there are changes in TpRegisterRequest. Remember to sync this # information in validator if changed. # Note: SDK_PROTOCOL_VERSION is the highest version the SDK supports class _FeatureVersion(Enum): FEATURE_UNUSED = 0 FEATURE_CUSTOM_HEADER_STYLE = 1 SDK_PROTOCOL_VERSION = 1 def __init__(self, url): """ Args: url (string): The URL of the validator """ self._stream = Stream(url) self._url = url self._handlers = [] self._highest_sdk_feature_requested = \ self._FeatureVersion.FEATURE_UNUSED self._header_style = TpRegisterRequest.HEADER_STYLE_UNSET @property def zmq_id(self): return self._stream.zmq_id def add_handler(self, handler): """Adds a transaction family handler Args: handler (TransactionHandler): the handler to be added """ self._handlers.append(handler) def set_header_style(self, style): """Sets a flag to request the validator for custom transaction header style in TpProcessRequest. Args: style (TpProcessRequestHeaderStyle): enum value to set header style """ if self._FeatureVersion.FEATURE_CUSTOM_HEADER_STYLE.value > \ self._highest_sdk_feature_requested.value: self._highest_sdk_feature_requested = \ self._FeatureVersion.FEATURE_CUSTOM_HEADER_STYLE self._header_style = style def _matches(self, handler, header): return header.family_name == handler.family_name \ and header.family_version in handler.family_versions def _find_handler(self, header): """Find a handler for a particular (family_name, family_versions) :param header transaction_pb2.TransactionHeader: :return: handler """ try: return next(handler for handler in self._handlers if self._matches(handler, header)) except StopIteration: LOGGER.debug("Missing handler for header: %s", header) return None def _register_requests(self): """Returns all of the TpRegisterRequests for handlers :return (list): list of TpRegisterRequests """ return itertools.chain.from_iterable( # flattens the nested list [[ TpRegisterRequest( family=n, version=v, namespaces=h.namespaces, protocol_version=self._highest_sdk_feature_requested.value, request_header_style=self._header_style) for n, v in itertools.product( [h.family_name], h.family_versions, ) ] for h in self._handlers]) def _unregister_request(self): """Returns a single TP_UnregisterRequest that requests that the validator stop sending transactions for previously registered handlers. :return (processor_pb2.TpUnregisterRequest): """ return TpUnregisterRequest() def _process(self, msg): if msg.message_type != Message.TP_PROCESS_REQUEST: LOGGER.debug( "Transaction Processor recieved invalid message type. " "Message type should be TP_PROCESS_REQUEST," " but is %s", Message.MessageType.Name(msg.message_type)) return request = TpProcessRequest() request.ParseFromString(msg.content) state = Context(self._stream, request.context_id) if self._header_style == TpRegisterRequest.RAW: header = TransactionHeader() header.ParseFromString(request.header_bytes) else: header = request.header try: if not self._stream.is_ready(): raise ValidatorConnectionError() handler = self._find_handler(header) if handler is None: return handler.apply(request, state) self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.OK).SerializeToString()) except InvalidTransaction as it: LOGGER.warning("Invalid Transaction %s", it) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INVALID_TRANSACTION, message=str(it), extended_data=it.extended_data).SerializeToString()) except ValidatorConnectionError as vce: # TP_PROCESS_REQUEST has made it through the # handler.apply and an INVALID_TRANSACTION would have been # sent back but the validator has disconnected and so it # doesn't care about the response. LOGGER.warning("during invalid transaction response: %s", vce) except InternalError as ie: LOGGER.warning("internal error: %s", ie) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INTERNAL_ERROR, message=str(ie), extended_data=ie.extended_data).SerializeToString()) except ValidatorConnectionError as vce: # Same as the prior except block, but an internal error has # happened, but because of the disconnect the validator # probably doesn't care about the response. LOGGER.warning("during internal error response: %s", vce) except ValidatorConnectionError as vce: # Somewhere within handler.apply a future resolved with an # error status that the validator has disconnected. There is # nothing left to do but reconnect. LOGGER.warning( "during handler.apply a future was resolved " "with error status: %s", vce) except AuthorizationException as ae: LOGGER.warning("AuthorizationException: %s", ae) try: self._stream.send_back( message_type=Message.TP_PROCESS_RESPONSE, correlation_id=msg.correlation_id, content=TpProcessResponse( status=TpProcessResponse.INVALID_TRANSACTION, message=str(ae), ).SerializeToString()) except ValidatorConnectionError as vce: # TP_PROCESS_REQUEST has made it through the # handler.apply and an INVALID_TRANSACTION would have been # sent back but the validator has disconnected and so it # doesn't care about the response. LOGGER.warning("during invalid transaction response: %s", vce) def _process_future(self, future, timeout=None, sigint=False): try: msg = future.result(timeout) except CancelledError: # This error is raised when Task.cancel is called on # disconnect from the validator in stream.py, for # this future. return if msg is RECONNECT_EVENT: if sigint is False: LOGGER.info("reregistering with validator") self._stream.wait_for_ready() self._register() else: LOGGER.debug('received message of type: %s', Message.MessageType.Name(msg.message_type)) if msg.message_type == Message.PING_REQUEST: self._stream.send_back( message_type=Message.PING_RESPONSE, correlation_id=msg.correlation_id, content=PingResponse().SerializeToString()) return self._process(msg) def _register(self): futures = [] for message in self._register_requests(): self._stream.wait_for_ready() future = self._stream.send( message_type=Message.TP_REGISTER_REQUEST, content=message.SerializeToString()) futures.append(future) for future in futures: resp = TpRegisterResponse() try: resp.ParseFromString(future.result().content) if resp.protocol_version != \ self._highest_sdk_feature_requested.value: LOGGER.error( "Validator version %s does not support " "requested feature by SDK version %s. " "Unregistering with the validator.", str(resp.protocol_version), str(self._highest_sdk_feature_requested.value)) raise ValidatorVersionError() LOGGER.info("register attempt: %s", TpRegisterResponse.Status.Name(resp.status)) if resp.status == TpRegisterResponse.ERROR: raise RuntimeError("Transaction processor registration " "failed") except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on registration: %s", vce) def _unregister(self): message = self._unregister_request() self._stream.wait_for_ready() future = self._stream.send(message_type=Message.TP_UNREGISTER_REQUEST, content=message.SerializeToString()) response = TpUnregisterResponse() try: response.ParseFromString(future.result(1).content) LOGGER.info("unregister attempt: %s", TpUnregisterResponse.Status.Name(response.status)) except ValidatorConnectionError as vce: LOGGER.info("during waiting for response on unregistration: %s", vce) def start(self): """Connects the transaction processor to a validator and starts listening for requests and routing them to an appropriate transaction handler. """ fut = None try: self._register() while True: # During long running processing this # is where the transaction processor will # spend most of its time fut = self._stream.receive() self._process_future(fut) except (KeyboardInterrupt, ValidatorVersionError): try: # tell the validator to not send any more messages self._unregister() while True: if fut is not None: # process futures as long as the tp has them, # if the TP_PROCESS_REQUEST doesn't come from # zeromq->asyncio in 1 second raise a # concurrent.futures.TimeOutError and be done. self._process_future(fut, 1, sigint=True) fut = self._stream.receive() except concurrent.futures.TimeoutError: # Where the tp will usually exit after # a KeyboardInterrupt. Caused by the 1 second # timeout in _process_future. pass except FutureTimeoutError: # If the validator is not able to respond to the # unregister request, exit. pass except RuntimeError as e: LOGGER.error("Error: %s", e) self.stop() def stop(self): """Closes the connection between the TransactionProcessor and the validator. """ self._stream.close()
class ZmqDriver(Driver): def __init__(self, engine): super().__init__(engine) self._engine = engine self._stream = None self._exit = False self._updates = None def start(self, endpoint): LOGGER.debug('ZmqDriver: start endpoint=%s', endpoint) self._stream = Stream(endpoint) startup_state = self._register() self._updates = Queue() driver_thread = Thread(target=self._driver_loop) driver_thread.start() try: self._engine.start( self._updates, ZmqService(stream=self._stream, timeout=SERVICE_TIMEOUT, name=self._engine.name(), version=self._engine.version()), startup_state) except Exception as ex: # pylint: disable=broad-except LOGGER.exception("Uncaught engine exception(%s)", ex) self.stop() driver_thread.join() def _driver_loop(self): try: future = self._stream.receive() LOGGER.debug('ZmqDriver: _driver_loop future=%s', future) while True: if self._exit: self._engine.stop() break try: message = future.result(1) future = self._stream.receive() except concurrent.futures.TimeoutError: continue #LOGGER.debug('ZmqDriver: _driver_loop _process') result = self._process(message) self._updates.put(result) except Exception: # pylint: disable=broad-except LOGGER.exception("Uncaught driver exception") def stop(self): self._exit = True self._engine.stop() self._stream.close() def _register(self): self._stream.wait_for_ready() request = consensus_pb2.ConsensusRegisterRequest( name=self._engine.name(), version=self._engine.version(), ).SerializeToString() while True: future = self._stream.send( message_type=Message.CONSENSUS_REGISTER_REQUEST, content=request) response = consensus_pb2.ConsensusRegisterResponse() response.ParseFromString(future.result(REGISTER_TIMEOUT).content) if (response.status == consensus_pb2.ConsensusRegisterResponse.NOT_READY): #LOGGER.debug('ZmqDriver:register NOT_READY: url=%s',self._stream._url) continue if response.status == consensus_pb2.ConsensusRegisterResponse.OK: LOGGER.debug('ZmqDriver:register DONE: url=%s', self._stream._url) return StartupState(response.chain_head, response.peers, response.local_peer_info, response.peering_mode) raise exceptions.ReceiveError( 'Registration failed with status {}'.format(response.status)) def _process(self, message): type_tag = message.message_type if type_tag == Message.CONSENSUS_NOTIFY_PEER_CONNECTED: notification = consensus_pb2.ConsensusNotifyPeerConnected() notification.ParseFromString(message.content) data = notification.peer_info, notification.status, notification.mode, notification.info elif type_tag == Message.CONSENSUS_NOTIFY_PEER_DISCONNECTED: notification = consensus_pb2.ConsensusNotifyPeerDisconnected() notification.ParseFromString(message.content) data = notification.peer_id elif type_tag == Message.CONSENSUS_NOTIFY_PEER_MESSAGE: notification = consensus_pb2.ConsensusNotifyPeerMessage() notification.ParseFromString(message.content) data = notification.message, notification.sender_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_NEW: notification = consensus_pb2.ConsensusNotifyBlockNew() notification.ParseFromString(message.content) data = notification.block elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_VALID: notification = consensus_pb2.ConsensusNotifyBlockValid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_INVALID: notification = consensus_pb2.ConsensusNotifyBlockInvalid() notification.ParseFromString(message.content) data = notification.block_id elif type_tag == Message.CONSENSUS_NOTIFY_BLOCK_COMMIT: notification = consensus_pb2.ConsensusNotifyBlockCommit() notification.ParseFromString(message.content) data = notification.block_id else: raise exceptions.ReceiveError( 'Received unexpected message type: {}'.format(type_tag)) self._stream.send_back( message_type=Message.CONSENSUS_NOTIFY_ACK, correlation_id=message.correlation_id, content=consensus_pb2.ConsensusNotifyAck().SerializeToString()) return type_tag, data