Ilya Brin
07/21/2025, 7:02 PMRunLLM
07/21/2025, 7:02 PMIlya Brin
07/21/2025, 7:04 PMRunLLM
07/21/2025, 7:04 PMimport org.apache.pulsar.client.api.*;
import java.util.concurrent.*;
public class ProducerCache implements AutoCloseable {
private final PulsarClient client;
private final ConcurrentHashMap<String, Producer<byte[]>> cache = new ConcurrentHashMap<>();
public ProducerCache(PulsarClient client) {
this.client = client;
}
public Producer<byte[]> getOrCreateProducer(String topic) throws PulsarClientException {
return cache.computeIfAbsent(topic, t -> {
try {
return client.newProducer().topic(t).create();
} catch (PulsarClientException e) {
throw new RuntimeException(e);
}
});
}
@Override
public void close() {
cache.values().forEach(producer -> {
try {
producer.close();
} catch (Exception ignored) {}
});
cache.clear();
}
}
Usage:
ProducerCache producerCache = new ProducerCache(pulsarClient);
Producer<byte[]> producer = producerCache.getOrCreateProducer("my-topic");
producer.send("message".getBytes());
producerCache.close();
This pattern is similar to what is used internally in Pulsar Functions and the Pulsar Reactive Client. For more advanced caching (with expiration or size limits), consider using Caffeine as shown in the Pulsar codebase.
Would you like an example using Caffeine or Guava for LRU/expiring cache?
Sources:
- ConcurrentHashMapProducerCacheProvider.java
- ProducerCache.java 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/26/2025, 11:56 PMRunLLM
07/26/2025, 11:57 PMIlya Brin
07/27/2025, 12:03 AMRunLLM
07/27/2025, 12:03 AMproducer.isConnected() to verify if the producer is still valid. If not, remove it from the cache, close it, and create a new one. This approach is used in the Pulsar Functions and Reactive Client caches, which also handle async close and flush on removal (ProducerCache.java, ProducerCacheEntry.java).
For unrecoverable errors (e.g., topic deletion, fencing, quota exceeded), the producer will throw exceptions on send or may transition to a closed/terminated state. In such cases, catch the exception, remove the producer from the cache, and attempt to create a new one if needed. This ensures your cache does not hold onto invalid producers indefinitely. Would you like a code example for this pattern?
Sources:
- ProducerCache.java
- ProducerCacheEntry.java 👍 1 button 👎 0 button 💡 Hint: Mention @RunLLM in the thread for followups.Ilya Brin
07/27/2025, 12:06 AMRunLLM
07/27/2025, 12:06 AMimport org.apache.pulsar.client.api.*;
import java.util.concurrent.*;
public class ValidatingProducerCache implements AutoCloseable {
private final PulsarClient client;
private final ConcurrentHashMap<String, Producer<byte[]>> cache = new ConcurrentHashMap<>();
public ValidatingProducerCache(PulsarClient client) {
this.client = client;
}
public Producer<byte[]> getOrCreateProducer(String topic) throws PulsarClientException {
Producer<byte[]> producer = cache.get(topic);
if (producer == null || !producer.isConnected()) {
// Remove and close invalid producer if present
if (producer != null) {
cache.remove(topic, producer);
try { producer.close(); } catch (Exception ignored) {}
}
// Create and cache a new producer
producer = client.newProducer().topic(topic).create();
cache.put(topic, producer);
}
return producer;
}
public void send(String topic, byte[] data) throws PulsarClientException {
try {
getOrCreateProducer(topic).send(data);
} catch (PulsarClientException e) {
// Handle unrecoverable errors by removing the producer from cache
cache.remove(topic);
throw e;
}
}
@Override
public void close() {
cache.values().forEach(producer -> {
try { producer.close(); } catch (Exception ignored) {}
});
cache.clear();
}
}
This pattern ensures that only valid, connected producers are reused, and invalid ones are closed and replaced as needed.
Sources:
- ProducerCache.java
- ProducerCacheEntry.java 👍 1 button 👎 0 button 💡 Hint: Mention @RunLLM in the thread for followups.Ilya Brin
07/28/2025, 3:37 AMproducer.isConnected before using the producer. looking at the code, it seems this method only returns true if the producer's state is Ready or RegisteringSchema. are freshly created producers guaranteed to be one of these two states? it seems the flow of states is Uninitialized -> Connecting -> RegisteringSchema -> Ready. Is it possible for the producer to still be in the Uninitialized or Connecting states, thus failing isConnected?RunLLM
07/28/2025, 3:37 AMproducer.isConnected() only returns true if the producer's state is Ready or RegisteringSchema. A freshly created producer may still be in Uninitialized or Connecting states immediately after creation, especially if you use asynchronous creation or access the producer before the connection is fully established. This means isConnected() could return false until the producer transitions to Ready or RegisteringSchema (ProducerImpl.java).
To ensure the producer is usable, you should wait for the producer to reach the Ready state before sending messages. If you use the synchronous create() method, the producer will be in a usable state after creation. If you use createAsync(), you must wait for the future to complete. Would you like more detail on handling producer state transitions?
Sources:
- ProducerImpl.java 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/28/2025, 3:43 AMcreateAsync(), you must wait for the future to complete."? are you saying that if createAsync completes successfully, the producer will always be in a Ready state? and yes, please include some more detail on handling producer state transitionsRunLLM
07/28/2025, 3:44 AMcreateAsync(), you must wait for the returned CompletableFuture<Producer<T>> to complete successfully before using the producer. When the future completes, the producer is guaranteed to be in a usable state—specifically, either Ready or, in some schema cases, RegisteringSchema (ProducerImpl.java). This means you should not use the producer (e.g., call send) until the future has completed.
The producer state transitions are: Uninitialized → Connecting → RegisteringSchema (if schema is involved) → Ready. The synchronous create() method blocks until the producer is ready, while createAsync() completes the future only after the producer is ready for use. If the future completes exceptionally, the producer is not usable and should not be cached or used. Would you like a code example for safe async producer creation and usage?
Sources:
- ProducerImpl.java 👍 1 button 👎 0 button 💡 Hint: Mention @RunLLM in the thread for followups.Ilya Brin
07/28/2025, 3:45 AMRunLLM
07/28/2025, 3:45 AMCompletableFuture<Producer<T>> returned by createAsync() and only use the producer after the future completes successfully. This ensures the producer is in a usable (Ready or RegisteringSchema) state (ProducerImpl.java).
import org.apache.pulsar.client.api.*;
import java.util.concurrent.CompletableFuture;
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://localhost:6650")
.build();
CompletableFuture<Producer<byte[]>> futureProducer = client.newProducer()
.topic("my-topic")
.createAsync();
futureProducer.thenAccept(producer -> {
// Safe to use the producer here
try {
producer.send("hello".getBytes());
} catch (PulsarClientException e) {
// Handle send error
}
}).exceptionally(ex -> {
// Handle producer creation failure
ex.printStackTrace();
return null;
});
This pattern ensures you never use the producer before it is fully initialized and ready.
Sources:
- ProducerImpl.java 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/28/2025, 4:07 AMPulsarClientException on publish, or to check isConnected before publishing and also to catch exceptions on publish?RunLLM
07/28/2025, 4:07 AMPulsarClientException when publishing, regardless of the result of isConnected(). Checking isConnected() before publishing can help avoid some errors, but it is not sufficient because the connection state can change between the check and the publish operation. The Pulsar Java client and official docs recommend handling exceptions on publish, as network failures, topic unloads, or broker errors can occur at any time (ProducerImpl.java, client-libraries-producers).
So, the best practice is to always catch PulsarClientException on publish, and optionally check isConnected() if you want to avoid obvious failures, but never rely on it alone.
Sources:
- ProducerImpl.java
- client-libraries-producers 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/28/2025, 4:38 AMPulsarClientException terminal/indicate that the client needs to be recycled? on the flip side, will a client in a bad state ever have isConnected = true?RunLLM
07/28/2025, 4:38 AMPulsarClientException errors are terminal or require recycling the client or producer. Many exceptions (e.g., transient network issues, timeouts) are retriable, and the client can recover automatically. Only unrecoverable exceptions—such as TopicDoesNotExistException, ProducerFencedException, or `AlreadyClosedException`—indicate that the producer or client should be closed and recreated (ProducerImpl.java, PulsarClientException.java).
A client or producer in a bad state (e.g., after unrecoverable errors) will generally have isConnected = false. However, there can be brief periods where isConnected returns true even as a failure is about to be detected, due to asynchronous state changes. Therefore, always handle exceptions on publish, and use isConnected as a hint, not a guarantee.
Sources:
- ProducerImpl.java
- PulsarClientException.java 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/28/2025, 4:40 AMPulsarClientException is recoverable or not?RunLLM
07/28/2025, 4:40 AMPulsarClientException is recoverable by using the static method PulsarClientException.isRetriableError(Throwable t). This method returns true for exceptions that are considered retriable (recoverable), and false for unrecoverable errors such as authorization failures, topic not found, or producer fenced (PulsarClientException.java).
Would you like more detail or a code example?
Sources:
- PulsarClientException.java 👍 1 button 👎 0 button 💡 Hint: Mention @RunLLM in the thread for followups.Ilya Brin
07/28/2025, 5:14 AMRunLLM
07/28/2025, 5:14 AMIlya Brin
07/28/2025, 9:22 PMpulsar-functions project) there's no handling that supports invalid producers as far as i can tell. can you clarify if pulsar-functions has any code to handle unrecoverable/unretriable errors on publish?RunLLM
07/28/2025, 9:22 PMProducerCache.java in the pulsar-functions project does not itself handle invalid or unrecoverable producers; it simply caches and closes producers (ProducerCache.java). The cache does not check producer state or remove producers on unrecoverable errors.
Handling of unrecoverable/unretriable errors on publish is typically done at a higher level, such as in the function or sink logic that uses the producer. For example, in PulsarSink.java, errors during publishing are logged and counted, but there is no automatic removal or recreation of producers in the cache when an unrecoverable error occurs (PulsarSink.java). Would you like more detail on how error handling is structured in Pulsar Functions?
Sources:
- ProducerCache.java
- PulsarSink.java 👍 0 button 👎 0 button 💡 Hint: Mention @runllm in the thread for followups.Ilya Brin
07/28/2025, 9:26 PMRunLLM
07/28/2025, 9:26 PM