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.
@@ -30,4 +30,4 @@ class GetBrokerVersion:
30
30
  def command(cls, client, args):
31
31
  broker_id = int(args.broker)
32
32
  bvd = client.get_broker_version_data(broker_id)
33
- return {broker_id: '.'.join(map(str, bvd.broker_version))}
33
+ return {broker_id: bvd.broker_version_str}
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': config.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
- if config.enable_logger is not None:
78
- log_level = _LOGGING_LEVELS[config.log_level.upper()]
79
- handler = logging.StreamHandler()
80
- handler.setLevel(log_level)
81
- handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
82
- for name in config.enable_logger:
83
- logger = logging.getLogger(name)
84
- logger.setLevel(log_level)
85
- logger.addHandler(handler)
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
- logging.basicConfig(level=_LOGGING_LEVELS[config.log_level.upper()])
88
- if config.disable_logger is not None:
89
- for name in config.disable_logger:
90
- logging.getLogger(name).setLevel(logging.CRITICAL + 1)
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 as exc:
129
- log.debug('Metadata refresh failed: %s', exc)
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). add_both fires synchronously on
236
- # already-done futures, closing the race where a future resolves
237
- # between scheduling and the wait setup.
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
- while True:
285
- if not self._fetch_futures:
286
- break
287
- if not self._fetch_futures[0].is_done:
288
- break
289
- self._fetch_futures.popleft()
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 in flight."""
293
- self._clean_done_fetch_futures()
294
- return bool(self._fetch_futures)
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.warning("Heartbeat failed for group %s because it is"
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.unschedule(timeout_task)
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.unschedule(timeout_task)
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
- log.info('%s: Broker version identified as %s', self, '.'.join(map(str, bvd.broker_version)))
404
- if self.broker_version_data is None or self.broker_version_data > bvd:
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
- else:
407
- log.info('%s: Clamping client to user-supplied broker version %s', self, '.'.join(map(str, self.broker_version)))
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
- try:
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' in configs:
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.debug('Attempting bootstrap with %s', bootstrap_broker)
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.debug('Bootstrap %s NodeNotReadyError: backoff %s', bootstrap_broker, delay)
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
- log.warning('Bootstrap metadata response has no brokers. Retrying.')
149
- self.update_backoff(bootstrap_broker.node_id)
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
- log.error(f'Bootstrap attempt to {bootstrap_broker.node_id} failed: {exc}')
153
- self.update_backoff(bootstrap_broker.node_id)
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
- if not timer.is_done:
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
- state['exception'] = exc
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._ready.append(task)
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 _remove_scheduled(self, task):
382
- if task.scheduled_at is not None:
383
- try:
384
- self._scheduled.remove((task.scheduled_at, task))
385
- except ValueError:
386
- pass
387
- else:
388
- # re-heapify to ensure heap structure is valid
389
- heapq.heapify(self._scheduled)
390
- task.scheduled_at = None
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 _retire_task(self, task):
393
- if task is self._current:
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
- self._remove_scheduled(task)
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
- state = {'fired': False, 'timer': None}
470
+ timer = None # set below iff there is a timeout
433
471
 
434
- def on_resume():
435
- state['fired'] = True
436
- if state['timer'] is not None:
437
- try:
438
- self.unschedule(state['timer'])
439
- except ValueError:
440
- pass
441
- self.unregister_event(fileobj, event)
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
- if state['fired']:
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._ready.append(suspended)
499
+ self._add_ready_task(suspended)
450
500
 
451
- suspended.push_stack(on_resume)
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._ready.append(task)
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
- inject = self._current._exc
562
- if inject is not None:
563
- self._current._exc = None
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
- # Task ran to completion. Drop the strong ref so the Task
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 as e:
574
- log.exception(e)
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._pending_tasks.discard(self._current)
628
+ self._task_done(self._current)
577
629
 
578
630
  else:
579
- if isinstance(event, KernelEvent):
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
- getattr(self, event.method)(*event.args)
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._ready.append(writer)
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._ready.append(reader)
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._write and not self._closed and self._write_buffer:
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 not self._write:
201
- self._sock.shutdown(socket.SHUT_WR)
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
- pass
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:
@@ -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 and not timer.is_done:
57
- try:
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>' % '.'.join(map(str, self.broker_version))
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.2'
1
+ __version__ = '3.0.4'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Pure Python client for Apache Kafka
5
5
  Author-email: Dana Powers <dana.powers@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -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=P1-ylBAa33FAelwufsod_ouIp2FCfE-MI6_sNnxJyJc,32429
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=FDUC8xOuizaRKSkyG1VOmlDwzcPJgbzFUWz58fhdUGQ,22
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=G7Bh68Dw-l0El46IgWJOCXgnrGOI89QcOPcHeEhCsts,3586
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=7s-dc1ZKzMVyO_5Vrgqdp4fokb5spwAmur6ShRs2d9E,1078
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=hOj0ecZxsp83pUf6K0PZY01PCpEZhBjoJymk9bpv4Qc,102423
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=gIzjv60NIC6zlkgUm1w-yG-S5i6fy_pSo3BJEBbOD9M,57841
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=D7D8KDU3TIoY9cLutfmYLXZNGjC7FjS-nZWWJuvrI00,25274
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=nq7eKgzkazQ3sFLXQ-IOIFQ3FdyZYzfKxYxVrwYPGsc,19427
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=4_g3AtYQfmhMjzFgtFOE_TbkIIifFWW5B8zvQmyBJCI,25733
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=hfdoQ9dMOXrMxxLJ6c4A69OGN3_FuRvDL5UuFJ1X-sA,15116
139
- kafka/net/wakeup_notifier.py,sha256=hmxwrrqZ8zFHSHGYaorV7vdOoKo9SF3-VoS7YGkPq_w,2926
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=UhSRsfHWq1eZ5sicWcRqp4TLGrHp4RRDzrpc5IyrZfU,27140
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.2.dist-info/licenses/LICENSE,sha256=vxnoJsqm6bKl3ZWdI1Q7Ikw_k9cOvO3vcvZNsY_1fP8,11374
369
- kafka_python-3.0.2.dist-info/METADATA,sha256=jQB9VNg9SS9r4_i52w7nofQF2k0ZWblwKCsiGc86a8k,11590
370
- kafka_python-3.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
371
- kafka_python-3.0.2.dist-info/entry_points.txt,sha256=LQvqZj3uM785ainO5HrXmukbjSrK-oPqrESqpvoR-As,51
372
- kafka_python-3.0.2.dist-info/top_level.txt,sha256=IivJz7l5WHdLNDT6RIiVAlhjQzYRwGqBBmKHZ7WjPeM,6
373
- kafka_python-3.0.2.dist-info/RECORD,,
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,,