github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/rest_api/sawtooth_rest_api/messaging.py (about)

     1  # Copyright 2017 Intel Corporation
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  # ------------------------------------------------------------------------------
    15  
    16  import asyncio
    17  from enum import Enum
    18  import logging
    19  import uuid
    20  
    21  from google.protobuf.message import DecodeError
    22  
    23  import zmq
    24  from zmq.asyncio import Context
    25  
    26  from sawtooth_rest_api.protobuf.validator_pb2 import Message
    27  
    28  LOGGER = logging.getLogger(__name__)
    29  
    30  
    31  class _Backoff:
    32      """Implements a simple backoff mechanism.
    33      """
    34  
    35      def __init__(self, max_retries=3, interval=100, error=Exception()):
    36          self.num_retries = 0
    37          self.max_retries = max_retries
    38          self.interval = interval
    39          self.error = error
    40  
    41      async def do_backoff(self, err_msg=" "):
    42          if self.num_retries == self.max_retries:
    43              LOGGER.warning("Failed sending message to the Validator. No more "
    44                             "retries left. Backoff terminated: %s",
    45                             err_msg)
    46              raise self.error
    47  
    48          self.num_retries += 1
    49          LOGGER.warning("Sleeping for %s ms after failed attempt %s of %s to "
    50                         "send message to the Validator: %s",
    51                         str(self.num_retries),
    52                         str(self.max_retries),
    53                         str(self.interval / 1000),
    54                         err_msg)
    55  
    56          await asyncio.sleep(self.interval / 1000)
    57          self.interval *= 2
    58  
    59  
    60  class _MessageRouter:
    61      """Manages message, routing them either to an incoming queue or to the
    62      futures for expected replies.
    63      """
    64  
    65      def __init__(self):
    66          self._queue = asyncio.Queue()
    67          self._futures = {}
    68  
    69      async def _push_incoming(self, msg):
    70          return await self._queue.put(msg)
    71  
    72      async def incoming(self):
    73          """Returns the next incoming message.
    74          """
    75          msg = await self._queue.get()
    76          self._queue.task_done()
    77          return msg
    78  
    79      def expect_reply(self, correlation_id):
    80          """Informs the router that a reply to the given correlation_id is
    81          expected.
    82          """
    83          self._futures[correlation_id] = asyncio.Future()
    84  
    85      def expected_replies(self):
    86          """Returns the correlation ids for the expected replies.
    87          """
    88          return (c_id for c_id in self._futures)
    89  
    90      async def await_reply(self, correlation_id, timeout=None):
    91          """Wait for a reply to a given correlation id.  If a timeout is
    92          provided, it will raise a asyncio.TimeoutError.
    93          """
    94          try:
    95              result = await asyncio.wait_for(
    96                  self._futures[correlation_id], timeout=timeout)
    97  
    98              return result
    99          finally:
   100              del self._futures[correlation_id]
   101  
   102      def _set_reply(self, correlation_id, msg):
   103          if correlation_id in self._futures:
   104              try:
   105                  self._futures[correlation_id].set_result(msg)
   106              except asyncio.InvalidStateError as e:
   107                  LOGGER.error(
   108                      'Attempting to set result on already-resolved future: %s',
   109                      str(e))
   110  
   111      def _fail_reply(self, correlation_id, err):
   112          if correlation_id in self._futures and \
   113                  not self._futures[correlation_id].done():
   114              try:
   115                  self._futures[correlation_id].set_exception(err)
   116              except asyncio.InvalidStateError as e:
   117                  LOGGER.error(
   118                      'Attempting to set exception on already-resolved future: '
   119                      '%s',
   120                      str(e))
   121  
   122      def fail_all(self, err):
   123          """Fail all the expected replies with a given error.
   124          """
   125          for c_id in self._futures:
   126              self._fail_reply(c_id, err)
   127  
   128      async def route_msg(self, msg):
   129          """Given a message, route it either to the incoming queue, or to the
   130          future associated with its correlation_id.
   131          """
   132          if msg.correlation_id in self._futures:
   133              self._set_reply(msg.correlation_id, msg)
   134          else:
   135              await self._push_incoming(msg)
   136  
   137  
   138  class _Receiver:
   139      """Receives messages and forwards them to a _MessageRouter.
   140      """
   141  
   142      def __init__(self, socket, msg_router):
   143          self._socket = socket
   144          self._msg_router = msg_router
   145  
   146          self._is_running = False
   147  
   148      async def start(self):
   149          """Starts receiving messages on the underlying socket and passes them
   150          to the message router.
   151          """
   152          self._is_running = True
   153  
   154          while self._is_running:
   155              try:
   156                  zmq_msg = await self._socket.recv_multipart()
   157  
   158                  message = Message()
   159                  message.ParseFromString(zmq_msg[-1])
   160  
   161                  await self._msg_router.route_msg(message)
   162              except DecodeError as e:
   163                  LOGGER.warning('Unable to decode: %s', e)
   164              except zmq.ZMQError as e:
   165                  LOGGER.warning('Unable to receive: %s', e)
   166                  return
   167              except asyncio.CancelledError:
   168                  self._is_running = False
   169  
   170      def cancel(self):
   171          self._is_running = False
   172  
   173  
   174  class _Sender:
   175      """Manages Sending messages over a ZMQ socket.
   176      """
   177  
   178      def __init__(self, socket, msg_router):
   179          self._msg_router = msg_router
   180          self._socket = socket
   181  
   182      async def send(self, message_type, message_content, timeout=None):
   183          correlation_id = uuid.uuid4().hex
   184  
   185          self._msg_router.expect_reply(correlation_id)
   186  
   187          message = Message(
   188              correlation_id=correlation_id,
   189              content=message_content,
   190              message_type=message_type)
   191  
   192          # Send the message. Backoff and retry in case of an error
   193          # We want a short backoff and retry attempt, so use the defaults
   194          # of 3 retries with 200ms of backoff
   195          backoff = _Backoff(max_retries=3,
   196                             interval=200,
   197                             error=SendBackoffTimeoutError())
   198  
   199          while True:
   200              try:
   201                  await self._socket.send_multipart(
   202                      [message.SerializeToString()])
   203                  break
   204              except asyncio.CancelledError:
   205                  raise
   206              except zmq.error.Again as e:
   207                  await backoff.do_backoff(err_msg=repr(e))
   208  
   209          return await self._msg_router.await_reply(correlation_id,
   210                                                    timeout=timeout)
   211  
   212  
   213  class DisconnectError(Exception):
   214      """Raised when a connection disconnects.
   215      """
   216  
   217      def __init__(self):
   218          super().__init__("The connection was lost")
   219  
   220  
   221  class SendBackoffTimeoutError(Exception):
   222      """Raised when the send times out.
   223      """
   224  
   225      def __init__(self):
   226          super().__init__("Timed out sending message over ZMQ")
   227  
   228  
   229  class ConnectionEvent(Enum):
   230      """Event types that indicate a state change in a connection.
   231  
   232      Attributes:
   233          DISCONNECTED (int): Event fired when a disconnect occurs
   234          RECONNECTED (int) Event fired on reconnect
   235      """
   236      DISCONNECTED = 1
   237      RECONNECTED = 2
   238  
   239  
   240  class Connection:
   241      """A connection, over which validator Message objects may be sent.
   242      """
   243  
   244      def __init__(self, url):
   245          self._url = url
   246  
   247          self._ctx = Context.instance()
   248          self._socket = self._ctx.socket(zmq.DEALER)
   249          self._socket.identity = uuid.uuid4().hex.encode()[0:16]
   250  
   251          self._msg_router = _MessageRouter()
   252          self._receiver = _Receiver(self._socket, self._msg_router)
   253          self._sender = _Sender(self._socket, self._msg_router)
   254  
   255          self._connection_state_listeners = {}
   256  
   257          self._recv_task = None
   258  
   259          # Monitoring properties
   260          self._monitor_sock = None
   261          self._monitor_fd = None
   262          self._monitor_task = None
   263  
   264      @property
   265      def url(self):
   266          return self._url
   267  
   268      def open(self):
   269          """Opens the connection.
   270  
   271          An open connection will monitor for disconnects from the remote end.
   272          Messages are either received as replies to outgoing messages, or
   273          received from an incoming queue.
   274          """
   275          LOGGER.info('Connecting to %s', self._url)
   276          asyncio.ensure_future(self._do_start())
   277  
   278      def on_connection_state_change(self, event_type, callback):
   279          """Register a callback for a specific connection state change.
   280  
   281          Register a callback to be triggered when the connection changes to
   282          the specified state, signified by a ConnectionEvent.
   283  
   284          The callback must be a coroutine.
   285  
   286          Args:
   287              event_type (ConnectionEvent): the connection event to listen for
   288              callback (coroutine): a coroutine to call on the event occurrence
   289          """
   290          listeners = self._connection_state_listeners.get(event_type, [])
   291          listeners.append(callback)
   292          self._connection_state_listeners[event_type] = listeners
   293  
   294      def _notify_listeners(self, event_type):
   295          listeners = self._connection_state_listeners.get(event_type, [])
   296          for coroutine_fn in listeners:
   297              asyncio.ensure_future(coroutine_fn())
   298  
   299      async def _do_start(self, reconnect=False):
   300          self._socket.connect(self._url)
   301  
   302          self._monitor_fd = "inproc://monitor.s-{}".format(
   303              uuid.uuid4().hex[0:5])
   304          self._monitor_sock = self._socket.get_monitor_socket(
   305              zmq.EVENT_DISCONNECTED,
   306              addr=self._monitor_fd)
   307  
   308          self._recv_task = asyncio.ensure_future(self._receiver.start())
   309  
   310          if reconnect:
   311              self._notify_listeners(ConnectionEvent.RECONNECTED)
   312  
   313          self._monitor_task = asyncio.ensure_future(self._monitor_disconnects())
   314  
   315      async def send(self, message_type, message_content, timeout=None):
   316          """Sends a message and returns a future for the response.
   317          """
   318          return await self._sender.send(
   319              message_type, message_content, timeout=timeout)
   320  
   321      async def receive(self):
   322          """Returns a future for an incoming message.
   323          """
   324          return await self._msg_router.incoming()
   325  
   326      def close(self):
   327          """Closes the connection.
   328  
   329          All outstanding futures for replies will be sent a DisconnectError.
   330          """
   331          if self._recv_task:
   332              self._recv_task.cancel()
   333  
   334          self._disable_monitoring()
   335  
   336          if self._monitor_task and not self._monitor_task.done():
   337              self._monitor_task.cancel()
   338  
   339          self._receiver.cancel()
   340          self._socket.close(linger=0)
   341  
   342          self._msg_router.fail_all(DisconnectError())
   343  
   344      def _disable_monitoring(self):
   345          if self._socket.closed:
   346              return
   347  
   348          self._socket.disable_monitor()
   349  
   350          if self._monitor_sock:
   351              self._monitor_sock.disconnect(self._monitor_fd)
   352              self._monitor_sock.close(linger=0)
   353  
   354              self._monitor_fd = None
   355              self._monitor_sock = None
   356  
   357      async def _monitor_disconnects(self):
   358          try:
   359              cancelled = False
   360              try:
   361                  await self._monitor_sock.recv_multipart()
   362              except asyncio.CancelledError:
   363                  cancelled = True
   364  
   365              # Only message received will be a disconnect event
   366              self._disable_monitoring()
   367  
   368              if not self._socket.closed:
   369                  self._socket.disconnect(self._url)
   370  
   371              # Inform the msg router that all replies failed.
   372              self._msg_router.fail_all(DisconnectError())
   373  
   374              self._recv_task.cancel()
   375              self._recv_task = None
   376  
   377              self._notify_listeners(ConnectionEvent.DISCONNECTED)
   378  
   379              if cancelled:
   380                  return
   381  
   382              # start it back up, but first wait a bit to give the other end time
   383              # to reappear
   384              try:
   385                  await asyncio.sleep(1)
   386              except asyncio.CancelledError:
   387                  # We've been cancelled, so let's just exit
   388                  return
   389  
   390              asyncio.ensure_future(self._do_start(reconnect=True))
   391          except zmq.ZMQError as e:
   392              # The monitor socket was probably closed
   393              LOGGER.warning('Error occurred while monitoring the socket: %s', e)