@@ -78,6 +78,8 @@ class BaseCoordinator:
78
78
79
79
DEFAULT_CONFIG = {
80
80
'group_id' : 'kafka-python-default-group' ,
81
+ 'group_instance_id' : '' ,
82
+ 'leave_group_on_close' : None ,
81
83
'session_timeout_ms' : 10000 ,
82
84
'heartbeat_interval_ms' : 3000 ,
83
85
'max_poll_interval_ms' : 300000 ,
@@ -92,6 +94,12 @@ def __init__(self, client, metrics, **configs):
92
94
group_id (str): name of the consumer group to join for dynamic
93
95
partition assignment (if enabled), and to use for fetching and
94
96
committing offsets. Default: 'kafka-python-default-group'
97
+ group_instance_id (str): the unique identifier to distinguish
98
+ each client instance. If set and leave_group_on_close is
99
+ False consumer group rebalancing won't be triggered until
100
+ sessiont_timeout_ms is met. Requires 2.3.0+.
101
+ leave_group_on_close (bool or None): whether to leave a consumer
102
+ group or not on consumer shutdown.
95
103
session_timeout_ms (int): The timeout used to detect failures when
96
104
using Kafka's group management facilities. Default: 30000
97
105
heartbeat_interval_ms (int): The expected time in milliseconds
@@ -117,6 +125,11 @@ def __init__(self, client, metrics, **configs):
117
125
"different values for max_poll_interval_ms "
118
126
"and session_timeout_ms" )
119
127
128
+ if self .config ['group_instance_id' ] and self .config ['api_version' ] < (2 , 3 , 0 ):
129
+ raise Errors .KafkaConfigurationError (
130
+ 'Broker version %s does not support static membership' % (self .config ['api_version' ],),
131
+ )
132
+
120
133
self ._client = client
121
134
self .group_id = self .config ['group_id' ]
122
135
self .heartbeat = Heartbeat (** self .config )
@@ -451,30 +464,48 @@ def _send_join_group_request(self):
451
464
if self .config ['api_version' ] < (0 , 9 ):
452
465
raise Errors .KafkaError ('JoinGroupRequest api requires 0.9+ brokers' )
453
466
elif (0 , 9 ) <= self .config ['api_version' ] < (0 , 10 , 1 ):
454
- request = JoinGroupRequest [0 ](
467
+ version = 0
468
+ args = (
455
469
self .group_id ,
456
470
self .config ['session_timeout_ms' ],
457
471
self ._generation .member_id ,
458
472
self .protocol_type (),
459
- member_metadata )
473
+ member_metadata ,
474
+ )
460
475
elif (0 , 10 , 1 ) <= self .config ['api_version' ] < (0 , 11 , 0 ):
461
- request = JoinGroupRequest [1 ](
476
+ version = 1
477
+ args = (
462
478
self .group_id ,
463
479
self .config ['session_timeout_ms' ],
464
480
self .config ['max_poll_interval_ms' ],
465
481
self ._generation .member_id ,
466
482
self .protocol_type (),
467
- member_metadata )
483
+ member_metadata ,
484
+ )
485
+ elif self .config ['api_version' ] >= (2 , 3 , 0 ) and self .config ['group_instance_id' ]:
486
+ version = 5
487
+ args = (
488
+ self .group_id ,
489
+ self .config ['session_timeout_ms' ],
490
+ self .config ['max_poll_interval_ms' ],
491
+ self ._generation .member_id ,
492
+ self .config ['group_instance_id' ],
493
+ self .protocol_type (),
494
+ member_metadata ,
495
+ )
468
496
else :
469
- request = JoinGroupRequest [2 ](
497
+ version = 2
498
+ args = (
470
499
self .group_id ,
471
500
self .config ['session_timeout_ms' ],
472
501
self .config ['max_poll_interval_ms' ],
473
502
self ._generation .member_id ,
474
503
self .protocol_type (),
475
- member_metadata )
504
+ member_metadata ,
505
+ )
476
506
477
507
# create the request for the coordinator
508
+ request = JoinGroupRequest [version ](* args )
478
509
log .debug ("Sending JoinGroup (%s) to coordinator %s" , request , self .coordinator_id )
479
510
future = Future ()
480
511
_f = self ._client .send (self .coordinator_id , request )
@@ -558,12 +589,25 @@ def _handle_join_group_response(self, future, send_time, response):
558
589
559
590
def _on_join_follower (self ):
560
591
# send follower's sync group with an empty assignment
561
- version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
562
- request = SyncGroupRequest [version ](
563
- self .group_id ,
564
- self ._generation .generation_id ,
565
- self ._generation .member_id ,
566
- {})
592
+ if self .config ['api_version' ] >= (2 , 3 , 0 ) and self .config ['group_instance_id' ]:
593
+ version = 3
594
+ args = (
595
+ self .group_id ,
596
+ self ._generation .generation_id ,
597
+ self ._generation .member_id ,
598
+ self .config ['group_instance_id' ],
599
+ {},
600
+ )
601
+ else :
602
+ version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
603
+ args = (
604
+ self .group_id ,
605
+ self ._generation .generation_id ,
606
+ self ._generation .member_id ,
607
+ {},
608
+ )
609
+
610
+ request = SyncGroupRequest [version ](* args )
567
611
log .debug ("Sending follower SyncGroup for group %s to coordinator %s: %s" ,
568
612
self .group_id , self .coordinator_id , request )
569
613
return self ._send_sync_group_request (request )
@@ -586,15 +630,30 @@ def _on_join_leader(self, response):
586
630
except Exception as e :
587
631
return Future ().failure (e )
588
632
589
- version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
590
- request = SyncGroupRequest [version ](
591
- self .group_id ,
592
- self ._generation .generation_id ,
593
- self ._generation .member_id ,
594
- [(member_id ,
595
- assignment if isinstance (assignment , bytes ) else assignment .encode ())
596
- for member_id , assignment in group_assignment .items ()])
633
+ group_assignment = [
634
+ (member_id , assignment if isinstance (assignment , bytes ) else assignment .encode ())
635
+ for member_id , assignment in group_assignment .items ()
636
+ ]
637
+
638
+ if self .config ['api_version' ] >= (2 , 3 , 0 ) and self .config ['group_instance_id' ]:
639
+ version = 3
640
+ args = (
641
+ self .group_id ,
642
+ self ._generation .generation_id ,
643
+ self ._generation .member_id ,
644
+ self .config ['group_instance_id' ],
645
+ group_assignment ,
646
+ )
647
+ else :
648
+ version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
649
+ args = (
650
+ self .group_id ,
651
+ self ._generation .generation_id ,
652
+ self ._generation .member_id ,
653
+ group_assignment ,
654
+ )
597
655
656
+ request = SyncGroupRequest [version ](* args )
598
657
log .debug ("Sending leader SyncGroup for group %s to coordinator %s: %s" ,
599
658
self .group_id , self .coordinator_id , request )
600
659
return self ._send_sync_group_request (request )
@@ -760,15 +819,22 @@ def close(self):
760
819
def maybe_leave_group (self ):
761
820
"""Leave the current group and reset local generation/memberId."""
762
821
with self ._client ._lock , self ._lock :
763
- if (not self .coordinator_unknown ()
822
+ if (
823
+ not self .coordinator_unknown ()
764
824
and self .state is not MemberState .UNJOINED
765
- and self ._generation is not Generation .NO_GENERATION ):
766
-
825
+ and self ._generation is not Generation .NO_GENERATION
826
+ and self ._leave_group_on_close ()
827
+ ):
767
828
# this is a minimal effort attempt to leave the group. we do not
768
829
# attempt any resending if the request fails or times out.
769
830
log .info ('Leaving consumer group (%s).' , self .group_id )
770
- version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
771
- request = LeaveGroupRequest [version ](self .group_id , self ._generation .member_id )
831
+ if self .config ['api_version' ] >= (2 , 3 , 0 ) and self .config ['group_instance_id' ]:
832
+ version = 3
833
+ args = (self .group_id , [(self ._generation .member_id , self .config ['group_instance_id' ])])
834
+ else :
835
+ version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
836
+ args = self .group_id , self ._generation .member_id
837
+ request = LeaveGroupRequest [version ](* args )
772
838
future = self ._client .send (self .coordinator_id , request )
773
839
future .add_callback (self ._handle_leave_group_response )
774
840
future .add_errback (log .error , "LeaveGroup request failed: %s" )
@@ -795,10 +861,23 @@ def _send_heartbeat_request(self):
795
861
e = Errors .NodeNotReadyError (self .coordinator_id )
796
862
return Future ().failure (e )
797
863
798
- version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
799
- request = HeartbeatRequest [version ](self .group_id ,
800
- self ._generation .generation_id ,
801
- self ._generation .member_id )
864
+ if self .config ['api_version' ] >= (2 , 3 , 0 ) and self .config ['group_instance_id' ]:
865
+ version = 2
866
+ args = (
867
+ self .group_id ,
868
+ self ._generation .generation_id ,
869
+ self ._generation .member_id ,
870
+ self .config ['group_instance_id' ],
871
+ )
872
+ else :
873
+ version = 0 if self .config ['api_version' ] < (0 , 11 , 0 ) else 1
874
+ args = (
875
+ self .group_id ,
876
+ self ._generation .generation_id ,
877
+ self ._generation .member_id ,
878
+ )
879
+
880
+ request = HeartbeatRequest [version ](* args )
802
881
log .debug ("Heartbeat: %s[%s] %s" , request .group , request .generation_id , request .member_id ) # pylint: disable-msg=no-member
803
882
future = Future ()
804
883
_f = self ._client .send (self .coordinator_id , request )
@@ -845,6 +924,9 @@ def _handle_heartbeat_response(self, future, send_time, response):
845
924
log .error ("Heartbeat failed: Unhandled error: %s" , error )
846
925
future .failure (error )
847
926
927
+ def _leave_group_on_close (self ):
928
+ return self .config ['leave_group_on_close' ] is None or self .config ['leave_group_on_close' ]
929
+
848
930
849
931
class GroupCoordinatorMetrics :
850
932
def __init__ (self , heartbeat , metrics , prefix , tags = None ):
0 commit comments