Skip to content

[Bug] Closing the client while create_producer()/subscribe() is pending returns an uninitialized object; producer_name() then segfaults #318

Open

Description

@ZiggiZagga

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 main at e0cff2c, built from source against pulsar-client-cpp 4.2.0. main is 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() returns False
  • close() raises ProducerNotInitialized
  • producer_name() and last_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

  1. aviralgarg05 commented on Nov 8, 2026

    @aviralgarg05

    Hi, I’d like to work on this. I checked the current exception handling and confirmed that ResultDisconnected is missing from both raiseException() 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 Disconnected exception, or should this map to an existing exception such as NotConnected or AlreadyClosed?

    Is this issue available?

  2. ZiggiZagga commented on Nov 8, 2026

    @ZiggiZagga
    Author

    Thanks! Happy for you to take it. On the design:

    • a new pulsar.Disconnected (subclass of PulsarException) fits the binding's one-exception-per-Result convention; NotConnected / AlreadyClosed are different native results with their own meaning.
    • worth also changing raiseException()'s default: branch to raise PulsarException for any unmapped result instead of returning a default-constructed object, so the next missing mapping fails loudly.
    • a regression test: close the client while create_producer() is pending (no broker needed: point the service URL at a closed port, e.g. pulsar://127.0.0.1:1, and call client.close() from a timer after ~1 s).
  3. aviralgarg05 commented on Nov 8, 2026

    @aviralgarg05

    Thanks, that matches what I found as well. I’ve prepared the focused change locally: a dedicated pulsar.Disconnected, the corresponding Result mapping, a fail-loud fallback for unmapped results, and a no-broker regression test covering close() during a pending create_producer() call. I’ll wait for maintainer confirmation before opening the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions