@@ -135,6 +135,7 @@ class KafkaClient(object):
135
135
'bootstrap_servers' : 'localhost' ,
136
136
'client_id' : 'kafka-python-' + __version__ ,
137
137
'request_timeout_ms' : 40000 ,
138
+ 'connections_max_idle_ms' : 9 * 60 * 1000 ,
138
139
'reconnect_backoff_ms' : 50 ,
139
140
'max_in_flight_requests_per_connection' : 5 ,
140
141
'receive_buffer_bytes' : None ,
@@ -194,6 +195,7 @@ def __init__(self, **configs):
194
195
self ._wake_r .setblocking (False )
195
196
self ._wake_lock = threading .Lock ()
196
197
self ._selector .register (self ._wake_r , selectors .EVENT_READ )
198
+ self ._idle_expiry_manager = IdleConnectionManager (self .config ['connections_max_idle_ms' ])
197
199
self ._closed = False
198
200
self ._sensors = None
199
201
if self .config ['metrics' ]:
@@ -291,6 +293,8 @@ def _conn_state_change(self, node_id, conn):
291
293
if self ._sensors :
292
294
self ._sensors .connection_created .record ()
293
295
296
+ self ._idle_expiry_manager .update (node_id )
297
+
294
298
if 'bootstrap' in self ._conns and node_id != 'bootstrap' :
295
299
bootstrap = self ._conns .pop ('bootstrap' )
296
300
# XXX: make conn.close() require error to cause refresh
@@ -308,7 +312,13 @@ def _conn_state_change(self, node_id, conn):
308
312
pass
309
313
if self ._sensors :
310
314
self ._sensors .connection_closed .record ()
311
- if self ._refresh_on_disconnects and not self ._closed :
315
+
316
+ idle_disconnect = False
317
+ if self ._idle_expiry_manager .is_expired (node_id ):
318
+ idle_disconnect = True
319
+ self ._idle_expiry_manager .remove (node_id )
320
+
321
+ if self ._refresh_on_disconnects and not self ._closed and not idle_disconnect :
312
322
log .warning ("Node %s connection failed -- refreshing metadata" , node_id )
313
323
self .cluster .request_update ()
314
324
@@ -513,10 +523,12 @@ def poll(self, timeout_ms=None, future=None, sleep=True):
513
523
if future and future .is_done :
514
524
timeout = 0
515
525
else :
526
+ idle_connection_timeout_ms = self ._idle_expiry_manager .next_check_ms ()
516
527
timeout = min (
517
528
timeout_ms ,
518
529
metadata_timeout_ms ,
519
530
self ._delayed_tasks .next_at () * 1000 ,
531
+ idle_connection_timeout_ms ,
520
532
self .config ['request_timeout_ms' ])
521
533
timeout = max (0 , timeout / 1000.0 ) # avoid negative timeouts
522
534
@@ -571,6 +583,8 @@ def _poll(self, timeout, sleep=True):
571
583
conn .close (Errors .ConnectionError ('Socket EVENT_READ without in-flight-requests' ))
572
584
continue
573
585
586
+ self ._idle_expiry_manager .update (conn .node_id )
587
+
574
588
# Accumulate as many responses as the connection has pending
575
589
while conn .in_flight_requests :
576
590
response = conn .recv () # Note: conn.recv runs callbacks / errbacks
@@ -592,6 +606,7 @@ def _poll(self, timeout, sleep=True):
592
606
593
607
if self ._sensors :
594
608
self ._sensors .io_time .record ((time .time () - end_select ) * 1000000000 )
609
+ self ._maybe_close_oldest_connection ()
595
610
return responses
596
611
597
612
def in_flight_request_count (self , node_id = None ):
@@ -844,6 +859,14 @@ def _clear_wake_fd(self):
844
859
except socket .error :
845
860
break
846
861
862
+ def _maybe_close_oldest_connection (self ):
863
+ expired_connection = self ._idle_expiry_manager .poll_expired_connection ()
864
+ if expired_connection :
865
+ conn_id , ts = expired_connection
866
+ idle_ms = (time .time () - ts ) * 1000
867
+ log .info ('Closing idle connection %s, last active %d ms ago' , conn_id , idle_ms )
868
+ self .close (node_id = conn_id )
869
+
847
870
848
871
class DelayedTaskQueue (object ):
849
872
# see https://docs.python.org/2/library/heapq.html
@@ -918,6 +941,76 @@ def pop_ready(self):
918
941
return ready_tasks
919
942
920
943
944
+ # OrderedDict requires python2.7+
945
+ try :
946
+ from collections import OrderedDict
947
+ except ImportError :
948
+ # If we dont have OrderedDict, we'll fallback to dict with O(n) priority reads
949
+ OrderedDict = dict
950
+
951
+
952
+ class IdleConnectionManager (object ):
953
+ def __init__ (self , connections_max_idle_ms ):
954
+ if connections_max_idle_ms > 0 :
955
+ self .connections_max_idle = connections_max_idle_ms / 1000
956
+ else :
957
+ self .connections_max_idle = float ('inf' )
958
+ self .next_idle_close_check_time = None
959
+ self .update_next_idle_close_check_time (time .time ())
960
+ self .lru_connections = OrderedDict ()
961
+
962
+ def update (self , conn_id ):
963
+ # order should reflect last-update
964
+ if conn_id in self .lru_connections :
965
+ del self .lru_connections [conn_id ]
966
+ self .lru_connections [conn_id ] = time .time ()
967
+
968
+ def remove (self , conn_id ):
969
+ if conn_id in self .lru_connections :
970
+ del self .lru_connections [conn_id ]
971
+
972
+ def is_expired (self , conn_id ):
973
+ if conn_id not in self .lru_connections :
974
+ return None
975
+ return time .time () >= self .lru_connections [conn_id ] + self .connections_max_idle
976
+
977
+ def next_check_ms (self ):
978
+ now = time .time ()
979
+ if not self .lru_connections :
980
+ return float ('inf' )
981
+ elif self .next_idle_close_check_time <= now :
982
+ return 0
983
+ else :
984
+ return int ((self .next_idle_close_check_time - now ) * 1000 )
985
+
986
+ def update_next_idle_close_check_time (self , ts ):
987
+ self .next_idle_close_check_time = ts + self .connections_max_idle
988
+
989
+ def poll_expired_connection (self ):
990
+ if time .time () < self .next_idle_close_check_time :
991
+ return None
992
+
993
+ if not len (self .lru_connections ):
994
+ return None
995
+
996
+ oldest_conn_id = None
997
+ oldest_ts = None
998
+ if OrderedDict is dict :
999
+ for conn_id , ts in self .lru_connections .items ():
1000
+ if oldest_conn_id is None or ts < oldest_ts :
1001
+ oldest_conn_id = conn_id
1002
+ oldest_ts = ts
1003
+ else :
1004
+ (oldest_conn_id , oldest_ts ) = next (iter (self .lru_connections .items ()))
1005
+
1006
+ self .update_next_idle_close_check_time (oldest_ts )
1007
+
1008
+ if time .time () >= oldest_ts + self .connections_max_idle :
1009
+ return (oldest_conn_id , oldest_ts )
1010
+ else :
1011
+ return None
1012
+
1013
+
921
1014
class KafkaClientMetrics (object ):
922
1015
def __init__ (self , metrics , metric_group_prefix , conns ):
923
1016
self .metrics = metrics
0 commit comments