kafka-python 3.0.2__py3-none-any.whl → 3.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kafka/cli/admin/cluster/versions.py +1 -1
- kafka/cli/common.py +88 -14
- kafka/cluster.py +3 -5
- kafka/consumer/fetcher.py +68 -14
- kafka/coordinator/base.py +3 -4
- kafka/net/connection.py +23 -11
- kafka/net/manager.py +29 -12
- kafka/net/selector.py +131 -59
- kafka/net/transport.py +18 -4
- kafka/net/wakeup_notifier.py +2 -5
- kafka/protocol/broker_version_data.py +5 -1
- kafka/version.py +1 -1
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/METADATA +1 -1
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/RECORD +18 -18
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/WHEEL +0 -0
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/entry_points.txt +0 -0
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/licenses/LICENSE +0 -0
- {kafka_python-3.0.2.dist-info → kafka_python-3.0.4.dist-info}/top_level.txt +0 -0
kafka/cli/common.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import logging.config
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def add_connect_cli_args(parser, bootstrap_required=True):
|
|
@@ -37,9 +38,13 @@ def build_kwargs(props):
|
|
|
37
38
|
def build_connect_kwargs(config):
|
|
38
39
|
if not config.bootstrap_servers:
|
|
39
40
|
raise ValueError('python -m kafka: error: the following arguments are required: -b/--bootstrap-servers')
|
|
41
|
+
# Accept both repeated -b flags and comma-separated lists within a single flag
|
|
42
|
+
bootstrap_servers = []
|
|
43
|
+
for entry in config.bootstrap_servers:
|
|
44
|
+
bootstrap_servers.extend(s.strip() for s in entry.split(',') if s.strip())
|
|
40
45
|
kwargs = build_kwargs(config.extra_config)
|
|
41
46
|
kwargs.update({
|
|
42
|
-
'bootstrap_servers':
|
|
47
|
+
'bootstrap_servers': bootstrap_servers,
|
|
43
48
|
'security_protocol': config.security_protocol,
|
|
44
49
|
'sasl_mechanism': config.sasl_mechanism,
|
|
45
50
|
'sasl_plain_username': config.sasl_user,
|
|
@@ -59,12 +64,60 @@ def add_logging_cli_args(parser):
|
|
|
59
64
|
logging_group.add_argument(
|
|
60
65
|
'-D', '--disable-logger', type=str, action='append',
|
|
61
66
|
help='disable a specific logger. Can be provided multiple times.')
|
|
67
|
+
logging_group.add_argument(
|
|
68
|
+
'--log-format', type=str, default=None,
|
|
69
|
+
help='log message format string, passed to logging.Formatter')
|
|
70
|
+
logging_group.add_argument(
|
|
71
|
+
'--log-date-format', type=str, default=None,
|
|
72
|
+
help='log date format string, passed to logging.Formatter')
|
|
73
|
+
logging_group.add_argument(
|
|
74
|
+
'--log-file', type=str, default=None,
|
|
75
|
+
help='write logs to this file instead of stderr')
|
|
76
|
+
logging_group.add_argument(
|
|
77
|
+
'--log-config', type=str, default=None,
|
|
78
|
+
help='path to a logging configuration file for full control over handlers, '
|
|
79
|
+
'formatters, etc. A .json (or .yaml/.yml, if PyYAML is installed) file is '
|
|
80
|
+
'loaded as a logging.config.dictConfig; any other extension is loaded as a '
|
|
81
|
+
'logging.config.fileConfig. The file owns handlers/formatters, so --log-format, '
|
|
82
|
+
'--log-date-format and --log-file are ignored, but --enable-logger and '
|
|
83
|
+
'--disable-logger still apply as logger level adjustments.')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def add_extended_cli_args(parser):
|
|
62
87
|
extended_group = parser.add_argument_group('extended')
|
|
63
88
|
extended_group.add_argument(
|
|
64
89
|
'-C', '--extra-config', type=str, action='append',
|
|
65
90
|
help='additional configuration properties for client in "key=val" format. Can be provided multiple times.')
|
|
66
91
|
|
|
67
92
|
|
|
93
|
+
def _load_log_config(path):
|
|
94
|
+
"""Configure logging from a dictConfig (.json/.yaml) or fileConfig (.ini) file."""
|
|
95
|
+
if path.endswith(('.yaml', '.yml')):
|
|
96
|
+
try:
|
|
97
|
+
import yaml
|
|
98
|
+
except ImportError:
|
|
99
|
+
raise ValueError('PyYAML is required to load a YAML logging config: %s' % (path,))
|
|
100
|
+
with open(path) as f:
|
|
101
|
+
_dict_config(yaml.safe_load(f))
|
|
102
|
+
elif path.endswith('.json'):
|
|
103
|
+
import json
|
|
104
|
+
with open(path) as f:
|
|
105
|
+
_dict_config(json.load(f))
|
|
106
|
+
else:
|
|
107
|
+
# disable_existing_loggers defaults to True, which would silence loggers
|
|
108
|
+
# configured before this call (e.g. the loggers we are about to enable);
|
|
109
|
+
# keep them around so --enable-logger still works.
|
|
110
|
+
logging.config.fileConfig(path, disable_existing_loggers=False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _dict_config(cfg):
|
|
114
|
+
# Default disable_existing_loggers to False (dictConfig defaults to True), so a
|
|
115
|
+
# config that does not mention an already-created logger does not silence it.
|
|
116
|
+
# A config may still set it explicitly to opt into the stdlib default.
|
|
117
|
+
cfg.setdefault('disable_existing_loggers', False)
|
|
118
|
+
logging.config.dictConfig(cfg)
|
|
119
|
+
|
|
120
|
+
|
|
68
121
|
def configure_logging(config):
|
|
69
122
|
_LOGGING_LEVELS = {
|
|
70
123
|
'NOTSET': 0,
|
|
@@ -74,22 +127,43 @@ def configure_logging(config):
|
|
|
74
127
|
'ERROR': 40,
|
|
75
128
|
'CRITICAL': 50,
|
|
76
129
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
logger
|
|
85
|
-
|
|
130
|
+
log_level = _LOGGING_LEVELS[config.log_level.upper()]
|
|
131
|
+
if getattr(config, 'log_config', None):
|
|
132
|
+
_load_log_config(config.log_config)
|
|
133
|
+
if config.enable_logger is not None:
|
|
134
|
+
# Preserve the no-config behavior of --enable-logger: ONLY the named
|
|
135
|
+
# loggers emit. The config file owns the handlers/formatters, so rather
|
|
136
|
+
# than attach our own we reuse them: silence everything else by raising
|
|
137
|
+
# the root level, then let each enabled logger opt back in and propagate
|
|
138
|
+
# its records up to the config's handlers.
|
|
139
|
+
logging.getLogger().setLevel(logging.CRITICAL + 1)
|
|
140
|
+
for name in config.enable_logger:
|
|
141
|
+
logger = logging.getLogger(name)
|
|
142
|
+
logger.disabled = False
|
|
143
|
+
logger.setLevel(log_level)
|
|
86
144
|
else:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
log_format = config.log_format or logging.BASIC_FORMAT
|
|
146
|
+
if config.enable_logger is not None:
|
|
147
|
+
if config.log_file:
|
|
148
|
+
handler = logging.FileHandler(config.log_file)
|
|
149
|
+
else:
|
|
150
|
+
handler = logging.StreamHandler()
|
|
151
|
+
handler.setLevel(log_level)
|
|
152
|
+
handler.setFormatter(logging.Formatter(log_format, datefmt=config.log_date_format))
|
|
153
|
+
for name in config.enable_logger:
|
|
154
|
+
logger = logging.getLogger(name)
|
|
155
|
+
logger.setLevel(log_level)
|
|
156
|
+
logger.addHandler(handler)
|
|
157
|
+
else:
|
|
158
|
+
logging.basicConfig(
|
|
159
|
+
level=log_level, format=log_format,
|
|
160
|
+
datefmt=config.log_date_format, filename=config.log_file)
|
|
161
|
+
# --disable-logger silences a named logger in either mode by raising its level.
|
|
162
|
+
for name in config.disable_logger or []:
|
|
163
|
+
logging.getLogger(name).setLevel(logging.CRITICAL + 1)
|
|
91
164
|
|
|
92
165
|
|
|
93
166
|
def add_common_cli_args(parser, bootstrap_required=True):
|
|
94
167
|
add_connect_cli_args(parser, bootstrap_required)
|
|
95
168
|
add_logging_cli_args(parser)
|
|
169
|
+
add_extended_cli_args(parser)
|
kafka/cluster.py
CHANGED
|
@@ -125,9 +125,8 @@ class ClusterMetadata:
|
|
|
125
125
|
if ttl_ms == 0:
|
|
126
126
|
try:
|
|
127
127
|
await self.refresh_metadata()
|
|
128
|
-
except Errors.KafkaError
|
|
129
|
-
log.debug('Metadata refresh failed
|
|
130
|
-
log.exception(exc)
|
|
128
|
+
except Errors.KafkaError:
|
|
129
|
+
log.debug('Metadata refresh failed', exc_info=True)
|
|
131
130
|
continue
|
|
132
131
|
try:
|
|
133
132
|
log.debug('Sleeping %s for next Metadata refresh', ttl_ms / 1000)
|
|
@@ -168,7 +167,6 @@ class ClusterMetadata:
|
|
|
168
167
|
raise Errors.NodeNotReadyError('metadata')
|
|
169
168
|
else:
|
|
170
169
|
self._manager.reset_backoff('metadata')
|
|
171
|
-
log.info(f'Metadata refresh (node_id={node_id})')
|
|
172
170
|
try:
|
|
173
171
|
request = self.metadata_request()
|
|
174
172
|
log.debug("Sending metadata request %s to node %s", request, node_id)
|
|
@@ -177,7 +175,7 @@ class ClusterMetadata:
|
|
|
177
175
|
log.error('Metadata refresh: failed %s', exc)
|
|
178
176
|
self.failed_update(exc)
|
|
179
177
|
raise
|
|
180
|
-
log.debug('Metadata refresh: success')
|
|
178
|
+
log.debug(f'Metadata refresh: success (node_id={node_id})')
|
|
181
179
|
self.update_metadata(response)
|
|
182
180
|
|
|
183
181
|
def _generate_bootstrap_brokers(self):
|
kafka/consumer/fetcher.py
CHANGED
|
@@ -232,9 +232,18 @@ class Fetcher:
|
|
|
232
232
|
# No records yet. Block until either an in-flight fetch
|
|
233
233
|
# completes (records may have arrived) or a pending offset-reset
|
|
234
234
|
# task completes (positions become available, enabling a fetch
|
|
235
|
-
# on the next caller iteration).
|
|
236
|
-
#
|
|
237
|
-
#
|
|
235
|
+
# on the next caller iteration).
|
|
236
|
+
#
|
|
237
|
+
# add_both fires synchronously on an already-done future: if a fetch
|
|
238
|
+
# response lands between the drain above and this wait setup, _wake
|
|
239
|
+
# fires immediately so we re-drain instead of stalling for the full
|
|
240
|
+
# timeout.
|
|
241
|
+
#
|
|
242
|
+
# This relies on _fetch_futures holding only *recent* completions.
|
|
243
|
+
# otherwise a fetch that completed and was already drained iterations
|
|
244
|
+
# ago lingers behind a slow broker's in-flight fetch and re-fires
|
|
245
|
+
# _wake on every call, busy-looping the poll loop until that slow
|
|
246
|
+
# fetch finally returns.
|
|
238
247
|
waited_on = list(self._fetch_futures)
|
|
239
248
|
if self._reset_task is not None and not self._reset_task.is_done:
|
|
240
249
|
waited_on.append(self._reset_task)
|
|
@@ -277,21 +286,66 @@ class Fetcher:
|
|
|
277
286
|
future.add_both(self._clear_pending_fetch_request, node_id)
|
|
278
287
|
futures.append(future)
|
|
279
288
|
self._fetch_futures.extend(futures)
|
|
280
|
-
self._clean_done_fetch_futures()
|
|
289
|
+
await self._clean_done_fetch_futures()
|
|
281
290
|
return futures
|
|
282
291
|
|
|
283
|
-
def _clean_done_fetch_futures(self):
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
async def _clean_done_fetch_futures(self):
|
|
293
|
+
# Drop every completed fetch future. With multiple brokers, fetches
|
|
294
|
+
# may complete out of order. fetch_records() relies on _fetch_futures
|
|
295
|
+
# holding only recent completions (it fires _wake synchronously on any
|
|
296
|
+
# done future to avoid stalling -- see the wait setup there); a
|
|
297
|
+
# lingering stale completion re-fires that wake on every call and busy-
|
|
298
|
+
# loops the poll loop until the slow broker's in-flight fetch returns.
|
|
299
|
+
#
|
|
300
|
+
# Threading: this REBINDS self._fetch_futures, which must happen on the
|
|
301
|
+
# IO thread so it never races the foreground's list(self._fetch_futures)
|
|
302
|
+
# read in fetch_records(). Defined async to enforce that -- the body
|
|
303
|
+
# can only run by being driven on the IO loop (awaited from another
|
|
304
|
+
# coroutine, or scheduled via manager.run/call_soon), so the rebind
|
|
305
|
+
# always executes on the IO thread regardless of who initiates it.
|
|
306
|
+
# The rebind is a single atomic attribute store, so a foreground reader
|
|
307
|
+
# always sees either the old or the new deque, never a half-cleaned one.
|
|
308
|
+
#
|
|
309
|
+
# Two alternate designs we considered (either would remove the need for
|
|
310
|
+
# this "evict every done future + rebind" dance):
|
|
311
|
+
#
|
|
312
|
+
# 1. Wakeup flag (Apache Kafka Java client, FetchBuffer). Instead of
|
|
313
|
+
# waiting on the fetch-future objects, wait on a single consumable
|
|
314
|
+
# signal: the IO thread sets a flag (wokenup) when it buffers a
|
|
315
|
+
# completed fetch; the foreground's wait loops `while not woken:
|
|
316
|
+
# await` and consumes the flag (compareAndSet true->false) on each
|
|
317
|
+
# pass. Because the signal is cleared on consumption and is not
|
|
318
|
+
# re-derived from lingering future objects, a stale/drained
|
|
319
|
+
# completion cannot re-trigger it -- so no busy-loop and no
|
|
320
|
+
# per-call cleanup of a future list at all. This is the most
|
|
321
|
+
# faithful port of the threaded Java consumer's design.
|
|
322
|
+
#
|
|
323
|
+
# 2. Per-node fetch tracking. Key fetches by broker: dict[node_id,
|
|
324
|
+
# deque] (or just dict[node_id, Future], since _create_fetch_-
|
|
325
|
+
# requests keeps at most one in-flight fetch per node). Within a
|
|
326
|
+
# single connection responses return in request order, so each
|
|
327
|
+
# per-node deque completes in order and the simple head-only
|
|
328
|
+
# popleft cleanup is correct again -- no out-of-order stranding,
|
|
329
|
+
# and cleanup is an in-place popleft (atomic, no rebind, so the
|
|
330
|
+
# threading note above goes away). This structure could also
|
|
331
|
+
# subsume _nodes_with_pending_fetch_requests entirely ("pending"
|
|
332
|
+
# == the node's last future is not done), collapsing two
|
|
333
|
+
# structures into one source of truth.
|
|
334
|
+
if not self._fetch_futures:
|
|
335
|
+
return
|
|
336
|
+
self._fetch_futures = collections.deque(
|
|
337
|
+
fut for fut in self._fetch_futures if not fut.is_done)
|
|
290
338
|
|
|
291
339
|
def in_flight_fetches(self):
|
|
292
|
-
"""Return True if there are any unprocessed FetchRequests
|
|
293
|
-
|
|
294
|
-
|
|
340
|
+
"""Return True if there are any unprocessed (incomplete) FetchRequests
|
|
341
|
+
in flight."""
|
|
342
|
+
|
|
343
|
+
# Read-only on purpose: this may be called from the foreground thread,
|
|
344
|
+
# which must not mutate _fetch_futures (see _clean_done_fetch_futures --
|
|
345
|
+
# cleanup is IO-thread-only). Snapshot first so we never iterate the
|
|
346
|
+
# deque while the IO thread extends it, and check is_done directly
|
|
347
|
+
# rather than relying on a prior cleanup pass.
|
|
348
|
+
return any(not fut.is_done for fut in list(self._fetch_futures))
|
|
295
349
|
|
|
296
350
|
def reset_offsets_if_needed(self, timeout_ms=None):
|
|
297
351
|
"""Schedule pending offset resets and return the in-flight Task.
|
kafka/coordinator/base.py
CHANGED
|
@@ -1117,10 +1117,11 @@ class BaseCoordinator(ABC):
|
|
|
1117
1117
|
try:
|
|
1118
1118
|
send_time = time.monotonic()
|
|
1119
1119
|
response = await self._manager.send(request, node_id=self.coordinator_id)
|
|
1120
|
-
return self._handle_heartbeat_response(response, send_time)
|
|
1121
1120
|
except Errors.KafkaError as exc:
|
|
1122
1121
|
self._failed_request(self.coordinator_id, request, exc)
|
|
1123
1122
|
raise
|
|
1123
|
+
else:
|
|
1124
|
+
return self._handle_heartbeat_response(response, send_time)
|
|
1124
1125
|
|
|
1125
1126
|
def _handle_heartbeat_response(self, response, send_time):
|
|
1126
1127
|
if self._sensors:
|
|
@@ -1138,8 +1139,7 @@ class BaseCoordinator(ABC):
|
|
|
1138
1139
|
self.coordinator_id)
|
|
1139
1140
|
self.coordinator_dead(error)
|
|
1140
1141
|
elif error_type is Errors.RebalanceInProgressError:
|
|
1141
|
-
heartbeat_log.
|
|
1142
|
-
" rebalancing", self.group_id)
|
|
1142
|
+
heartbeat_log.info("Group %s is rebalancing; rejoining.", self.group_id)
|
|
1143
1143
|
self.request_rejoin()
|
|
1144
1144
|
elif error_type is Errors.IllegalGenerationError:
|
|
1145
1145
|
heartbeat_log.warning("Heartbeat failed for group %s: generation id is not "
|
|
@@ -1158,7 +1158,6 @@ class BaseCoordinator(ABC):
|
|
|
1158
1158
|
heartbeat_log.error("Heartbeat failed: authorization error: %s", error)
|
|
1159
1159
|
else:
|
|
1160
1160
|
heartbeat_log.error("Heartbeat failed: Unhandled error: %s", error)
|
|
1161
|
-
|
|
1162
1161
|
raise error
|
|
1163
1162
|
|
|
1164
1163
|
|
kafka/net/connection.py
CHANGED
|
@@ -96,7 +96,7 @@ class KafkaConnection:
|
|
|
96
96
|
return self._init_future
|
|
97
97
|
|
|
98
98
|
def __await__(self):
|
|
99
|
-
yield self.init_future
|
|
99
|
+
yield from self.init_future.__await__() # == await self.init_future; raises on failure
|
|
100
100
|
return self
|
|
101
101
|
|
|
102
102
|
@property
|
|
@@ -203,7 +203,7 @@ class KafkaConnection:
|
|
|
203
203
|
if req_correlation_id != resp_correlation_id:
|
|
204
204
|
return self.close(Errors.KafkaConnectionError('Received unrecognized correlation id'))
|
|
205
205
|
|
|
206
|
-
self.net.
|
|
206
|
+
self.net.cancel(timeout_task)
|
|
207
207
|
latency_ms = (time.monotonic() - sent_time) * 1000
|
|
208
208
|
if self._sensors:
|
|
209
209
|
self._sensors.request_time.record(latency_ms)
|
|
@@ -239,8 +239,10 @@ class KafkaConnection:
|
|
|
239
239
|
self._init_future.failure(error)
|
|
240
240
|
if not self._close_future.is_done:
|
|
241
241
|
if exc is None:
|
|
242
|
+
log.info('%s: Connection closed', self)
|
|
242
243
|
self._close_future.success(None)
|
|
243
244
|
else:
|
|
245
|
+
log.error('%s: Connection lost: %s', self, exc)
|
|
244
246
|
self._close_future.failure(exc)
|
|
245
247
|
|
|
246
248
|
def fail_in_flight_requests(self, error):
|
|
@@ -252,7 +254,7 @@ class KafkaConnection:
|
|
|
252
254
|
future.failure(error)
|
|
253
255
|
while self.in_flight_requests:
|
|
254
256
|
_, future, _, _, timeout_task = self.in_flight_requests.popleft()
|
|
255
|
-
self.net.
|
|
257
|
+
self.net.cancel(timeout_task)
|
|
256
258
|
future.failure(error)
|
|
257
259
|
|
|
258
260
|
def connection_made(self, transport):
|
|
@@ -262,6 +264,13 @@ class KafkaConnection:
|
|
|
262
264
|
To receive data, wait for data_received() calls.
|
|
263
265
|
When the connection is closed, connection_lost() is called.
|
|
264
266
|
"""
|
|
267
|
+
if self.closed:
|
|
268
|
+
# A concurrent close() may have torn the connection down while the
|
|
269
|
+
# transport was still being built. Setting initializing=True below
|
|
270
|
+
# would resurrect an already-closed connection mid-teardown and
|
|
271
|
+
# break the fail_in_flight_requests invariant; refuse instead. The
|
|
272
|
+
# caller (manager._connect) closes the orphaned transport.
|
|
273
|
+
raise Errors.KafkaConnectionError('Connection closed during connect')
|
|
265
274
|
self.transport = transport
|
|
266
275
|
if self.transport.get_protocol() != self:
|
|
267
276
|
self.transport.set_protocol(self)
|
|
@@ -276,6 +285,7 @@ class KafkaConnection:
|
|
|
276
285
|
client_id=self.config['client_id'],
|
|
277
286
|
receive_message_max_bytes=self.config['receive_message_max_bytes'],
|
|
278
287
|
ident=log_prefix)
|
|
288
|
+
log.debug('%s: Connection made', self)
|
|
279
289
|
|
|
280
290
|
def pause(self, v):
|
|
281
291
|
self.paused.add(v)
|
|
@@ -362,6 +372,7 @@ class KafkaConnection:
|
|
|
362
372
|
self.close(error)
|
|
363
373
|
else:
|
|
364
374
|
self._init_complete()
|
|
375
|
+
log.info('%s: Connected', self)
|
|
365
376
|
|
|
366
377
|
async def _get_api_versions(self, timeout_at=None):
|
|
367
378
|
if timeout_at is None:
|
|
@@ -400,11 +411,15 @@ class KafkaConnection:
|
|
|
400
411
|
api_versions = {api_version.api_key: (api_version.min_version, api_version.max_version)
|
|
401
412
|
for api_version in response.api_keys}
|
|
402
413
|
bvd = BrokerVersionData(api_versions=api_versions)
|
|
403
|
-
|
|
404
|
-
|
|
414
|
+
if self.broker_version_data is None:
|
|
415
|
+
log.info('%s: Broker version identified as %s', self, bvd.broker_version_str)
|
|
405
416
|
self.broker_version_data = bvd
|
|
406
|
-
|
|
407
|
-
log.info('%s:
|
|
417
|
+
elif self.broker_version_data > bvd:
|
|
418
|
+
log.info('%s: Broker version identified as %s (lower than user-supplied %s)', self, bvd.broker_version_str, self.broker_version_data.broker_version_str)
|
|
419
|
+
self.broker_version_data = bvd
|
|
420
|
+
elif self.broker_version_data is not None and self.broker_version_data < bvd:
|
|
421
|
+
log.info('%s: Broker version identified as %s; clamping to user-supplied %s', self, bvd.broker_version_str, self.broker_version_data.broker_version_str)
|
|
422
|
+
# No log if user-supplied api_version is the same as broker-identified version
|
|
408
423
|
|
|
409
424
|
@property
|
|
410
425
|
def sasl_enabled(self):
|
|
@@ -534,10 +549,7 @@ class SaslReauthenticator:
|
|
|
534
549
|
"""Cancel any pending re-auth and fail the drain awaiter if present.
|
|
535
550
|
Called from KafkaConnection.connection_lost."""
|
|
536
551
|
if self._task is not None:
|
|
537
|
-
|
|
538
|
-
self._conn.net.unschedule(self._task)
|
|
539
|
-
except (ValueError, KeyError):
|
|
540
|
-
pass
|
|
552
|
+
self._conn.net.cancel(self._task)
|
|
541
553
|
self._task = None
|
|
542
554
|
if self._drain_future is not None and not self._drain_future.is_done:
|
|
543
555
|
self._drain_future.failure(Errors.KafkaConnectionError())
|
kafka/net/manager.py
CHANGED
|
@@ -73,7 +73,7 @@ class KafkaConnectionManager:
|
|
|
73
73
|
"client_dns_lookup must be one of %s; got %r"
|
|
74
74
|
% (self._VALID_DNS_LOOKUP_MODES, self.config['client_dns_lookup']))
|
|
75
75
|
|
|
76
|
-
if 'socks5_proxy'
|
|
76
|
+
if configs.get('socks5_proxy') is not None:
|
|
77
77
|
if self.config['proxy_url'] is None:
|
|
78
78
|
log.warning('socks5_proxy is deprecated, use proxy_url instead')
|
|
79
79
|
self.config['proxy_url'] = configs['socks5_proxy']
|
|
@@ -117,7 +117,9 @@ class KafkaConnectionManager:
|
|
|
117
117
|
async def _do_bootstrap(self, deadline):
|
|
118
118
|
while not self.closed and (deadline is None or time.monotonic() < deadline):
|
|
119
119
|
bootstrap_broker = random.choice(self.cluster.bootstrap_brokers())
|
|
120
|
-
log.
|
|
120
|
+
log.info('Attempting bootstrap to %s at %s:%s (rack %s)',
|
|
121
|
+
bootstrap_broker.node_id, bootstrap_broker.host,
|
|
122
|
+
bootstrap_broker.port, bootstrap_broker.rack)
|
|
121
123
|
try:
|
|
122
124
|
timeout_ms = (deadline - time.monotonic()) * 1000 if deadline is not None else None
|
|
123
125
|
conn = self.get_connection(bootstrap_broker.node_id,
|
|
@@ -129,7 +131,7 @@ class KafkaConnectionManager:
|
|
|
129
131
|
delay = self.connection_delay(bootstrap_broker.node_id)
|
|
130
132
|
if deadline is not None:
|
|
131
133
|
delay = min(delay, max(0, deadline - time.monotonic()))
|
|
132
|
-
log.
|
|
134
|
+
log.warning('Bootstrap %s not ready; waiting %.2f secs', bootstrap_broker.node_id, delay)
|
|
133
135
|
await self._bootstrap_wakeup(delay)
|
|
134
136
|
continue
|
|
135
137
|
|
|
@@ -145,12 +147,12 @@ class KafkaConnectionManager:
|
|
|
145
147
|
try:
|
|
146
148
|
await self.cluster.refresh_metadata(bootstrap_broker.node_id)
|
|
147
149
|
if not self.cluster.brokers():
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
backoff_ms = self.update_backoff(bootstrap_broker.node_id)
|
|
151
|
+
log.warning('Bootstrap metadata response has no brokers. Retrying in %.2f secs.', backoff_ms / 1000)
|
|
150
152
|
continue
|
|
151
153
|
except Exception as exc:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
backoff_ms = self.update_backoff(bootstrap_broker.node_id)
|
|
155
|
+
log.error(f'Bootstrap attempt to {bootstrap_broker.node_id} failed: {exc} (backoff {(backoff_ms / 1000):.2f} secs)')
|
|
154
156
|
continue
|
|
155
157
|
else:
|
|
156
158
|
self.reset_backoff(bootstrap_broker.node_id)
|
|
@@ -158,6 +160,7 @@ class KafkaConnectionManager:
|
|
|
158
160
|
log.info('Bootstrap complete: %s', self.cluster)
|
|
159
161
|
return True
|
|
160
162
|
finally:
|
|
163
|
+
log.info('Closing bootstrap connection %s', bootstrap_broker.node_id)
|
|
161
164
|
self._conns.pop(bootstrap_broker.node_id, conn).close()
|
|
162
165
|
else:
|
|
163
166
|
raise Errors.KafkaTimeoutError(
|
|
@@ -239,9 +242,22 @@ class KafkaConnectionManager:
|
|
|
239
242
|
return transport
|
|
240
243
|
|
|
241
244
|
async def _connect(self, node, conn, reset_backoff_on_connect=True, timeout_at=None):
|
|
245
|
+
# Tracks ownership of the freshly built transport: while non-None it is
|
|
246
|
+
# ours to clean up (the connection hasn't taken it over yet), so the
|
|
247
|
+
# finally clause closes it. Cleared once connection_made() succeeds.
|
|
248
|
+
transport = None
|
|
242
249
|
try:
|
|
243
250
|
transport = await self._build_transport(node, timeout_at=timeout_at)
|
|
251
|
+
# The connection (or the whole manager) may have been closed while
|
|
252
|
+
# we were building the transport. Handing it to connection_made()
|
|
253
|
+
# would flip the conn back to `initializing` and resurrect a
|
|
254
|
+
# connection that is already being torn down. Discard
|
|
255
|
+
# the new transport instead of reviving a dead connection.
|
|
256
|
+
if conn.closed or self.closed:
|
|
257
|
+
log.debug('%s: closed during connect; discarding new transport', conn)
|
|
258
|
+
return
|
|
244
259
|
conn.connection_made(transport)
|
|
260
|
+
transport = None # conn owns cleanup now; skip finally: transport.close()
|
|
245
261
|
await conn.initialize(timeout_at=timeout_at)
|
|
246
262
|
except Exception as exc:
|
|
247
263
|
log.error('Connection failed: %s', exc)
|
|
@@ -251,6 +267,9 @@ class KafkaConnectionManager:
|
|
|
251
267
|
Errors.AuthorizationError)):
|
|
252
268
|
self._auth_failures[node.node_id] = exc
|
|
253
269
|
return
|
|
270
|
+
finally:
|
|
271
|
+
if transport is not None:
|
|
272
|
+
transport.close()
|
|
254
273
|
|
|
255
274
|
if self._sensors:
|
|
256
275
|
self._sensors.connection_created.record()
|
|
@@ -275,6 +294,7 @@ class KafkaConnectionManager:
|
|
|
275
294
|
node = self.cluster.broker_metadata(node_id)
|
|
276
295
|
if node is None:
|
|
277
296
|
raise Errors.UnknownBrokerIdError(node_id)
|
|
297
|
+
log.info('Initializing connection for node_id %s at %s:%s (rack=%s)', node_id, node.host, node.port, node.rack)
|
|
278
298
|
conn = KafkaConnection(self._net, node_id=node_id, broker_version_data=self.broker_version_data, **self.config)
|
|
279
299
|
if pop_on_close:
|
|
280
300
|
conn.close_future.add_both(lambda _: self._conns.pop(node.node_id, None))
|
|
@@ -359,6 +379,7 @@ class KafkaConnectionManager:
|
|
|
359
379
|
node_id, backoff_ms, connect_ms, failures)
|
|
360
380
|
backoff_until_time = time.monotonic() + (backoff_ms / 1000)
|
|
361
381
|
self._backoff[node_id] = (failures, backoff_until_time, connect_ms)
|
|
382
|
+
return backoff_ms
|
|
362
383
|
|
|
363
384
|
def connection_delay(self, node_id):
|
|
364
385
|
"""Connection delay in seconds.
|
|
@@ -423,11 +444,7 @@ class KafkaConnectionManager:
|
|
|
423
444
|
try:
|
|
424
445
|
return await wrapper
|
|
425
446
|
finally:
|
|
426
|
-
|
|
427
|
-
try:
|
|
428
|
-
self._net.unschedule(timer)
|
|
429
|
-
except ValueError:
|
|
430
|
-
pass
|
|
447
|
+
self._net.cancel(timer)
|
|
431
448
|
|
|
432
449
|
def call_soon(self, coro, *args):
|
|
433
450
|
"""Accepts a coroutine / awaitable / function and schedules it on the event loop.
|
kafka/net/selector.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
import copy
|
|
3
|
+
import enum
|
|
3
4
|
import inspect
|
|
4
5
|
import logging
|
|
5
6
|
import heapq
|
|
@@ -48,12 +49,25 @@ class KernelEvent:
|
|
|
48
49
|
return (yield self)
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
class TaskState(enum.Enum):
|
|
53
|
+
CREATED = 'created'
|
|
54
|
+
SCHEDULED = 'scheduled' # in _scheduled heap
|
|
55
|
+
UNSCHEDULED = 'unscheduled' # maybe lost
|
|
56
|
+
READY = 'ready' # in _ready deque
|
|
57
|
+
RUNNING = 'running' # is _current
|
|
58
|
+
WAIT_IO = 'wait_io' # parked on I/O
|
|
59
|
+
WAIT_FUTURE = 'wait_future' # waiting on Future to resolve
|
|
60
|
+
DONE = 'done' # completed (exception is None or not)
|
|
61
|
+
CANCELLED = 'cancelled'
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
class Task:
|
|
52
65
|
def __init__(self, coro):
|
|
53
66
|
self._stack = (_initialize_coro(coro), None)
|
|
54
67
|
self._res = None
|
|
55
68
|
self._exc = None
|
|
56
69
|
self.scheduled_at = None
|
|
70
|
+
self.state = TaskState.CREATED
|
|
57
71
|
|
|
58
72
|
def __lt__(self, other):
|
|
59
73
|
# heapq requires the heap entries to be orderable. When two tasks
|
|
@@ -93,6 +107,7 @@ class Task:
|
|
|
93
107
|
self._stack = self._stack[1]
|
|
94
108
|
if not self._stack:
|
|
95
109
|
# we're done, back to event loop
|
|
110
|
+
self.state = TaskState.DONE
|
|
96
111
|
self._res = final.value
|
|
97
112
|
raise
|
|
98
113
|
else:
|
|
@@ -102,6 +117,7 @@ class Task:
|
|
|
102
117
|
except BaseException as e:
|
|
103
118
|
self._stack = self._stack[1]
|
|
104
119
|
if not self._stack:
|
|
120
|
+
self.state = TaskState.DONE
|
|
105
121
|
self._exc = e
|
|
106
122
|
raise
|
|
107
123
|
else:
|
|
@@ -123,19 +139,20 @@ class Task:
|
|
|
123
139
|
self._exc = exc
|
|
124
140
|
|
|
125
141
|
def close(self):
|
|
142
|
+
if self.is_done:
|
|
143
|
+
return
|
|
144
|
+
assert self.state is not TaskState.RUNNING
|
|
126
145
|
stack = self._stack
|
|
127
146
|
while stack:
|
|
128
147
|
coro, stack = stack
|
|
129
148
|
if inspect.isgenerator(coro) or inspect.iscoroutine(coro):
|
|
130
149
|
try:
|
|
131
150
|
coro.close()
|
|
132
|
-
except ValueError:
|
|
133
|
-
# currently-executing coroutine -- can't close it from
|
|
134
|
-
# within itself; bail without corrupting _stack.
|
|
135
|
-
return
|
|
136
151
|
except Exception:
|
|
137
152
|
log.exception('Error closing coroutine for cancelled task')
|
|
138
153
|
self._stack = None
|
|
154
|
+
self.state = TaskState.CANCELLED
|
|
155
|
+
self._exc = Errors.Cancelled()
|
|
139
156
|
|
|
140
157
|
@property
|
|
141
158
|
def is_done(self):
|
|
@@ -298,7 +315,11 @@ class NetworkSelector:
|
|
|
298
315
|
try:
|
|
299
316
|
state['value'] = await self._invoke(coro, *args)
|
|
300
317
|
except BaseException as exc:
|
|
301
|
-
|
|
318
|
+
# fail_pending_waiters sets 'exception'; dont overwrite
|
|
319
|
+
if state['exception'] is None:
|
|
320
|
+
state['exception'] = exc
|
|
321
|
+
elif not isinstance(exc, GeneratorExit):
|
|
322
|
+
log.warning("During exception %s, caught additional error %s (ignoring)", state['exception'], exc)
|
|
302
323
|
finally:
|
|
303
324
|
with self._pending_waiters_lock:
|
|
304
325
|
self._pending_waiters.pop(event, None)
|
|
@@ -321,6 +342,7 @@ class NetworkSelector:
|
|
|
321
342
|
if not isinstance(task, Task):
|
|
322
343
|
task = Task(task)
|
|
323
344
|
task.scheduled_at = when
|
|
345
|
+
task.state = TaskState.SCHEDULED
|
|
324
346
|
heapq.heappush(self._scheduled, (when, task))
|
|
325
347
|
self._pending_tasks.add(task)
|
|
326
348
|
return task
|
|
@@ -331,10 +353,20 @@ class NetworkSelector:
|
|
|
331
353
|
self.call_at(time.monotonic() + delay, task)
|
|
332
354
|
return task
|
|
333
355
|
|
|
356
|
+
def _add_ready_task(self, task):
|
|
357
|
+
self._ready.append(task)
|
|
358
|
+
task.state = TaskState.READY
|
|
359
|
+
|
|
360
|
+
def _task_done(self, task):
|
|
361
|
+
if not task.is_done:
|
|
362
|
+
raise RuntimeError('Task is not done yet!')
|
|
363
|
+
self._pending_tasks.discard(task)
|
|
364
|
+
task.state = TaskState.DONE
|
|
365
|
+
|
|
334
366
|
def call_soon(self, task):
|
|
335
367
|
if not isinstance(task, Task):
|
|
336
368
|
task = Task(task)
|
|
337
|
-
self.
|
|
369
|
+
self._add_ready_task(task)
|
|
338
370
|
self._pending_tasks.add(task)
|
|
339
371
|
return task
|
|
340
372
|
|
|
@@ -378,29 +410,38 @@ class NetworkSelector:
|
|
|
378
410
|
result = await result
|
|
379
411
|
return result
|
|
380
412
|
|
|
381
|
-
def
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
def _unschedule(self, task):
|
|
414
|
+
assert task.state is TaskState.SCHEDULED
|
|
415
|
+
assert task.scheduled_at is not None
|
|
416
|
+
try:
|
|
417
|
+
self._scheduled.remove((task.scheduled_at, task))
|
|
418
|
+
except ValueError:
|
|
419
|
+
pass
|
|
420
|
+
else:
|
|
421
|
+
# re-heapify to ensure heap structure is valid
|
|
422
|
+
heapq.heapify(self._scheduled)
|
|
423
|
+
task.scheduled_at = None
|
|
424
|
+
task.state = TaskState.UNSCHEDULED
|
|
391
425
|
|
|
392
|
-
def
|
|
393
|
-
if task
|
|
426
|
+
def cancel(self, task):
|
|
427
|
+
if task.state in (TaskState.DONE, TaskState.CANCELLED):
|
|
394
428
|
return
|
|
429
|
+
elif task.state is TaskState.RUNNING:
|
|
430
|
+
assert task is self._current
|
|
431
|
+
self._current.state = TaskState.CANCELLED
|
|
432
|
+
return
|
|
433
|
+
elif task.state is TaskState.SCHEDULED:
|
|
434
|
+
self._unschedule(task)
|
|
435
|
+
elif task.state is TaskState.WAIT_IO:
|
|
436
|
+
# close() below drives the io_guard finalizer, which unregisters
|
|
437
|
+
# the fileobj and cancels any paired timeout timer.
|
|
438
|
+
pass
|
|
395
439
|
self._pending_tasks.discard(task)
|
|
396
440
|
task.close()
|
|
397
441
|
|
|
398
|
-
def unschedule(self, task):
|
|
399
|
-
self._remove_scheduled(task)
|
|
400
|
-
self._retire_task(task)
|
|
401
|
-
|
|
402
442
|
def reschedule(self, when, task):
|
|
403
|
-
|
|
443
|
+
if task.state is TaskState.SCHEDULED:
|
|
444
|
+
self._unschedule(task)
|
|
404
445
|
self.call_at(when, task)
|
|
405
446
|
return task
|
|
406
447
|
|
|
@@ -425,37 +466,45 @@ class NetworkSelector:
|
|
|
425
466
|
def _wait_io(self, fileobj, event, timeout_at):
|
|
426
467
|
suspended = self._current
|
|
427
468
|
self.register_event(fileobj, event, suspended)
|
|
428
|
-
if timeout_at is None or self._closed:
|
|
429
|
-
suspended.push_stack(lambda: self.unregister_event(fileobj, event))
|
|
430
|
-
return
|
|
431
469
|
|
|
432
|
-
|
|
470
|
+
timer = None # set below iff there is a timeout
|
|
433
471
|
|
|
434
|
-
def
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
472
|
+
def io_guard():
|
|
473
|
+
# Primed and parked on the stack just above the waiting coroutine.
|
|
474
|
+
# Its finally runs exactly once, whichever way the wait ends:
|
|
475
|
+
# I/O ready -> driven past the yield
|
|
476
|
+
# cancel() -> Task.close() throws GeneratorExit in at the yield
|
|
477
|
+
# timeout -> on_timeout injects an exc that propagates through
|
|
478
|
+
try:
|
|
479
|
+
yield
|
|
480
|
+
finally:
|
|
481
|
+
if timer is not None and not timer.is_done:
|
|
482
|
+
self.cancel(timer)
|
|
483
|
+
self.unregister_event(fileobj, event)
|
|
484
|
+
|
|
485
|
+
guard = io_guard()
|
|
486
|
+
next(guard) # prime: suspend at the yield so close() triggers finally
|
|
487
|
+
suspended.push_stack(guard)
|
|
488
|
+
suspended.state = TaskState.WAIT_IO
|
|
489
|
+
|
|
490
|
+
if timeout_at is None or self._closed:
|
|
491
|
+
return
|
|
442
492
|
|
|
443
493
|
def on_timeout():
|
|
444
|
-
|
|
494
|
+
nonlocal timer
|
|
495
|
+
timer = None # we are the timer; don't try to cancel ourselves
|
|
496
|
+
if suspended.is_done:
|
|
445
497
|
return
|
|
446
|
-
state['fired'] = True
|
|
447
|
-
self.unregister_event(fileobj, event)
|
|
448
498
|
suspended.inject_exc(Errors.KafkaTimeoutError('I/O wait timed out'))
|
|
449
|
-
self.
|
|
499
|
+
self._add_ready_task(suspended)
|
|
450
500
|
|
|
451
|
-
|
|
452
|
-
state['timer'] = self.call_at(timeout_at, on_timeout)
|
|
501
|
+
timer = self.call_at(timeout_at, on_timeout)
|
|
453
502
|
|
|
454
503
|
def _schedule_tasks(self):
|
|
455
504
|
while self._scheduled and self._scheduled[0][0] <= time.monotonic():
|
|
456
505
|
_, task = heapq.heappop(self._scheduled)
|
|
457
506
|
task.scheduled_at = None
|
|
458
|
-
self.
|
|
507
|
+
self._add_ready_task(task)
|
|
459
508
|
|
|
460
509
|
def _next_scheduled_timeout(self, now):
|
|
461
510
|
try:
|
|
@@ -489,7 +538,11 @@ class NetworkSelector:
|
|
|
489
538
|
self._selector.unregister(fileobj)
|
|
490
539
|
else:
|
|
491
540
|
self._selector.modify(fileobj, events, (None, writer) if event == selectors.EVENT_READ else (reader, None))
|
|
492
|
-
except KeyError:
|
|
541
|
+
except (KeyError, ValueError):
|
|
542
|
+
# KeyError: fileobj was never registered.
|
|
543
|
+
# ValueError: fileobj is closed (fileno() == -1) and no longer in
|
|
544
|
+
# the selector map -- e.g. the socket was closed before the wait's
|
|
545
|
+
# io_guard ran during shutdown. Either way there is nothing to do.
|
|
493
546
|
pass
|
|
494
547
|
|
|
495
548
|
def add_reader(self, fileobj, task):
|
|
@@ -555,35 +608,52 @@ class NetworkSelector:
|
|
|
555
608
|
n = len(self._ready)
|
|
556
609
|
for i in range(n):
|
|
557
610
|
self._current = self._ready.popleft()
|
|
611
|
+
# Silently skip tasks that are done or cancelled
|
|
612
|
+
if self._current.state in (TaskState.DONE, TaskState.CANCELLED):
|
|
613
|
+
continue
|
|
614
|
+
self._current.state = TaskState.RUNNING
|
|
558
615
|
step_start = time.monotonic() if threshold else None
|
|
559
616
|
try:
|
|
560
617
|
log_trace('Calling task %s', self._current)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
event = self._current(inject)
|
|
565
|
-
else:
|
|
566
|
-
event = self._current()
|
|
618
|
+
# __call__ consumes self._exc (set via inject_exc) itself;
|
|
619
|
+
# don't clear it here or the injected exception is dropped.
|
|
620
|
+
event = self._current()
|
|
567
621
|
|
|
568
622
|
except StopIteration:
|
|
569
|
-
|
|
570
|
-
# (and its coroutine, frames, locals) is now collectable.
|
|
571
|
-
self._pending_tasks.discard(self._current)
|
|
623
|
+
self._task_done(self._current)
|
|
572
624
|
|
|
573
|
-
except BaseException
|
|
574
|
-
log.exception(
|
|
625
|
+
except BaseException:
|
|
626
|
+
log.exception('Unhandled exception in task %s:', self._current)
|
|
575
627
|
# Same as StopIteration -- task is done either way.
|
|
576
|
-
self.
|
|
628
|
+
self._task_done(self._current)
|
|
577
629
|
|
|
578
630
|
else:
|
|
579
|
-
if
|
|
631
|
+
if self._current.state is TaskState.CANCELLED:
|
|
632
|
+
# ignores any returned KernelEvent/Future
|
|
633
|
+
self._pending_tasks.discard(self._current)
|
|
634
|
+
self._current.close()
|
|
635
|
+
elif isinstance(event, KernelEvent):
|
|
580
636
|
log_trace('kernel event %s', event.method)
|
|
581
|
-
|
|
637
|
+
try:
|
|
638
|
+
getattr(self, event.method)(*event.args)
|
|
639
|
+
except BaseException as e:
|
|
640
|
+
log_trace('kernel event %s raised %r; injecting into %s',
|
|
641
|
+
event.method, e, self._current)
|
|
642
|
+
self._current.inject_exc(e)
|
|
643
|
+
self._add_ready_task(self._current)
|
|
582
644
|
elif isinstance(event, Future):
|
|
583
645
|
event.add_both(lambda _, task=self._current: self.call_soon(task))
|
|
646
|
+
self._current.state = TaskState.WAIT_FUTURE
|
|
584
647
|
else:
|
|
585
648
|
raise RuntimeError('Unhandled event type: %s' % event)
|
|
586
649
|
|
|
650
|
+
finally:
|
|
651
|
+
# No Task should leave io_loop in RUNNING state.
|
|
652
|
+
if self._current is not None and self._current.state is TaskState.RUNNING:
|
|
653
|
+
log.warning('Task %s left RUNNING after step; demoting to '
|
|
654
|
+
'UNSCHEDULED', self._current)
|
|
655
|
+
self._current.state = TaskState.UNSCHEDULED
|
|
656
|
+
|
|
587
657
|
if threshold:
|
|
588
658
|
elapsed = time.monotonic() - step_start
|
|
589
659
|
if elapsed > threshold:
|
|
@@ -632,6 +702,8 @@ class NetworkSelector:
|
|
|
632
702
|
if self._io_thread is not None:
|
|
633
703
|
self.stop()
|
|
634
704
|
self.drain()
|
|
705
|
+
for task in list(self._pending_tasks):
|
|
706
|
+
self.cancel(task)
|
|
635
707
|
for s in (self._wakeup_r, self._wakeup_w):
|
|
636
708
|
try:
|
|
637
709
|
self._selector.unregister(s)
|
|
@@ -664,12 +736,12 @@ class NetworkSelector:
|
|
|
664
736
|
|
|
665
737
|
if events & selectors.EVENT_WRITE:
|
|
666
738
|
if writer is not None:
|
|
667
|
-
self.
|
|
739
|
+
self._add_ready_task(writer)
|
|
668
740
|
else:
|
|
669
741
|
log.warning("Selector got WRITE event without writer...")
|
|
670
742
|
|
|
671
743
|
if events & selectors.EVENT_READ:
|
|
672
744
|
if reader is not None:
|
|
673
|
-
self.
|
|
745
|
+
self._add_ready_task(reader)
|
|
674
746
|
else:
|
|
675
747
|
log.warning("Selector got READ event without reader...")
|
kafka/net/transport.py
CHANGED
|
@@ -186,7 +186,7 @@ class KafkaTCPTransport:
|
|
|
186
186
|
|
|
187
187
|
async def _write_to_sock(self):
|
|
188
188
|
try:
|
|
189
|
-
while self.
|
|
189
|
+
while self._write_buffer:
|
|
190
190
|
await self._net.wait_write(self._sock)
|
|
191
191
|
total_bytes, err = self._sock_send()
|
|
192
192
|
if err:
|
|
@@ -197,11 +197,18 @@ class KafkaTCPTransport:
|
|
|
197
197
|
self._protocol._sensors.bytes_sent.record(total_bytes)
|
|
198
198
|
finally:
|
|
199
199
|
self._writing = False
|
|
200
|
-
if
|
|
201
|
-
self.
|
|
200
|
+
if self._closed:
|
|
201
|
+
self._close()
|
|
202
|
+
elif not self._write:
|
|
203
|
+
try:
|
|
204
|
+
self._sock.shutdown(socket.SHUT_WR)
|
|
205
|
+
except OSError:
|
|
206
|
+
pass
|
|
202
207
|
|
|
203
208
|
def _sock_send(self):
|
|
204
209
|
total_bytes = 0
|
|
210
|
+
if self._sock is None:
|
|
211
|
+
return total_bytes, Errors.KafkaConnectionError('Connection closed during send')
|
|
205
212
|
while self._write_buffer:
|
|
206
213
|
next_chunk = self._write_buffer.popleft()
|
|
207
214
|
# Wrap in memoryview so partial-send slicing is O(1) instead of
|
|
@@ -260,6 +267,10 @@ class KafkaTCPTransport:
|
|
|
260
267
|
self._net.unregister_event(sock, selectors.EVENT_READ | selectors.EVENT_WRITE)
|
|
261
268
|
except (KeyError, ValueError):
|
|
262
269
|
pass
|
|
270
|
+
try:
|
|
271
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
272
|
+
except OSError:
|
|
273
|
+
pass
|
|
263
274
|
sock.close()
|
|
264
275
|
proto = self._protocol
|
|
265
276
|
self._protocol = None
|
|
@@ -333,7 +344,7 @@ class KafkaTCPTransport:
|
|
|
333
344
|
return self.writelines(data)
|
|
334
345
|
|
|
335
346
|
async def handshake(self):
|
|
336
|
-
|
|
347
|
+
log.info('%s: connected to %s', self, self._sock)
|
|
337
348
|
|
|
338
349
|
def host_port(self):
|
|
339
350
|
if self._sock is None:
|
|
@@ -365,6 +376,7 @@ class KafkaSSLTransport(KafkaTCPTransport):
|
|
|
365
376
|
while True:
|
|
366
377
|
try:
|
|
367
378
|
self._sock.do_handshake()
|
|
379
|
+
log.info('%s: connected to %s', self, self._sock)
|
|
368
380
|
return
|
|
369
381
|
except ssl.SSLWantReadError:
|
|
370
382
|
await self._net.wait_read(self._sock)
|
|
@@ -397,6 +409,8 @@ class KafkaSSLTransport(KafkaTCPTransport):
|
|
|
397
409
|
def _sock_send(self):
|
|
398
410
|
total_bytes = 0
|
|
399
411
|
err = None
|
|
412
|
+
if self._sock is None:
|
|
413
|
+
return total_bytes, Errors.KafkaConnectionError('Connection closed during send')
|
|
400
414
|
while self._write_buffer:
|
|
401
415
|
next_chunk = self._write_buffer.popleft()
|
|
402
416
|
while next_chunk:
|
kafka/net/wakeup_notifier.py
CHANGED
|
@@ -53,11 +53,8 @@ class WakeupNotifier:
|
|
|
53
53
|
await self._fut
|
|
54
54
|
finally:
|
|
55
55
|
self._fut = None
|
|
56
|
-
if timer is not None
|
|
57
|
-
|
|
58
|
-
self._net.unschedule(timer)
|
|
59
|
-
except (ValueError, RuntimeError, ReferenceError):
|
|
60
|
-
pass
|
|
56
|
+
if timer is not None:
|
|
57
|
+
self._net.cancel(timer)
|
|
61
58
|
|
|
62
59
|
def notify(self):
|
|
63
60
|
# Always queue _wakeup on the IO thread. Skipping the queue when
|
|
@@ -100,8 +100,12 @@ class BrokerVersionData:
|
|
|
100
100
|
f" and broker [{broker_min_version}-{broker_max_version}].")
|
|
101
101
|
return min(max_version, broker_max_version)
|
|
102
102
|
|
|
103
|
+
@property
|
|
104
|
+
def broker_version_str(self):
|
|
105
|
+
return '.'.join(map(str, self.broker_version))
|
|
106
|
+
|
|
103
107
|
def __str__(self):
|
|
104
|
-
return '<BrokerVersionData %s>' %
|
|
108
|
+
return '<BrokerVersionData %s>' % self.broker_version_str
|
|
105
109
|
|
|
106
110
|
def __eq__(self, other):
|
|
107
111
|
return self.broker_version == other.broker_version and self.api_versions == other.api_versions
|
kafka/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '3.0.
|
|
1
|
+
__version__ = '3.0.4'
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
kafka/__init__.py,sha256=lp8Gk_P3zzDabNl6PZxJ8B24Xit-D-LvJY8rauL3FnI,1200
|
|
2
2
|
kafka/__main__.py,sha256=HNnJxekZZkNDXQwqnE5AfaM56JNCNYvq135wu4jxvhk,107
|
|
3
|
-
kafka/cluster.py,sha256=
|
|
3
|
+
kafka/cluster.py,sha256=wePKTGh8aO7EYigy5_ob8Vz1FBmLlV4SD7Qmigmytj0,32352
|
|
4
4
|
kafka/codec.py,sha256=xApRCUaj6HsGM6T5q-AeK9kRPkH6Au1wqsXwXhwE2Lo,9843
|
|
5
5
|
kafka/errors.py,sha256=9owgtTIcttdTCD1vAF9wz5jL9G72ljpR7M-vjLKEGvY,33206
|
|
6
6
|
kafka/future.py,sha256=INmMIy2-BPxJ4zlw-V_i8Tj_OG9DMbyYyUEjOQ5pn_o,5440
|
|
7
7
|
kafka/structs.py,sha256=ZOFmVJRbhSXykkw9cnuv-DGYE7g2KjaUGsytSOv4xVw,3146
|
|
8
8
|
kafka/util.py,sha256=hvSdRbh5-IsUbpD_BqjabjEldtNmXufZH68b_xCkNjE,5194
|
|
9
|
-
kafka/version.py,sha256=
|
|
9
|
+
kafka/version.py,sha256=zL37B2sw3qNZN5JX3JbC-5k0KFwjSViL9oBB2x4vwDY,22
|
|
10
10
|
kafka/admin/__init__.py,sha256=nTXg2CQo8Bydxsj80pVBzF2-jq4IhK7_gLnboeNWG60,1575
|
|
11
11
|
kafka/admin/__main__.py,sha256=l5ujkHa3lBFEbKPCl7U0_pi6IIgj1IJfmB219vtcj50,69
|
|
12
12
|
kafka/admin/_acls.py,sha256=QJlHguEM9t46qlABoQqTEDFQErDgG2RKwfV1NOKeKmU,13983
|
|
@@ -29,7 +29,7 @@ kafka/benchmarks/record_batch_compose.py,sha256=_oYa6kqjeKDOCCvi-HB5owz2elGpoCCY
|
|
|
29
29
|
kafka/benchmarks/record_batch_read.py,sha256=edlq0Z42p4f0TIceAUeF-DCff35jtM5c84xnoYhEhhA,2146
|
|
30
30
|
kafka/benchmarks/varint_speed.py,sha256=FfLikmv0y_70uWWPvowp6Fv82rm5xVHScL3oMGgItfA,12260
|
|
31
31
|
kafka/cli/__init__.py,sha256=rC9r6I9d4ypNbFcZPI80FHehudqpCKmlRGe6YIn6uj0,1055
|
|
32
|
-
kafka/cli/common.py,sha256=
|
|
32
|
+
kafka/cli/common.py,sha256=VCn8eDXxrPdc9ODT-Sr0zL3CIANyLJ0efhJtFRqVleU,7248
|
|
33
33
|
kafka/cli/admin/__init__.py,sha256=GTABhBAWy5PJAy4ZyUWwiS6V-TsKdVXH3CSj4It4mjM,3655
|
|
34
34
|
kafka/cli/admin/acls/__init__.py,sha256=FjGMmvaoYs0zW7N_PoUsqt0RDroOhe4DCCcE8QzQqOM,227
|
|
35
35
|
kafka/cli/admin/acls/common.py,sha256=N8vmQWaoZ1mYAr6y43YVWXfz0rnIxcZRYSVyvh0Mf3k,3732
|
|
@@ -41,7 +41,7 @@ kafka/cli/admin/cluster/describe.py,sha256=WEAir7STL7AJNvDRWfhPL_brafYZ6cfRH8i3X
|
|
|
41
41
|
kafka/cli/admin/cluster/describe_quorum.py,sha256=ikCcC57t7tRtXXBxSYFR8hJqcOCtn0ttkn-O45CgyT0,272
|
|
42
42
|
kafka/cli/admin/cluster/features.py,sha256=pLnvWsJSYVQdY5RVCdxicP8bLWjH0v1i_3V2h8v20Zk,1943
|
|
43
43
|
kafka/cli/admin/cluster/log_dirs.py,sha256=N4_ULfQa1Jhl96GolE5b-4UZ6wy5sGnRoTCaAo4Gsdo,1692
|
|
44
|
-
kafka/cli/admin/cluster/versions.py,sha256=
|
|
44
|
+
kafka/cli/admin/cluster/versions.py,sha256=MILUv9ycelvvRHo-HIi8dPnNceXRGyR5o9noX4jvpcA,1062
|
|
45
45
|
kafka/cli/admin/configs/__init__.py,sha256=JrD3hOX2s7KUkw2LzozFrsM6NwP5EH33-tSXmoUM5-c,313
|
|
46
46
|
kafka/cli/admin/configs/alter.py,sha256=nd9iAwPJPvcOgM8_gyPKiR9J-8M0c607KnPHY2ymmJA,1921
|
|
47
47
|
kafka/cli/admin/configs/common.py,sha256=NXSQ0Of7B2tWRGTmmPdDUGFobBzQwgadu3-p_F_NRQc,712
|
|
@@ -83,11 +83,11 @@ kafka/cli/consumer/__init__.py,sha256=eNb-AFzLj9QJrEeZZsp427uZ-BDGSIG9Bp3bbOYgvp
|
|
|
83
83
|
kafka/cli/producer/__init__.py,sha256=W3Rs0Darw2f3h1IZZawep1K-LVg2AA0bEuwEEaqPjKg,1682
|
|
84
84
|
kafka/consumer/__init__.py,sha256=I7nuPUb103S1CvCqcudbIRG_isJbRgTXkK1mpHIhoVs,82
|
|
85
85
|
kafka/consumer/__main__.py,sha256=bnxM30LbGVvLb0ZBcgJEt_8bZHADJpboHZCxzd8QEhA,72
|
|
86
|
-
kafka/consumer/fetcher.py,sha256=
|
|
86
|
+
kafka/consumer/fetcher.py,sha256=Xr-JNlyNZgVNanh6AlYn1ImsyuBbpMqjPGwMZL50Hmc,106075
|
|
87
87
|
kafka/consumer/group.py,sha256=lJo2U_wHSk_j-qwfa2GeU_gOZq5kWbxh2qAkjyMer6Q,68423
|
|
88
88
|
kafka/consumer/subscription_state.py,sha256=H1bDpDdy3uk3U8TtyjCuo_HmaZmSpOYevaYtrc7LvgY,38567
|
|
89
89
|
kafka/coordinator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
kafka/coordinator/base.py,sha256=
|
|
90
|
+
kafka/coordinator/base.py,sha256=UgMXWES0VVgDsvLEEZB8tAvchGFRzJFO6N9bL06vGm4,57794
|
|
91
91
|
kafka/coordinator/consumer.py,sha256=cQ-zo0p_lL5H3j8DT4Q6ZrBBxCWWh4p1ub1X2IHlx5w,60186
|
|
92
92
|
kafka/coordinator/heartbeat.py,sha256=JUlq_-TqOd6kwiFcapa20sgaM8lSZ2JgObEzeTTcDm0,3229
|
|
93
93
|
kafka/coordinator/subscription.py,sha256=rq-syb2zwjRiY1797Yi4dxTZH-QFJSsaNAAJVwBHrqA,873
|
|
@@ -128,15 +128,15 @@ kafka/metrics/stats/sensor.py,sha256=OiW7qWkIWxJykubKGfklLl1MEpjy668ZJL42Bifwd-k
|
|
|
128
128
|
kafka/metrics/stats/total.py,sha256=YSWSlkkSPygFrqoJS7LEdcQDm14ABCS-6MODo413l7o,407
|
|
129
129
|
kafka/net/__init__.py,sha256=Dd0ZrMv3hyhMHIiAeY2CFEeKGEFyqHmOdAoh7-squOE,751
|
|
130
130
|
kafka/net/compat.py,sha256=xBwWj5WgcfFbiPWy-46VCa25jDC9x5DOTl_ds_VXdyg,6415
|
|
131
|
-
kafka/net/connection.py,sha256=
|
|
131
|
+
kafka/net/connection.py,sha256=gxUHGAESQneccx0JXY0apoxcsyL-XtMmryCJV4r_DOQ,26349
|
|
132
132
|
kafka/net/http_connect.py,sha256=j8R1yLNwhTF-nQVMEgRF88mOF5-v1Sdj7uwkBvvg5PU,5133
|
|
133
133
|
kafka/net/inet.py,sha256=zMo0NTCS7EY81uoNPri5ZHKcCnRVUIcCJiW6BeZWnCs,4730
|
|
134
|
-
kafka/net/manager.py,sha256=
|
|
134
|
+
kafka/net/manager.py,sha256=R5dlShO8gPzJstHIh-r-ICAMdH2gxlhmO9eEfyL9_zs,20744
|
|
135
135
|
kafka/net/metrics.py,sha256=-cdWXQtonCO7-LpovLoEcWM-_3R9sAtyQxWf_QHOAWE,7299
|
|
136
|
-
kafka/net/selector.py,sha256=
|
|
136
|
+
kafka/net/selector.py,sha256=kKKhVA6UdyiTNAnRDlRJNA3rC-C0anUcpYHnQy_yRZQ,29333
|
|
137
137
|
kafka/net/socks5.py,sha256=euTwJl2avRmCJhVKyGmhZWInqgY6MNHPA1Tz2f2J1Z0,10361
|
|
138
|
-
kafka/net/transport.py,sha256=
|
|
139
|
-
kafka/net/wakeup_notifier.py,sha256=
|
|
138
|
+
kafka/net/transport.py,sha256=ZQOTR8cqtjAmvsaQZndlyOKH_U0lJSBhkLPPknInxgU,15675
|
|
139
|
+
kafka/net/wakeup_notifier.py,sha256=gLnfg-HEjVS5J9p-7kZfZpIeLT0AjvK5MOc6EMj3R88,2783
|
|
140
140
|
kafka/net/sasl/__init__.py,sha256=vKE2jkS_BZ2OEVANlXZhW2xIVvH5C1o2kCxugw2PfYY,1006
|
|
141
141
|
kafka/net/sasl/abc.py,sha256=H3tHUPevoaQdS395_ID88Wxhwvc8XjOTcAP8dKXxO9I,547
|
|
142
142
|
kafka/net/sasl/gssapi.py,sha256=VIzRgrDw0sQUp8bUo2kEM5L6ngeNBUCpCkI6dolUsCY,4676
|
|
@@ -162,7 +162,7 @@ kafka/protocol/api_data.py,sha256=fyiEuEftBfJl23SL3EkF-Sd0lps9ZmTlDT9PY6YV_PY,47
|
|
|
162
162
|
kafka/protocol/api_header.py,sha256=UZxHNQon5K2UnyUIj_XeGE1WqV_S8Qg3L0VK4wdvxcI,2356
|
|
163
163
|
kafka/protocol/api_key.py,sha256=c87C4FtfUujUvcukS9iRieBSAy-a7zOCGTXYlvvRFc8,2456
|
|
164
164
|
kafka/protocol/api_message.py,sha256=832m3NeRZarniJFkArtpB5zdnhNVVcZuW1MympXQ3bQ,11096
|
|
165
|
-
kafka/protocol/broker_version_data.py,sha256=
|
|
165
|
+
kafka/protocol/broker_version_data.py,sha256=tzBE3GPTu8pW4bJcjYi5dMIwWGN0GbqRW0AoIUm566c,27228
|
|
166
166
|
kafka/protocol/data_container.py,sha256=mp2YQPQy8QkahQ_gf3JXjzLklz8T4hfyu6BV5oMGnsU,7783
|
|
167
167
|
kafka/protocol/frame.py,sha256=exKvWkszmCgkDpjlUQv-Jyz4_mCQwnQmgFx4QtJ4ZcI,718
|
|
168
168
|
kafka/protocol/generate_stubs.py,sha256=bZIQUPCiyN-eckI7h72oGbscdjwmCc3sJ1hWfG7bvtk,17176
|
|
@@ -365,9 +365,9 @@ kafka/serializer/default.py,sha256=ZKzTWlG9N4vS3QXovFLygNVrnjAcMZKS0RE4OvQ6DL4,4
|
|
|
365
365
|
kafka/serializer/json.py,sha256=lErgU66KZGf33hofwlObYIaxLDk9gcolhy_rlcOuf8Y,469
|
|
366
366
|
kafka/serializer/wrapper.py,sha256=RGXFj-PXQyL3rdMXI9ACaohRa6Kg5SHOZTRruU9jluM,485
|
|
367
367
|
kafka/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
368
|
-
kafka_python-3.0.
|
|
369
|
-
kafka_python-3.0.
|
|
370
|
-
kafka_python-3.0.
|
|
371
|
-
kafka_python-3.0.
|
|
372
|
-
kafka_python-3.0.
|
|
373
|
-
kafka_python-3.0.
|
|
368
|
+
kafka_python-3.0.4.dist-info/licenses/LICENSE,sha256=vxnoJsqm6bKl3ZWdI1Q7Ikw_k9cOvO3vcvZNsY_1fP8,11374
|
|
369
|
+
kafka_python-3.0.4.dist-info/METADATA,sha256=7jklPUJ14w6vgdx3Dc3dJ1f3WG-LE7tEjIp8_QC-TKg,11590
|
|
370
|
+
kafka_python-3.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
371
|
+
kafka_python-3.0.4.dist-info/entry_points.txt,sha256=LQvqZj3uM785ainO5HrXmukbjSrK-oPqrESqpvoR-As,51
|
|
372
|
+
kafka_python-3.0.4.dist-info/top_level.txt,sha256=IivJz7l5WHdLNDT6RIiVAlhjQzYRwGqBBmKHZ7WjPeM,6
|
|
373
|
+
kafka_python-3.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|