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)