@@ -84,6 +84,8 @@ def __init__(self, client, subscription, metrics, **configs):
84
84
self .config [key ] = configs [key ]
85
85
86
86
self ._subscription = subscription
87
+ self ._is_leader = False
88
+ self ._joined_subscription = set ()
87
89
self ._metadata_snapshot = self ._build_metadata_snapshot (subscription , client .cluster )
88
90
self ._assignment_snapshot = None
89
91
self ._cluster = client .cluster
@@ -119,6 +121,7 @@ def __init__(self, client, subscription, metrics, **configs):
119
121
self .consumer_sensors = ConsumerCoordinatorMetrics (
120
122
metrics , self .config ['metric_group_prefix' ], self ._subscription )
121
123
124
+ #self._handle_metadata_update(self._cluster)
122
125
self ._cluster .request_update ()
123
126
self ._cluster .add_listener (WeakMethod (self ._handle_metadata_update ))
124
127
@@ -132,11 +135,22 @@ def protocol_type(self):
132
135
133
136
def group_protocols (self ):
134
137
"""Returns list of preferred (protocols, metadata)"""
135
- topics = self ._subscription .subscription
136
- assert topics is not None , 'Consumer has not subscribed to topics'
138
+ if self ._subscription .subscription is None :
139
+ raise Errors .IllegalStateError ('Consumer has not subscribed to topics' )
140
+ # dpkp note: I really dislike this.
141
+ # why? because we are using this strange method group_protocols,
142
+ # which is seemingly innocuous, to set internal state (_joined_subscription)
143
+ # that is later used to check whether metadata has changed since we joined a group
144
+ # but there is no guarantee that this method, group_protocols, will get called
145
+ # in the correct sequence or that it will only be called when we want it to be.
146
+ # So this really should be moved elsewhere, but I don't have the energy to
147
+ # work that out right now. If you read this at some later date after the mutable
148
+ # state has bitten you... I'm sorry! It mimics the java client, and that's the
149
+ # best I've got for now.
150
+ self ._joined_subscription = set (self ._subscription .subscription )
137
151
metadata_list = []
138
152
for assignor in self .config ['assignors' ]:
139
- metadata = assignor .metadata (topics )
153
+ metadata = assignor .metadata (self . _joined_subscription )
140
154
group_protocol = (assignor .name , metadata )
141
155
metadata_list .append (group_protocol )
142
156
return metadata_list
@@ -158,21 +172,29 @@ def _handle_metadata_update(self, cluster):
158
172
159
173
# check if there are any changes to the metadata which should trigger
160
174
# a rebalance
161
- if self ._subscription_metadata_changed (cluster ):
162
-
163
- if (self .config ['api_version' ] >= (0 , 9 )
164
- and self .config ['group_id' ] is not None ):
165
-
166
- self ._subscription .mark_for_reassignment ()
167
-
168
- # If we haven't got group coordinator support,
169
- # just assign all partitions locally
170
- else :
171
- self ._subscription .assign_from_subscribed ([
172
- TopicPartition (topic , partition )
173
- for topic in self ._subscription .subscription
174
- for partition in self ._metadata_snapshot [topic ]
175
- ])
175
+ if self ._subscription .partitions_auto_assigned ():
176
+ metadata_snapshot = self ._build_metadata_snapshot (self ._subscription , cluster )
177
+ if self ._metadata_snapshot != metadata_snapshot :
178
+ self ._metadata_snapshot = metadata_snapshot
179
+
180
+ # If we haven't got group coordinator support,
181
+ # just assign all partitions locally
182
+ if self ._auto_assign_all_partitions ():
183
+ self ._subscription .assign_from_subscribed ([
184
+ TopicPartition (topic , partition )
185
+ for topic in self ._subscription .subscription
186
+ for partition in self ._metadata_snapshot [topic ]
187
+ ])
188
+
189
+ def _auto_assign_all_partitions (self ):
190
+ # For users that use "subscribe" without group support,
191
+ # we will simply assign all partitions to this consumer
192
+ if self .config ['api_version' ] < (0 , 9 ):
193
+ return True
194
+ elif self .config ['group_id' ] is None :
195
+ return True
196
+ else :
197
+ return False
176
198
177
199
def _build_metadata_snapshot (self , subscription , cluster ):
178
200
metadata_snapshot = {}
@@ -181,16 +203,6 @@ def _build_metadata_snapshot(self, subscription, cluster):
181
203
metadata_snapshot [topic ] = set (partitions )
182
204
return metadata_snapshot
183
205
184
- def _subscription_metadata_changed (self , cluster ):
185
- if not self ._subscription .partitions_auto_assigned ():
186
- return False
187
-
188
- metadata_snapshot = self ._build_metadata_snapshot (self ._subscription , cluster )
189
- if self ._metadata_snapshot != metadata_snapshot :
190
- self ._metadata_snapshot = metadata_snapshot
191
- return True
192
- return False
193
-
194
206
def _lookup_assignor (self , name ):
195
207
for assignor in self .config ['assignors' ]:
196
208
if assignor .name == name :
@@ -199,12 +211,10 @@ def _lookup_assignor(self, name):
199
211
200
212
def _on_join_complete (self , generation , member_id , protocol ,
201
213
member_assignment_bytes ):
202
- # if we were the assignor, then we need to make sure that there have
203
- # been no metadata updates since the rebalance begin. Otherwise, we
204
- # won't rebalance again until the next metadata change
205
- if self ._assignment_snapshot is not None and self ._assignment_snapshot != self ._metadata_snapshot :
206
- self ._subscription .mark_for_reassignment ()
207
- return
214
+ # only the leader is responsible for monitoring for metadata changes
215
+ # (i.e. partition changes)
216
+ if not self ._is_leader :
217
+ self ._assignment_snapshot = None
208
218
209
219
assignor = self ._lookup_assignor (protocol )
210
220
assert assignor , 'Coordinator selected invalid assignment protocol: %s' % protocol
@@ -307,6 +317,7 @@ def _perform_assignment(self, leader_id, assignment_strategy, members):
307
317
# keep track of the metadata used for assignment so that we can check
308
318
# after rebalance completion whether anything has changed
309
319
self ._cluster .request_update ()
320
+ self ._is_leader = True
310
321
self ._assignment_snapshot = self ._metadata_snapshot
311
322
312
323
log .debug ("Performing assignment for group %s using strategy %s"
@@ -338,18 +349,32 @@ def _on_join_prepare(self, generation, member_id):
338
349
" for group %s failed on_partitions_revoked" ,
339
350
self ._subscription .listener , self .group_id )
340
351
341
- self ._assignment_snapshot = None
342
- self ._subscription .mark_for_reassignment ()
352
+ self ._is_leader = False
353
+ self ._subscription .reset_group_subscription ()
343
354
344
355
def need_rejoin (self ):
345
356
"""Check whether the group should be rejoined
346
357
347
358
Returns:
348
359
bool: True if consumer should rejoin group, False otherwise
349
360
"""
350
- return (self ._subscription .partitions_auto_assigned () and
351
- (super (ConsumerCoordinator , self ).need_rejoin () or
352
- self ._subscription .needs_partition_assignment ))
361
+ if not self ._subscription .partitions_auto_assigned ():
362
+ return False
363
+
364
+ if self ._auto_assign_all_partitions ():
365
+ return False
366
+
367
+ # we need to rejoin if we performed the assignment and metadata has changed
368
+ if (self ._assignment_snapshot is not None
369
+ and self ._assignment_snapshot != self ._metadata_snapshot ):
370
+ return True
371
+
372
+ # we need to join if our subscription has changed since the last join
373
+ if (self ._joined_subscription is not None
374
+ and self ._joined_subscription != self ._subscription .subscription ):
375
+ return True
376
+
377
+ return super (ConsumerCoordinator , self ).need_rejoin ()
353
378
354
379
def refresh_committed_offsets_if_needed (self ):
355
380
"""Fetch committed offsets for assigned partitions."""
0 commit comments