Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-kafka-python for 
openSUSE:Factory checked in at 2026-07-07 21:03:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-kafka-python (Old)
 and      /work/SRC/openSUSE:Factory/.python-kafka-python.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-kafka-python"

Tue Jul  7 21:03:21 2026 rev:15 rq:1364124 version:3.0.7

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-kafka-python/python-kafka-python.changes  
2026-06-28 21:11:55.673001471 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-kafka-python.new.1982/python-kafka-python.changes
        2026-07-07 21:05:51.103758828 +0200
@@ -1,0 +2,7 @@
+Mon Jul  6 20:40:25 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.0.7:
+  * transport: cancel pending io coroutines on close()
+  * producer: fix stalled shutdown when brokers unreachable
+
+-------------------------------------------------------------------

Old:
----
  python-kafka-python-3.0.6.tar.gz

New:
----
  python-kafka-python-3.0.7.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-kafka-python.spec ++++++
--- /var/tmp/diff_new_pack.OMEEMG/_old  2026-07-07 21:05:52.071792713 +0200
+++ /var/tmp/diff_new_pack.OMEEMG/_new  2026-07-07 21:05:52.075792854 +0200
@@ -23,7 +23,7 @@
 %endif
 
 Name:           python-kafka-python
-Version:        3.0.6
+Version:        3.0.7
 Release:        0
 Summary:        Pure Python client for Apache Kafka
 License:        Apache-2.0

++++++ python-kafka-python-3.0.6.tar.gz -> python-kafka-python-3.0.7.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/CHANGES.md 
new/kafka-python-3.0.7/CHANGES.md
--- old/kafka-python-3.0.6/CHANGES.md   2026-06-25 21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/CHANGES.md   2026-06-29 00:11:16.000000000 +0200
@@ -1,9 +1,14 @@
+# 3.0.7 (Jun 28, 2026)
+
+### Fixes
+* transport: cancel pending io coroutines on close() (#3109)
+* producer: fix stalled shutdown when brokers unreachable (#3108)
+
 # 3.0.6 (Jun 25, 2026)
 
 ### Fixes
 * consumer: track current leader epoch in addition to record epoch (#3107)
 
-
 ### CI
 * Also `wait_for_topics` on <0.10 test fixtures (#3105)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/docs/changelog.rst 
new/kafka-python-3.0.7/docs/changelog.rst
--- old/kafka-python-3.0.6/docs/changelog.rst   2026-06-25 21:02:57.000000000 
+0200
+++ new/kafka-python-3.0.7/docs/changelog.rst   2026-06-29 00:11:16.000000000 
+0200
@@ -1,6 +1,15 @@
 Changelog
 =========
 
+3.0.7 (Jun 28, 2026)
+####################
+
+Fixes
+-----
+* transport: cancel pending io coroutines on close() (#3109)
+* producer: fix stalled shutdown when brokers unreachable (#3108)
+
+
 3.0.6 (Jun 25, 2026)
 ####################
 
@@ -8,7 +17,6 @@
 -----
 * consumer: track current leader epoch in addition to record epoch (#3107)
 
-
 CI
 --
 * Also `wait_for_topics` on <0.10 test fixtures (#3105)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/kafka/net/transport.py 
new/kafka-python-3.0.7/kafka/net/transport.py
--- old/kafka-python-3.0.6/kafka/net/transport.py       2026-06-25 
21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/kafka/net/transport.py       2026-06-29 
00:11:16.000000000 +0200
@@ -19,6 +19,8 @@
         self._closed = False
         self._write_buffer = deque()
         self._writing = False
+        self._read_task = None
+        self._write_task = None
         self._protocol = None
         self._read = False
         self._write = True
@@ -80,7 +82,7 @@
         data_received() method.
         """
         if not self._read:
-            self._net.call_soon(self._read_from_sock)
+            self._read_task = self._net.call_soon(self._read_from_sock)
         self._read = True
         log.debug('%s: Resumed reading', self)
 
@@ -173,7 +175,7 @@
         self._write_buffer.append(data)
         if not self._writing:
             self._writing = True
-            self._net.call_soon(self._write_to_sock)
+            self._write_task =  self._net.call_soon(self._write_to_sock)
 
     def writelines(self, list_of_data):
         """Write a list (or any iterable) of data bytes to the transport."""
@@ -182,7 +184,7 @@
         self._write_buffer.extend(list_of_data)
         if not self._writing:
             self._writing = True
-            self._net.call_soon(self._write_to_sock)
+            self._write_task = self._net.call_soon(self._write_to_sock)
 
     async def _write_to_sock(self):
         try:
@@ -272,6 +274,10 @@
             except OSError:
                 pass
             sock.close()
+        for task in (self._read_task, self._write_task):
+            if task is not None:
+                self._net.cancel(task)
+        self._read_task = self._write_task = None
         proto = self._protocol
         self._protocol = None
         if proto is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/kafka/producer/kafka.py 
new/kafka-python-3.0.7/kafka/producer/kafka.py
--- old/kafka-python-3.0.6/kafka/producer/kafka.py      2026-06-25 
21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/kafka/producer/kafka.py      2026-06-29 
00:11:16.000000000 +0200
@@ -646,7 +646,7 @@
         _self = weakref.proxy(self)
         def wrapper():
             try:
-                _self.close(timeout=0, null_logger=True)
+                _self.close(timeout=1, null_logger=True)
             except (ReferenceError, AttributeError):
                 pass
         return wrapper
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/kafka/producer/sender.py 
new/kafka-python-3.0.7/kafka/producer/sender.py
--- old/kafka-python-3.0.6/kafka/producer/sender.py     2026-06-25 
21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/kafka/producer/sender.py     2026-06-29 
00:11:16.000000000 +0200
@@ -310,7 +310,7 @@
             return False
 
         log.debug("%s: Sending transactional request %s", str(self), 
next_request_handler.request)
-        while not self._force_close:
+        while self._running and not self._force_close:
             target_node = None
             try:
                 if next_request_handler.needs_coordinator():
@@ -325,7 +325,8 @@
                 else:
                     target_node = self._client.least_loaded_node()
                     if target_node is None:
-                        
self._client.poll(future=self._metadata.request_update())
+                        
self._client.poll(timeout_ms=self.config['retry_backoff_ms'],
+                                          
future=self._metadata.request_update())
                     elif not self._client.await_ready(target_node, 
timeout_ms=self.config['request_timeout_ms']):
                         continue
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/kafka/version.py 
new/kafka-python-3.0.7/kafka/version.py
--- old/kafka-python-3.0.6/kafka/version.py     2026-06-25 21:02:57.000000000 
+0200
+++ new/kafka-python-3.0.7/kafka/version.py     2026-06-29 00:11:16.000000000 
+0200
@@ -1 +1 @@
-__version__ = '3.0.6'
+__version__ = '3.0.7'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.6/test/net/test_transport.py 
new/kafka-python-3.0.7/test/net/test_transport.py
--- old/kafka-python-3.0.6/test/net/test_transport.py   2026-06-25 
21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/test/net/test_transport.py   2026-06-29 
00:11:16.000000000 +0200
@@ -6,7 +6,7 @@
 
 import kafka.errors as Errors
 from kafka.future import Future
-from kafka.net.selector import NetworkSelector
+from kafka.net.selector import NetworkSelector, TaskState
 from kafka.net.transport import KafkaTCPTransport
 
 
@@ -365,3 +365,58 @@
         t._closed = True
         s = str(t)
         assert 'closed' in s
+
+
+class TestTransportWaiterCleanup:
+    """Regression: a locally-initiated close()/abort() must reclaim the socket
+    read/write coroutine tasks parked in the event loop.
+
+    These tests fail until ``KafkaTCPTransport._close`` cancels its read/write
+    waiter tasks (``net.cancel(task)``); the selector's existing WAIT_IO branch
+    in ``cancel()`` then drives the io_guard finalizer and discards the task.
+    """
+
+    def test_local_close_reclaims_parked_reader(self, net, socketpair):
+        rsock, wsock = socketpair
+        t = KafkaTCPTransport(net, wsock)
+        t.set_protocol(MagicMock())
+        baseline = len(net._pending_tasks)
+
+        t.resume_reading()
+        net.drain()  # reader runs and parks in WAIT_IO on wait_read(wsock)
+        parked = [task for task in net._pending_tasks if task.state is 
TaskState.WAIT_IO]
+        assert len(parked) == 1, 'reader did not park as expected'
+        assert len(net._pending_tasks) == baseline + 1
+
+        # Empty write buffer -> close() tears the socket down synchronously. 
The
+        # peer (rsock) never sent anything, so there is no I/O event to wake 
the
+        # parked reader; close() itself must reclaim it.
+        t.close()
+        net.drain()  # running the loop must not be needed -- and must not 
help either
+
+        assert t._sock is None
+        assert len(net._pending_tasks) == baseline, (
+            'parked reader leaked into _pending_tasks after local close')
+        assert not any(task.state is TaskState.WAIT_IO for task in 
net._pending_tasks)
+
+    def test_local_close_reclaims_parked_writer(self, net, socketpair):
+        rsock, wsock = socketpair
+        t = KafkaTCPTransport(net, wsock)
+        t.set_protocol(MagicMock())
+        baseline = len(net._pending_tasks)
+
+        # write() schedules _write_to_sock, whose loop parks on the first
+        # wait_write before the bytes leave the buffer. drain() steps it once
+        # to the park and returns (no _ready left), leaving it suspended.
+        t.write(b'data')
+        net.drain()
+        parked = [task for task in net._pending_tasks if task.state is 
TaskState.WAIT_IO]
+        assert len(parked) == 1, 'writer did not park as expected'
+
+        t.abort(error=Errors.KafkaConnectionError('boom'))
+        net.drain()
+
+        assert t._sock is None
+        assert len(net._pending_tasks) == baseline, (
+            'parked writer leaked into _pending_tasks after abort')
+        assert not any(task.state is TaskState.WAIT_IO for task in 
net._pending_tasks)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kafka-python-3.0.6/test/producer/test_kip679_config.py 
new/kafka-python-3.0.7/test/producer/test_kip679_config.py
--- old/kafka-python-3.0.6/test/producer/test_kip679_config.py  2026-06-25 
21:02:57.000000000 +0200
+++ new/kafka-python-3.0.7/test/producer/test_kip679_config.py  2026-06-29 
00:11:16.000000000 +0200
@@ -12,66 +12,52 @@
 # same process. Observable behavior on the producer instance 
(enable_idempotence
 # flag, _transaction_manager presence, config['acks']) is what callers actually
 # depend on, and is what these tests verify.
+#
+# These producers point at the default localhost:9092 with no broker running, 
so
+# the `with KafkaProducer(...) as p` form relies on close() tearing down the
+# Sender thread even though it never connects (see 
Sender._maybe_send_pending_request).
 
 
 class TestDefaultsAreIdempotent:
     """KIP-679: defaults instantiate an idempotent producer with acks=all."""
 
     def test_bare_construct_is_idempotent(self):
-        p = KafkaProducer(api_version=(0, 11))
-        try:
+        with KafkaProducer(api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is True
             assert p.config['acks'] == -1
             assert p._transaction_manager is not None
             assert p._transaction_manager.is_transactional() is False
-        finally:
-            p.close(timeout=0)
 
     def test_opt_out_disables_idempotence_but_keeps_acks_default(self):
-        p = KafkaProducer(enable_idempotence=False, api_version=(0, 11))
-        try:
+        with KafkaProducer(enable_idempotence=False, api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is False
             assert p.config['acks'] == -1
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
 
 class TestDefaultIdempotenceSilentDisable:
     """User-provided incompatible config + default idempotence -> silent 
disable."""
 
     def test_acks_1_silently_disables(self):
-        p = KafkaProducer(acks=1, api_version=(0, 11))
-        try:
+        with KafkaProducer(acks=1, api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is False
             assert p.config['acks'] == 1
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
     def test_acks_0_silently_disables(self):
-        p = KafkaProducer(acks=0, api_version=(0, 11))
-        try:
+        with KafkaProducer(acks=0, api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is False
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
     def test_retries_zero_silently_disables(self):
-        p = KafkaProducer(retries=0, api_version=(0, 11))
-        try:
+        with KafkaProducer(retries=0, api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is False
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
     def test_max_in_flight_too_high_silently_disables(self):
-        p = KafkaProducer(max_in_flight_requests_per_connection=10, 
api_version=(0, 11))
-        try:
+        with KafkaProducer(max_in_flight_requests_per_connection=10, 
api_version=(0, 11)) as p:
             assert p.config['enable_idempotence'] is False
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
 
 class TestOldBrokerFallback:
@@ -79,14 +65,11 @@
     acks=-1 retained (acks=all is valid on any wire version)."""
 
     def test_old_broker_silently_disables(self):
-        p = KafkaProducer(api_version=(0, 10))
-        try:
+        with KafkaProducer(api_version=(0, 10)) as p:
             assert p.config['enable_idempotence'] is False
             # Per KIP-679 + project decision: acks stays at -1 even on old 
brokers.
             assert p.config['acks'] == -1
             assert p._transaction_manager is None
-        finally:
-            p.close(timeout=0)
 
 
 class TestExplicitIdempotenceConflicts:
@@ -139,16 +122,13 @@
 
     def test_acks_all_string_accepted(self):
         """'all' normalizes to -1 before the conflict check fires."""
-        p = KafkaProducer(
+        with KafkaProducer(
             enable_idempotence=True,
             acks='all',
             api_version=(0, 11),
-        )
-        try:
+        ) as p:
             assert p.config['acks'] == -1
             assert p._transaction_manager is not None
-        finally:
-            p.close(timeout=0)
 
     def test_old_broker_with_explicit_idempotence_raises(self):
         with pytest.raises(Errors.KafkaConfigurationError, 
match="api_version"):

Reply via email to