Repository navigation
Description
Note: AI was used to help write this text and the patch; I reproduced and verified the results.
Search before asking
- I searched in the issues and found nothing similar.
Version
- pulsar-client 3.13.0 (latest on PyPI), bundling pulsar-client-cpp 4.2.0
- Python 3.12, Linux x86_64
- Also reproduced against
mainat e0cff2c, built from source against pulsar-client-cpp 4.2.0.mainis now bd46d51 (fix: register exception translator as module-local #315, a one-line change to how the exception translator is registered); the missing mapping is unchanged. The fix's tests were re-run on bd46d51 (2026-11-08).
Minimal reproduce step
No broker is needed: the service URL points at a port where nothing listens, so the lookup keeps retrying until close() cancels it.
import threading, pulsar
client = pulsar.Client('pulsar://127.0.0.1:1', operation_timeout_seconds=30)
threading.Timer(1.0, client.close).start()
producer = client.create_producer('my-topic') # expected: an exception
print(repr(producer.topic()), producer.is_connected()) # prints: '' False
producer.producer_name() # Segmentation fault (exit code 139)The C++ log shows the cause:
ERROR ClientImpl:275 | Error Checking/Getting Partition Metadata while creating producer on persistent://public/default/my-topic -- ResultDisconnected
What did you expect to see?
create_producer() raises an exception, the way it raises pulsar.ConnectError or pulsar.Timeout for other failures.
What did you see instead?
create_producer() returns normally. The returned pulsar.Producer wraps a default-constructed native Producer with no implementation behind it:
topic()returns'',is_connected()returnsFalseclose()raisesProducerNotInitializedproducer_name()andlast_sequence_id()dereference a null pointer and crash the interpreter (SIGSEGV)
subscribe(), create_reader() and get_topic_partitions() act the same way: they return an empty Consumer, an empty Reader and [].
Anything else?
Root cause: raiseException() in src/exceptions.cc has no CASE_RESULT for ResultDisconnected, which pulsar-client-cpp added in 3.2.0. The switch's default: branch is return;, so any result code without a case is treated as success, and waitForAsyncValue() returns the default-constructed value. Compared with the Result enum in pulsar-client-cpp 4.2.0, ResultDisconnected is the only public error code missing from the list; the only other result without a case is the internal ResultRetryable = -1.
In pulsar-client-cpp 4.2.0 a pending lookup completes with ResultDisconnected when the client is closed (RetryableOperation::cancel()); a dropped connection during the lookup is retried and ends as ResultTimeout, so in practice the trigger is close() racing a pending call.
The null-pointer crash itself comes from pulsar-client-cpp: Producer::getProducerName(), getLastSequenceId() and getSchemaVersion() call impl_->... with no null check, while getTopic(), close(), sendAsync(), flushAsync() and isConnected() have one, as do Consumer's getters. Fixing the binding stops the public API from handing out such an object; the missing guards could be fixed separately in pulsar-client-cpp.
Are you willing to submit a PR?
- I'm willing to submit a PR!
Activity
aviralgarg05 commentedon Nov 8, 2026
Hi, I’d like to work on this. I checked the current exception handling and confirmed that
ResultDisconnectedis missing from bothraiseException()and the Python exception map.I can add the binding exception and mapping, along with a regression test covering a client being closed while an operation is pending. Would you prefer a new
Disconnectedexception, or should this map to an existing exception such asNotConnectedorAlreadyClosed?Is this issue available?
ZiggiZagga commentedon Nov 8, 2026
Thanks! Happy for you to take it. On the design:
pulsar.Disconnected(subclass ofPulsarException) fits the binding's one-exception-per-Resultconvention;NotConnected/AlreadyClosedare different native results with their own meaning.raiseException()'sdefault:branch to raisePulsarExceptionfor any unmapped result instead of returning a default-constructed object, so the next missing mapping fails loudly.create_producer()is pending (no broker needed: point the service URL at a closed port, e.g.pulsar://127.0.0.1:1, and callclient.close()from a timer after ~1 s).aviralgarg05 commentedon Nov 8, 2026
Thanks, that matches what I found as well. I’ve prepared the focused change locally: a dedicated
pulsar.Disconnected, the correspondingResultmapping, a fail-loud fallback for unmapped results, and a no-broker regression test coveringclose()during a pendingcreate_producer()call. I’ll wait for maintainer confirmation before opening the PR.