apache/pulsar · error · java.lang.IllegalStateException

MessageIdV5 missing parent topic — was the message delivered

Error message

MessageIdV5 missing parent topic — was the message delivered through a multi-topic consumer?

What it means

Thrown by MultiTopicQueueConsumer.routeAck when the MessageIdV5 is valid but its parentTopic() is null. A multi-topic ack needs to know which underlying topic the message came from; a null parent topic means the id was not produced by this multi-topic consumer's delivery path.

Source

Thrown at pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java:335

    @Override
    public void acknowledge(MessageId messageId, Transaction txn) {
        routeAck(messageId, ptc -> ptc.acknowledge(messageId, txn));
    }

    @Override
    public void negativeAcknowledge(MessageId messageId) {
        routeAck(messageId, ptc -> ptc.negativeAcknowledge(messageId));
    }

    /** Look up the per-topic consumer via the parent topic tag and delegate. */
    private void routeAck(MessageId messageId, java.util.function.Consumer<QueueConsumer<T>> action) {
        if (!(messageId instanceof MessageIdV5 id)) {
            throw new IllegalArgumentException("Expected MessageIdV5, got: " + messageId.getClass());
        }
        String parent = id.parentTopic();
        if (parent == null) {
            throw new IllegalStateException("MessageIdV5 missing parent topic — was the message"
                    + " delivered through a multi-topic consumer?");
        }
        PerTopicState<T> state = perTopic.get(parent);
        if (state == null) {
            // Topic was removed between deliver and ack. Fine — broker has dropped the
            // session for that topic. Drop the ack silently.
            log.debug().attr("topic", parent)
                    .log("Ack for removed topic; dropping");
            return;
        }
        action.accept(state.consumer);
    }

    @Override
    public AsyncQueueConsumer<T> async() {
        return asyncView;
    }

View on GitHub (pinned to 820761864e)

Solutions

  1. Ack messages with the same consumer instance that delivered them
  2. Verify the MessageIdV5 was created by multi-topic delivery (parentTopic() != null) before acking
  3. If persisting ids, store the parent topic alongside and reconstruct a valid id

Example fix

// before
otherConsumer.acknowledge(messageId); // id has no parent topic
// after
if (messageId instanceof MessageIdV5 id && id.parentTopic() != null) {
    multiTopicConsumer.acknowledge(id);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (id instanceof MessageIdV5 v5 && v5.parentTopic() == null) { handleOrphan(); }

Type guard

static boolean hasParentTopic(MessageId id) { return id instanceof MessageIdV5 v5 && v5.parentTopic() != null; }

Try / catch

try { consumer.acknowledge(id); } catch (IllegalStateException e) { log.warn("id not from multi-topic delivery", e); }

Prevention

When it happens

Trigger: Calling acknowledge/negativeAcknowledge with a MessageIdV5 that was created outside multi-topic delivery — e.g. from a single-topic MessageIdV5 consumer, manually constructed, or deserialized without topic metadata.

Common situations: Ids shared across consumer instances; ids deserialized from a database/journal where parent-topic metadata was stripped; acking a message received by a different consumer.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/1a3b5d2131798716. Report an issue: GitHub.