/pika/heartbeat.py

http://github.com/pika/pika · Python · 209 lines · 132 code · 18 blank · 59 comment · 5 complexity · 3627d7348706827503b8b1d34c7b598d MD5 · raw file

  1. """Handle AMQP Heartbeats"""
  2. import logging
  3. import pika.exceptions
  4. from pika import frame
  5. LOGGER = logging.getLogger(__name__)
  6. class HeartbeatChecker(object):
  7. """Sends heartbeats to the broker. The provided timeout is used to
  8. determine if the connection is stale - no received heartbeats or
  9. other activity will close the connection. See the parameter list for more
  10. details.
  11. """
  12. _STALE_CONNECTION = "No activity or too many missed heartbeats in the last %i seconds"
  13. def __init__(self, connection, timeout):
  14. """Create an object that will check for activity on the provided
  15. connection as well as receive heartbeat frames from the broker. The
  16. timeout parameter defines a window within which this activity must
  17. happen. If not, the connection is considered dead and closed.
  18. The value passed for timeout is also used to calculate an interval
  19. at which a heartbeat frame is sent to the broker. The interval is
  20. equal to the timeout value divided by two.
  21. :param pika.connection.Connection: Connection object
  22. :param int timeout: Connection idle timeout. If no activity occurs on the
  23. connection nor heartbeat frames received during the
  24. timeout window the connection will be closed. The
  25. interval used to send heartbeats is calculated from
  26. this value by dividing it by two.
  27. """
  28. if timeout < 1:
  29. raise ValueError('timeout must >= 0, but got %r' % (timeout,))
  30. self._connection = connection
  31. # Note: see the following documents:
  32. # https://www.rabbitmq.com/heartbeats.html#heartbeats-timeout
  33. # https://github.com/pika/pika/pull/1072
  34. # https://groups.google.com/d/topic/rabbitmq-users/Fmfeqe5ocTY/discussion
  35. # There is a certain amount of confusion around how client developers
  36. # interpret the spec. The spec talks about 2 missed heartbeats as a
  37. # *timeout*, plus that any activity on the connection counts for a
  38. # heartbeat. This is to avoid edge cases and not to depend on network
  39. # latency.
  40. self._timeout = timeout
  41. self._send_interval = float(timeout) / 2
  42. # Note: Pika will calculate the heartbeat / connectivity check interval
  43. # by adding 5 seconds to the negotiated timeout to leave a bit of room
  44. # for broker heartbeats that may be right at the edge of the timeout
  45. # window. This is different behavior from the RabbitMQ Java client and
  46. # the spec that suggests a check interval equivalent to two times the
  47. # heartbeat timeout value. But, one advantage of adding a small amount
  48. # is that bad connections will be detected faster.
  49. # https://github.com/pika/pika/pull/1072#issuecomment-397850795
  50. # https://github.com/rabbitmq/rabbitmq-java-client/blob/b55bd20a1a236fc2d1ea9369b579770fa0237615/src/main/java/com/rabbitmq/client/impl/AMQConnection.java#L773-L780
  51. # https://github.com/ruby-amqp/bunny/blob/3259f3af2e659a49c38c2470aa565c8fb825213c/lib/bunny/session.rb#L1187-L1192
  52. self._check_interval = timeout + 5
  53. LOGGER.debug('timeout: %f send_interval: %f check_interval: %f',
  54. self._timeout, self._send_interval, self._check_interval)
  55. # Initialize counters
  56. self._bytes_received = 0
  57. self._bytes_sent = 0
  58. self._heartbeat_frames_received = 0
  59. self._heartbeat_frames_sent = 0
  60. self._idle_byte_intervals = 0
  61. self._send_timer = None
  62. self._check_timer = None
  63. self._start_send_timer()
  64. self._start_check_timer()
  65. @property
  66. def bytes_received_on_connection(self):
  67. """Return the number of bytes received by the connection bytes object.
  68. :rtype int
  69. """
  70. return self._connection.bytes_received
  71. @property
  72. def connection_is_idle(self):
  73. """Returns true if the byte count hasn't changed in enough intervals
  74. to trip the max idle threshold.
  75. """
  76. return self._idle_byte_intervals > 0
  77. def received(self):
  78. """Called when a heartbeat is received"""
  79. LOGGER.debug('Received heartbeat frame')
  80. self._heartbeat_frames_received += 1
  81. def _send_heartbeat(self):
  82. """Invoked by a timer to send a heartbeat when we need to.
  83. """
  84. LOGGER.debug('Sending heartbeat frame')
  85. self._send_heartbeat_frame()
  86. self._start_send_timer()
  87. def _check_heartbeat(self):
  88. """Invoked by a timer to check for broker heartbeats. Checks to see
  89. if we've missed any heartbeats and disconnect our connection if it's
  90. been idle too long.
  91. """
  92. if self._has_received_data:
  93. self._idle_byte_intervals = 0
  94. else:
  95. # Connection has not received any data, increment the counter
  96. self._idle_byte_intervals += 1
  97. LOGGER.debug(
  98. 'Received %i heartbeat frames, sent %i, '
  99. 'idle intervals %i', self._heartbeat_frames_received,
  100. self._heartbeat_frames_sent, self._idle_byte_intervals)
  101. if self.connection_is_idle:
  102. self._close_connection()
  103. return
  104. self._start_check_timer()
  105. def stop(self):
  106. """Stop the heartbeat checker"""
  107. if self._send_timer:
  108. LOGGER.debug('Removing timer for next heartbeat send interval')
  109. self._connection._adapter_remove_timeout(self._send_timer) # pylint: disable=W0212
  110. self._send_timer = None
  111. if self._check_timer:
  112. LOGGER.debug('Removing timer for next heartbeat check interval')
  113. self._connection._adapter_remove_timeout(self._check_timer) # pylint: disable=W0212
  114. self._check_timer = None
  115. def _close_connection(self):
  116. """Close the connection with the AMQP Connection-Forced value."""
  117. LOGGER.info('Connection is idle, %i stale byte intervals',
  118. self._idle_byte_intervals)
  119. text = HeartbeatChecker._STALE_CONNECTION % self._timeout
  120. # Abort the stream connection. There is no point trying to gracefully
  121. # close the AMQP connection since lack of heartbeat suggests that the
  122. # stream is dead.
  123. self._connection._terminate_stream( # pylint: disable=W0212
  124. pika.exceptions.AMQPHeartbeatTimeout(text))
  125. @property
  126. def _has_received_data(self):
  127. """Returns True if the connection has received data.
  128. :rtype: bool
  129. """
  130. return self._bytes_received != self.bytes_received_on_connection
  131. @staticmethod
  132. def _new_heartbeat_frame():
  133. """Return a new heartbeat frame.
  134. :rtype pika.frame.Heartbeat
  135. """
  136. return frame.Heartbeat()
  137. def _send_heartbeat_frame(self):
  138. """Send a heartbeat frame on the connection.
  139. """
  140. LOGGER.debug('Sending heartbeat frame')
  141. self._connection._send_frame( # pylint: disable=W0212
  142. self._new_heartbeat_frame())
  143. self._heartbeat_frames_sent += 1
  144. def _start_send_timer(self):
  145. """Start a new heartbeat send timer."""
  146. self._send_timer = self._connection._adapter_call_later( # pylint: disable=W0212
  147. self._send_interval,
  148. self._send_heartbeat)
  149. def _start_check_timer(self):
  150. """Start a new heartbeat check timer."""
  151. # Note: update counters now to get current values
  152. # at the start of the timeout window. Values will be
  153. # checked against the connection's byte count at the
  154. # end of the window
  155. self._update_counters()
  156. self._check_timer = self._connection._adapter_call_later( # pylint: disable=W0212
  157. self._check_interval,
  158. self._check_heartbeat)
  159. def _update_counters(self):
  160. """Update the internal counters for bytes sent and received and the
  161. number of frames received
  162. """
  163. self._bytes_sent = self._connection.bytes_sent
  164. self._bytes_received = self._connection.bytes_received