m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

501 lines
21 KiB

6 months ago
  1. from __future__ import absolute_import
  2. import abc
  3. import logging
  4. import re
  5. from kafka.vendor import six
  6. from kafka.errors import IllegalStateError
  7. from kafka.protocol.offset import OffsetResetStrategy
  8. from kafka.structs import OffsetAndMetadata
  9. log = logging.getLogger(__name__)
  10. class SubscriptionState(object):
  11. """
  12. A class for tracking the topics, partitions, and offsets for the consumer.
  13. A partition is "assigned" either directly with assign_from_user() (manual
  14. assignment) or with assign_from_subscribed() (automatic assignment from
  15. subscription).
  16. Once assigned, the partition is not considered "fetchable" until its initial
  17. position has been set with seek(). Fetchable partitions track a fetch
  18. position which is used to set the offset of the next fetch, and a consumed
  19. position which is the last offset that has been returned to the user. You
  20. can suspend fetching from a partition through pause() without affecting the
  21. fetched/consumed offsets. The partition will remain unfetchable until the
  22. resume() is used. You can also query the pause state independently with
  23. is_paused().
  24. Note that pause state as well as fetch/consumed positions are not preserved
  25. when partition assignment is changed whether directly by the user or
  26. through a group rebalance.
  27. This class also maintains a cache of the latest commit position for each of
  28. the assigned partitions. This is updated through committed() and can be used
  29. to set the initial fetch position (e.g. Fetcher._reset_offset() ).
  30. """
  31. _SUBSCRIPTION_EXCEPTION_MESSAGE = (
  32. "You must choose only one way to configure your consumer:"
  33. " (1) subscribe to specific topics by name,"
  34. " (2) subscribe to topics matching a regex pattern,"
  35. " (3) assign itself specific topic-partitions.")
  36. # Taken from: https://github.com/apache/kafka/blob/39eb31feaeebfb184d98cc5d94da9148c2319d81/clients/src/main/java/org/apache/kafka/common/internals/Topic.java#L29
  37. _MAX_NAME_LENGTH = 249
  38. _TOPIC_LEGAL_CHARS = re.compile('^[a-zA-Z0-9._-]+$')
  39. def __init__(self, offset_reset_strategy='earliest'):
  40. """Initialize a SubscriptionState instance
  41. Keyword Arguments:
  42. offset_reset_strategy: 'earliest' or 'latest', otherwise
  43. exception will be raised when fetching an offset that is no
  44. longer available. Default: 'earliest'
  45. """
  46. try:
  47. offset_reset_strategy = getattr(OffsetResetStrategy,
  48. offset_reset_strategy.upper())
  49. except AttributeError:
  50. log.warning('Unrecognized offset_reset_strategy, using NONE')
  51. offset_reset_strategy = OffsetResetStrategy.NONE
  52. self._default_offset_reset_strategy = offset_reset_strategy
  53. self.subscription = None # set() or None
  54. self.subscribed_pattern = None # regex str or None
  55. self._group_subscription = set()
  56. self._user_assignment = set()
  57. self.assignment = dict()
  58. self.listener = None
  59. # initialize to true for the consumers to fetch offset upon starting up
  60. self.needs_fetch_committed_offsets = True
  61. def subscribe(self, topics=(), pattern=None, listener=None):
  62. """Subscribe to a list of topics, or a topic regex pattern.
  63. Partitions will be dynamically assigned via a group coordinator.
  64. Topic subscriptions are not incremental: this list will replace the
  65. current assignment (if there is one).
  66. This method is incompatible with assign_from_user()
  67. Arguments:
  68. topics (list): List of topics for subscription.
  69. pattern (str): Pattern to match available topics. You must provide
  70. either topics or pattern, but not both.
  71. listener (ConsumerRebalanceListener): Optionally include listener
  72. callback, which will be called before and after each rebalance
  73. operation.
  74. As part of group management, the consumer will keep track of the
  75. list of consumers that belong to a particular group and will
  76. trigger a rebalance operation if one of the following events
  77. trigger:
  78. * Number of partitions change for any of the subscribed topics
  79. * Topic is created or deleted
  80. * An existing member of the consumer group dies
  81. * A new member is added to the consumer group
  82. When any of these events are triggered, the provided listener
  83. will be invoked first to indicate that the consumer's assignment
  84. has been revoked, and then again when the new assignment has
  85. been received. Note that this listener will immediately override
  86. any listener set in a previous call to subscribe. It is
  87. guaranteed, however, that the partitions revoked/assigned
  88. through this interface are from topics subscribed in this call.
  89. """
  90. if self._user_assignment or (topics and pattern):
  91. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  92. assert topics or pattern, 'Must provide topics or pattern'
  93. if pattern:
  94. log.info('Subscribing to pattern: /%s/', pattern)
  95. self.subscription = set()
  96. self.subscribed_pattern = re.compile(pattern)
  97. else:
  98. self.change_subscription(topics)
  99. if listener and not isinstance(listener, ConsumerRebalanceListener):
  100. raise TypeError('listener must be a ConsumerRebalanceListener')
  101. self.listener = listener
  102. def _ensure_valid_topic_name(self, topic):
  103. """ Ensures that the topic name is valid according to the kafka source. """
  104. # See Kafka Source:
  105. # https://github.com/apache/kafka/blob/39eb31feaeebfb184d98cc5d94da9148c2319d81/clients/src/main/java/org/apache/kafka/common/internals/Topic.java
  106. if topic is None:
  107. raise TypeError('All topics must not be None')
  108. if not isinstance(topic, six.string_types):
  109. raise TypeError('All topics must be strings')
  110. if len(topic) == 0:
  111. raise ValueError('All topics must be non-empty strings')
  112. if topic == '.' or topic == '..':
  113. raise ValueError('Topic name cannot be "." or ".."')
  114. if len(topic) > self._MAX_NAME_LENGTH:
  115. raise ValueError('Topic name is illegal, it can\'t be longer than {0} characters, topic: "{1}"'.format(self._MAX_NAME_LENGTH, topic))
  116. if not self._TOPIC_LEGAL_CHARS.match(topic):
  117. raise ValueError('Topic name "{0}" is illegal, it contains a character other than ASCII alphanumerics, ".", "_" and "-"'.format(topic))
  118. def change_subscription(self, topics):
  119. """Change the topic subscription.
  120. Arguments:
  121. topics (list of str): topics for subscription
  122. Raises:
  123. IllegalStateError: if assign_from_user has been used already
  124. TypeError: if a topic is None or a non-str
  125. ValueError: if a topic is an empty string or
  126. - a topic name is '.' or '..' or
  127. - a topic name does not consist of ASCII-characters/'-'/'_'/'.'
  128. """
  129. if self._user_assignment:
  130. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  131. if isinstance(topics, six.string_types):
  132. topics = [topics]
  133. if self.subscription == set(topics):
  134. log.warning("subscription unchanged by change_subscription(%s)",
  135. topics)
  136. return
  137. for t in topics:
  138. self._ensure_valid_topic_name(t)
  139. log.info('Updating subscribed topics to: %s', topics)
  140. self.subscription = set(topics)
  141. self._group_subscription.update(topics)
  142. # Remove any assigned partitions which are no longer subscribed to
  143. for tp in set(self.assignment.keys()):
  144. if tp.topic not in self.subscription:
  145. del self.assignment[tp]
  146. def group_subscribe(self, topics):
  147. """Add topics to the current group subscription.
  148. This is used by the group leader to ensure that it receives metadata
  149. updates for all topics that any member of the group is subscribed to.
  150. Arguments:
  151. topics (list of str): topics to add to the group subscription
  152. """
  153. if self._user_assignment:
  154. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  155. self._group_subscription.update(topics)
  156. def reset_group_subscription(self):
  157. """Reset the group's subscription to only contain topics subscribed by this consumer."""
  158. if self._user_assignment:
  159. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  160. assert self.subscription is not None, 'Subscription required'
  161. self._group_subscription.intersection_update(self.subscription)
  162. def assign_from_user(self, partitions):
  163. """Manually assign a list of TopicPartitions to this consumer.
  164. This interface does not allow for incremental assignment and will
  165. replace the previous assignment (if there was one).
  166. Manual topic assignment through this method does not use the consumer's
  167. group management functionality. As such, there will be no rebalance
  168. operation triggered when group membership or cluster and topic metadata
  169. change. Note that it is not possible to use both manual partition
  170. assignment with assign() and group assignment with subscribe().
  171. Arguments:
  172. partitions (list of TopicPartition): assignment for this instance.
  173. Raises:
  174. IllegalStateError: if consumer has already called subscribe()
  175. """
  176. if self.subscription is not None:
  177. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  178. if self._user_assignment != set(partitions):
  179. self._user_assignment = set(partitions)
  180. for partition in partitions:
  181. if partition not in self.assignment:
  182. self._add_assigned_partition(partition)
  183. for tp in set(self.assignment.keys()) - self._user_assignment:
  184. del self.assignment[tp]
  185. self.needs_fetch_committed_offsets = True
  186. def assign_from_subscribed(self, assignments):
  187. """Update the assignment to the specified partitions
  188. This method is called by the coordinator to dynamically assign
  189. partitions based on the consumer's topic subscription. This is different
  190. from assign_from_user() which directly sets the assignment from a
  191. user-supplied TopicPartition list.
  192. Arguments:
  193. assignments (list of TopicPartition): partitions to assign to this
  194. consumer instance.
  195. """
  196. if not self.partitions_auto_assigned():
  197. raise IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
  198. for tp in assignments:
  199. if tp.topic not in self.subscription:
  200. raise ValueError("Assigned partition %s for non-subscribed topic." % (tp,))
  201. # after rebalancing, we always reinitialize the assignment state
  202. self.assignment.clear()
  203. for tp in assignments:
  204. self._add_assigned_partition(tp)
  205. self.needs_fetch_committed_offsets = True
  206. log.info("Updated partition assignment: %s", assignments)
  207. def unsubscribe(self):
  208. """Clear all topic subscriptions and partition assignments"""
  209. self.subscription = None
  210. self._user_assignment.clear()
  211. self.assignment.clear()
  212. self.subscribed_pattern = None
  213. def group_subscription(self):
  214. """Get the topic subscription for the group.
  215. For the leader, this will include the union of all member subscriptions.
  216. For followers, it is the member's subscription only.
  217. This is used when querying topic metadata to detect metadata changes
  218. that would require rebalancing (the leader fetches metadata for all
  219. topics in the group so that it can do partition assignment).
  220. Returns:
  221. set: topics
  222. """
  223. return self._group_subscription
  224. def seek(self, partition, offset):
  225. """Manually specify the fetch offset for a TopicPartition.
  226. Overrides the fetch offsets that the consumer will use on the next
  227. poll(). If this API is invoked for the same partition more than once,
  228. the latest offset will be used on the next poll(). Note that you may
  229. lose data if this API is arbitrarily used in the middle of consumption,
  230. to reset the fetch offsets.
  231. Arguments:
  232. partition (TopicPartition): partition for seek operation
  233. offset (int): message offset in partition
  234. """
  235. self.assignment[partition].seek(offset)
  236. def assigned_partitions(self):
  237. """Return set of TopicPartitions in current assignment."""
  238. return set(self.assignment.keys())
  239. def paused_partitions(self):
  240. """Return current set of paused TopicPartitions."""
  241. return set(partition for partition in self.assignment
  242. if self.is_paused(partition))
  243. def fetchable_partitions(self):
  244. """Return set of TopicPartitions that should be Fetched."""
  245. fetchable = set()
  246. for partition, state in six.iteritems(self.assignment):
  247. if state.is_fetchable():
  248. fetchable.add(partition)
  249. return fetchable
  250. def partitions_auto_assigned(self):
  251. """Return True unless user supplied partitions manually."""
  252. return self.subscription is not None
  253. def all_consumed_offsets(self):
  254. """Returns consumed offsets as {TopicPartition: OffsetAndMetadata}"""
  255. all_consumed = {}
  256. for partition, state in six.iteritems(self.assignment):
  257. if state.has_valid_position:
  258. all_consumed[partition] = OffsetAndMetadata(state.position, '')
  259. return all_consumed
  260. def need_offset_reset(self, partition, offset_reset_strategy=None):
  261. """Mark partition for offset reset using specified or default strategy.
  262. Arguments:
  263. partition (TopicPartition): partition to mark
  264. offset_reset_strategy (OffsetResetStrategy, optional)
  265. """
  266. if offset_reset_strategy is None:
  267. offset_reset_strategy = self._default_offset_reset_strategy
  268. self.assignment[partition].await_reset(offset_reset_strategy)
  269. def has_default_offset_reset_policy(self):
  270. """Return True if default offset reset policy is Earliest or Latest"""
  271. return self._default_offset_reset_strategy != OffsetResetStrategy.NONE
  272. def is_offset_reset_needed(self, partition):
  273. return self.assignment[partition].awaiting_reset
  274. def has_all_fetch_positions(self):
  275. for state in self.assignment.values():
  276. if not state.has_valid_position:
  277. return False
  278. return True
  279. def missing_fetch_positions(self):
  280. missing = set()
  281. for partition, state in six.iteritems(self.assignment):
  282. if not state.has_valid_position:
  283. missing.add(partition)
  284. return missing
  285. def is_assigned(self, partition):
  286. return partition in self.assignment
  287. def is_paused(self, partition):
  288. return partition in self.assignment and self.assignment[partition].paused
  289. def is_fetchable(self, partition):
  290. return partition in self.assignment and self.assignment[partition].is_fetchable()
  291. def pause(self, partition):
  292. self.assignment[partition].pause()
  293. def resume(self, partition):
  294. self.assignment[partition].resume()
  295. def _add_assigned_partition(self, partition):
  296. self.assignment[partition] = TopicPartitionState()
  297. class TopicPartitionState(object):
  298. def __init__(self):
  299. self.committed = None # last committed OffsetAndMetadata
  300. self.has_valid_position = False # whether we have valid position
  301. self.paused = False # whether this partition has been paused by the user
  302. self.awaiting_reset = False # whether we are awaiting reset
  303. self.reset_strategy = None # the reset strategy if awaitingReset is set
  304. self._position = None # offset exposed to the user
  305. self.highwater = None
  306. self.drop_pending_message_set = False
  307. # The last message offset hint available from a message batch with
  308. # magic=2 which includes deleted compacted messages
  309. self.last_offset_from_message_batch = None
  310. def _set_position(self, offset):
  311. assert self.has_valid_position, 'Valid position required'
  312. self._position = offset
  313. def _get_position(self):
  314. return self._position
  315. position = property(_get_position, _set_position, None, "last position")
  316. def await_reset(self, strategy):
  317. self.awaiting_reset = True
  318. self.reset_strategy = strategy
  319. self._position = None
  320. self.last_offset_from_message_batch = None
  321. self.has_valid_position = False
  322. def seek(self, offset):
  323. self._position = offset
  324. self.awaiting_reset = False
  325. self.reset_strategy = None
  326. self.has_valid_position = True
  327. self.drop_pending_message_set = True
  328. self.last_offset_from_message_batch = None
  329. def pause(self):
  330. self.paused = True
  331. def resume(self):
  332. self.paused = False
  333. def is_fetchable(self):
  334. return not self.paused and self.has_valid_position
  335. class ConsumerRebalanceListener(object):
  336. """
  337. A callback interface that the user can implement to trigger custom actions
  338. when the set of partitions assigned to the consumer changes.
  339. This is applicable when the consumer is having Kafka auto-manage group
  340. membership. If the consumer's directly assign partitions, those
  341. partitions will never be reassigned and this callback is not applicable.
  342. When Kafka is managing the group membership, a partition re-assignment will
  343. be triggered any time the members of the group changes or the subscription
  344. of the members changes. This can occur when processes die, new process
  345. instances are added or old instances come back to life after failure.
  346. Rebalances can also be triggered by changes affecting the subscribed
  347. topics (e.g. when then number of partitions is administratively adjusted).
  348. There are many uses for this functionality. One common use is saving offsets
  349. in a custom store. By saving offsets in the on_partitions_revoked(), call we
  350. can ensure that any time partition assignment changes the offset gets saved.
  351. Another use is flushing out any kind of cache of intermediate results the
  352. consumer may be keeping. For example, consider a case where the consumer is
  353. subscribed to a topic containing user page views, and the goal is to count
  354. the number of page views per users for each five minute window. Let's say
  355. the topic is partitioned by the user id so that all events for a particular
  356. user will go to a single consumer instance. The consumer can keep in memory
  357. a running tally of actions per user and only flush these out to a remote
  358. data store when its cache gets too big. However if a partition is reassigned
  359. it may want to automatically trigger a flush of this cache, before the new
  360. owner takes over consumption.
  361. This callback will execute in the user thread as part of the Consumer.poll()
  362. whenever partition assignment changes.
  363. It is guaranteed that all consumer processes will invoke
  364. on_partitions_revoked() prior to any process invoking
  365. on_partitions_assigned(). So if offsets or other state is saved in the
  366. on_partitions_revoked() call, it should be saved by the time the process
  367. taking over that partition has their on_partitions_assigned() callback
  368. called to load the state.
  369. """
  370. __metaclass__ = abc.ABCMeta
  371. @abc.abstractmethod
  372. def on_partitions_revoked(self, revoked):
  373. """
  374. A callback method the user can implement to provide handling of offset
  375. commits to a customized store on the start of a rebalance operation.
  376. This method will be called before a rebalance operation starts and
  377. after the consumer stops fetching data. It is recommended that offsets
  378. should be committed in this callback to either Kafka or a custom offset
  379. store to prevent duplicate data.
  380. NOTE: This method is only called before rebalances. It is not called
  381. prior to KafkaConsumer.close()
  382. Arguments:
  383. revoked (list of TopicPartition): the partitions that were assigned
  384. to the consumer on the last rebalance
  385. """
  386. pass
  387. @abc.abstractmethod
  388. def on_partitions_assigned(self, assigned):
  389. """
  390. A callback method the user can implement to provide handling of
  391. customized offsets on completion of a successful partition
  392. re-assignment. This method will be called after an offset re-assignment
  393. completes and before the consumer starts fetching data.
  394. It is guaranteed that all the processes in a consumer group will execute
  395. their on_partitions_revoked() callback before any instance executes its
  396. on_partitions_assigned() callback.
  397. Arguments:
  398. assigned (list of TopicPartition): the partitions assigned to the
  399. consumer (may include partitions that were previously assigned)
  400. """
  401. pass