blob: b718446613e5c51a0fc20e9c3cbd0340bca469d5 [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#pragma once
#include <pulsar/defines.h>
#include <pulsar/st/Consumer.h>
#include <pulsar/st/Expected.h>
#include <pulsar/st/Future.h>
#include <pulsar/st/Message.h>
#include <pulsar/st/MessageId.h>
#include <pulsar/st/Schema.h>
#include <pulsar/st/Transaction.h>
#include <pulsar/st/detail/ClientCore.h>
#include <pulsar/st/detail/QueueConsumerCore.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace pulsar::st {
/**
* Plain-old-data configuration accumulated by `QueueConsumerBuilder<T>`.
*
* Each field mirrors a builder setter. Prefer building through
* `QueueConsumerBuilder<T>` rather than populating this struct directly; the
* builder enforces the invariants (notably that exactly one of topic vs. namespace
* mode is selected and that `subscriptionName` is set).
*
* This POD can also express states the type system does not rule out — including
* its **default value**, which selects single-topic mode with an empty `topic`. A
* configuration with no target (empty `topic` while `useNamespace == false`, or
* empty `namespaceName` while `useNamespace == true`) or no `subscriptionName` is
* invalid: `create()` / `createAsync()` reject it with an `Error` instead of
* connecting. Fields not selected by `useNamespace` are ignored.
*/
struct QueueConsumerConfig {
/** Selects namespace mode over single-topic mode. When `false` (the default),
* `topic` is used; when `true`, `namespaceName` (and `propertyFilters`) apply. */
bool useNamespace = false;
/** Fully-qualified topic name. Used only when `useNamespace == false`. Mutually
* exclusive with `namespaceName`. */
std::string topic; // when !useNamespace
/** Namespace name (`tenant/namespace`). Used only when `useNamespace == true`.
* Subscribes to all scalable topics in the namespace with live membership. */
std::string namespaceName; // when useNamespace
/** Namespace mode only: AND filters matched against topic properties to select
* which topics in the namespace are included. Empty means no filtering (all
* topics). Ignored in single-topic mode. */
Properties propertyFilters; // namespace mode: AND filters over topic properties
/** REQUIRED. Subscription name shared by all consumers of this subscription. */
std::string subscriptionName; // REQUIRED
/** Where the subscription starts when it is first created. Default
* `SubscriptionInitialPosition::Latest` (skip the backlog). Has no effect once
* the subscription already exists. */
SubscriptionInitialPosition initialPosition = SubscriptionInitialPosition::Latest;
/** Optional consumer name (useful for diagnostics and metrics). Default unset, in
* which case the broker assigns one. */
std::optional<std::string> consumerName;
/** Acknowledgment tuning (e.g. the ack-grouping/batching window and negative-ack
* redelivery delay). Default-constructed `AckPolicy` when unset. */
AckPolicy ackPolicy;
/** Optional dead-letter policy: route messages to a dead-letter topic after
* repeated redelivery. Default unset (no dead-lettering). */
std::optional<DeadLetterPolicy> deadLetterPolicy;
/** Arbitrary client-side consumer properties (reported in topic stats). Default empty. */
Properties properties;
/** Schema descriptor for the value type `T`. Populated automatically by the builder
* from the `Schema<T>` it was constructed with. */
SchemaInfo schema;
};
template <typename T>
class QueueConsumerBuilder;
/**
* Parallel, broker-managed consumer with **individual ack + nack + dead-letter**.
*
* This is the analog of a classic Shared subscription, attached to all segments of
* a scalable topic (spec §7.2): work is distributed across all consumers on the
* subscription with no ordering or key affinity. Each message is acknowledged
* individually with `acknowledge`, can be negatively acknowledged with
* `negativeAcknowledge` to schedule redelivery, and can be routed to a dead-letter
* topic after repeated redelivery (see `QueueConsumerBuilder::deadLetterPolicy`).
* For ordered, cumulative-ack consumption use `StreamConsumer<T>` instead.
*
* Obtain an instance from `QueueConsumerBuilder<T>`. The default-constructed
* consumer is an empty handle (`operator bool` is `false`) until assigned one.
*
* @tparam T the decoded message value type, determined by the `Schema<T>` used to
* build this consumer.
*/
template <typename T>
class QueueConsumer {
public:
/** Construct an empty, non-live handle. `operator bool` returns `false` until a
* subscribed consumer is move-assigned into it. */
QueueConsumer() = default;
/** Copyable, movable handle; copies share the underlying consumer. */
QueueConsumer(const QueueConsumer&) = default;
QueueConsumer& operator=(const QueueConsumer&) = default;
QueueConsumer(QueueConsumer&&) = default;
QueueConsumer& operator=(QueueConsumer&&) = default;
/**
* Block until the next message arrives and return it.
*
* Returns `Expected` because a receive can fail *without* yielding a message —
* the consumer was closed or the connection dropped. On such failures the result
* holds an `Error` instead of a message; call `.value()` on the result if you
* would rather throw a `ClientException`. (A message whose payload cannot be
* decoded is handled by the SDK and never delivered, so it is not a receive
* failure.)
*
* @return the next `Message<T>`, or an `Error` describing why no message could
* be delivered.
*/
Expected<Message<T>> receive() { return toTyped(core_.receiveAsync().get()); }
/**
* Block for the next message, but for no longer than `timeout`.
*
* @param timeout maximum time to wait for a message.
* @return the next `Message<T>`; if no message arrives within `timeout`, an
* `Error{ResultTimeout}`; or another `Error` on close/disconnect
* failure.
*/
Expected<Message<T>> receive(std::chrono::milliseconds timeout) {
return toTyped(core_.receiveAsync(timeout).get());
}
/**
* Request the next message without blocking.
*
* @return a `Future<Message<T>>` completed with the message when one is
* available, or completed with an `Error` (via the future's `Expected`
* result) on close/disconnect failure.
*/
Future<Message<T>> receiveAsync() {
Schema<T> schema = schema_;
return core_.receiveAsync().thenApply(
[schema](const detail::MessageCore& core) { return Message<T>(core, schema); });
}
/**
* Individually acknowledge one message.
*
* Fire-and-forget: it does not block and does not report an error. Acks are
* buffered and delivered best-effort; a lost ack simply causes redelivery.
*
* @param id the id of the message to acknowledge.
*/
void acknowledge(const MessageId& id) { core_.acknowledge(id); }
/**
* Transactional individual acknowledge: enlist the ack of `id` in `txn`.
*
* The acknowledgment becomes effective only when `txn` commits; its outcome (and
* any error) surfaces at `Transaction::commit()`, not here.
*
* @param id the id of the message to acknowledge.
* @param txn the transaction the acknowledgment is enlisted in.
*/
void acknowledge(const MessageId& id, const Transaction& txn) { core_.acknowledge(id, txn); }
/**
* Negatively acknowledge a message, scheduling it for redelivery.
*
* Fire-and-forget `void`: it does not block and does not report an error. The
* redelivery delay is governed by the configured `AckPolicy`. After enough
* redeliveries the message may be sent to the dead-letter topic if a
* `DeadLetterPolicy` was configured.
*
* @param id the id of the message to redeliver.
*/
void negativeAcknowledge(const MessageId& id) { core_.negativeAcknowledge(id); }
/**
* Close the consumer, releasing its broker-side resources. Blocking.
*
* @return an empty `Expected<void>` on success, or an `Error` if the close
* failed. Call `.value()` to throw instead.
*/
Expected<void> close() { return core_.closeAsync().get(); }
/**
* Close the consumer without blocking.
*
* @return a `Future<void>` completed when the close finishes (or with an `Error`
* on failure).
*/
Future<void> closeAsync() { return core_.closeAsync(); }
/** @return a view of the topic this consumer is subscribed to (in namespace mode, the
* namespace-derived subscription target), valid while the consumer is alive. */
std::string_view topic() const { return core_.topic(); }
/** @return a view of the subscription name, valid while the consumer is alive. */
std::string_view subscription() const { return core_.subscription(); }
/** @return a view of the consumer name (broker-assigned if none was set on the builder),
* valid while the consumer is alive. */
std::string_view consumerName() const { return core_.consumerName(); }
/** @return `true` if this is a live, subscribed consumer; `false` if it is an empty
* (default-constructed or closed/moved-from) handle. */
explicit operator bool() const { return static_cast<bool>(core_); }
private:
template <typename U>
friend class QueueConsumerBuilder;
QueueConsumer(detail::QueueConsumerCore core, Schema<T> schema)
: core_(std::move(core)), schema_(std::move(schema)) {}
Expected<Message<T>> toTyped(Expected<detail::MessageCore> r) const {
if (r) return Message<T>(*r, schema_);
return Expected<Message<T>>(r.error());
}
detail::QueueConsumerCore core_;
Schema<T> schema_;
};
/**
* Fluent builder for a `QueueConsumer<T>`.
*
* Obtain one from `PulsarClient`. Set **exactly one** of `topic()` or
* `inNamespace()` to choose the subscription target, set the REQUIRED
* `subscriptionName()`, then call `subscribe()` / `subscribeAsync()` to create the
* consumer. All setters return `*this` for chaining.
*
* @tparam T the decoded message value type, fixed by the `Schema<T>` the builder
* was created with.
*/
template <typename T>
class QueueConsumerBuilder {
public:
/**
* Subscribe to a single scalable topic. Mutually exclusive with `inNamespace()`;
* set exactly one. Calling this clears any namespace selection.
*
* @param t the fully-qualified topic name.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& topic(std::string t) {
config_.useNamespace = false;
config_.topic = std::move(t);
return *this;
}
/**
* Subscribe to all scalable topics in a namespace with live membership — topics
* created or removed later are joined/dropped automatically (spec §7.2).
* Mutually exclusive with `topic()`; set exactly one.
*
* Named `inNamespace` because `namespace` is a C++ keyword.
*
* @param ns the namespace (`tenant/namespace`) to subscribe across.
* @param propertyFilters optional AND filters matched against topic properties;
* only topics matching all entries are included. Default empty (no
* filtering — every topic in the namespace).
* @return `*this` for chaining.
*/
QueueConsumerBuilder& inNamespace(std::string ns, Properties propertyFilters = {}) {
config_.useNamespace = true;
config_.namespaceName = std::move(ns);
config_.propertyFilters = std::move(propertyFilters);
return *this;
}
/**
* REQUIRED. Set the subscription name shared by all consumers of this
* subscription.
*
* @param s the subscription name.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& subscriptionName(std::string s) {
config_.subscriptionName = std::move(s);
return *this;
}
/**
* Set where the subscription starts when first created.
*
* @param p the initial position. Default `SubscriptionInitialPosition::Latest`.
* Has no effect if the subscription already exists.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& subscriptionInitialPosition(SubscriptionInitialPosition p) {
config_.initialPosition = p;
return *this;
}
/**
* Set an explicit consumer name (useful for diagnostics and metrics).
*
* @param n the consumer name. Default unset, in which case the broker assigns one.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& consumerName(std::string n) {
config_.consumerName = std::move(n);
return *this;
}
/**
* Tune acknowledgment behavior (e.g. the ack-grouping/batching window and the
* negative-ack redelivery delay).
*
* @param policy the ack policy. Default-constructed `AckPolicy` when unset.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& ackPolicy(AckPolicy policy) {
config_.ackPolicy = std::move(policy);
return *this;
}
/**
* Route messages to a dead-letter topic after repeated redelivery (spec §7.2).
* QueueConsumer only.
*
* @param policy the dead-letter policy (max redeliveries, DLQ topic name, etc.).
* Default unset (no dead-lettering).
* @return `*this` for chaining.
*/
QueueConsumerBuilder& deadLetterPolicy(DeadLetterPolicy policy) {
config_.deadLetterPolicy = std::move(policy);
return *this;
}
/**
* Add a single client-side consumer property (reported in topic stats). Call
* repeatedly to add multiple; a repeated key overwrites the previous value.
*
* @param k property key.
* @param v property value.
* @return `*this` for chaining.
*/
QueueConsumerBuilder& property(std::string k, std::string v) {
config_.properties.insert_or_assign(std::move(k), std::move(v));
return *this;
}
/**
* Create the consumer and subscribe. Blocking.
*
* @return the live `QueueConsumer<T>` on success, or an `Error` if the
* subscription failed (e.g. missing `subscriptionName`, both/neither of
* topic and namespace set, or a broker error). Call `.value()` to throw
* instead.
*/
Expected<QueueConsumer<T>> subscribe() { return subscribeAsync().get(); }
/**
* Create the consumer and subscribe without blocking.
*
* @return a `Future<QueueConsumer<T>>` completed with the live consumer, or with
* an `Error` on failure.
*/
Future<QueueConsumer<T>> subscribeAsync() {
Schema<T> schema = schema_;
QueueConsumerConfig config = config_;
config.schema = schema.info();
return client_.subscribeQueueAsync(std::move(config))
.thenApply(
[schema](const detail::QueueConsumerCore& core) { return QueueConsumer<T>(core, schema); });
}
private:
friend class PulsarClient;
QueueConsumerBuilder(detail::ClientCore client, Schema<T> schema)
: client_(std::move(client)), schema_(std::move(schema)) {}
detail::ClientCore client_;
Schema<T> schema_;
QueueConsumerConfig config_;
};
} // namespace pulsar::st