blob: 9874a2b3a27a48bd6059da25de9f329d670608e4 [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/StreamConsumerCore.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 `StreamConsumerBuilder<T>`.
*
* Each field mirrors a builder setter. Prefer building through
* `StreamConsumerBuilder<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 StreamConsumerConfig {
/** 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 key/value properties attached to the subscription itself (persisted
* broker-side). Default empty. */
Properties subscriptionProperties;
/** 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). Default-constructed
* `AckPolicy` when unset. Note: `AckPolicy::negativeAckRedeliveryDelay` is ignored here —
* a StreamConsumer acknowledges cumulatively and has no negative-ack/redelivery path. */
AckPolicy ackPolicy;
/** When set to `true`, read from the topic's compacted view (latest value per key)
* instead of the full log. Default unset (broker default, i.e. uncompacted). */
std::optional<bool> readCompacted;
/** When set to `true`, replicate the subscription's acknowledged position across
* geo-replication clusters. Default unset (broker default, i.e. disabled). */
std::optional<bool> replicateSubscriptionState;
/** 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 StreamConsumerBuilder;
/**
* Ordered (per-key), broker-managed consumer with **cumulative ack only**.
*
* This is the closest analog to a classic Failover subscription, but spanning all
* segments of a scalable topic (spec §7.1): messages are delivered in order
* (per-key) and the broker manages segment assignment for you. A single
* `acknowledgeCumulative` advances every segment up to the delivered message's
* position; there is no individual ack, no negative ack, and no dead-letter
* support. For parallel, unordered consumption with per-message ack use
* `QueueConsumer<T>` instead.
*
* Obtain an instance from `StreamConsumerBuilder<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 StreamConsumer {
public:
/** Construct an empty, non-live handle. `operator bool` returns `false` until a
* subscribed consumer is move-assigned into it. */
StreamConsumer() = default;
/** Copyable, movable handle; copies share the underlying consumer. */
StreamConsumer(const StreamConsumer&) = default;
StreamConsumer& operator=(const StreamConsumer&) = default;
StreamConsumer(StreamConsumer&&) = default;
StreamConsumer& operator=(StreamConsumer&&) = 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); });
}
/**
* Block for a batch of up to `maxMessages`, bounded by `timeout`.
*
* Returns as soon as `maxMessages` have been collected or `timeout` elapses,
* whichever comes first, so the returned batch may contain fewer than
* `maxMessages` (including zero on timeout).
*
* @param maxMessages the maximum number of messages to return in the batch.
* @param timeout maximum time to wait while accumulating the batch.
* @return the collected `Messages<T>`, or an `Error` on close/disconnect
* failure.
*/
Expected<Messages<T>> receiveMulti(int maxMessages, std::chrono::milliseconds timeout) {
return toTypedBatch(core_.receiveMultiAsync(maxMessages, timeout).get());
}
/**
* Cumulatively acknowledge every message up to and including `id`, advancing all
* segments up to that position (spec §7.1).
*
* 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 message position up to which (inclusive) to acknowledge.
*/
void acknowledgeCumulative(const MessageId& id) { core_.acknowledgeCumulative(id); }
/**
* Transactional cumulative acknowledge: enlist the cumulative ack up to `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 message position up to which (inclusive) to acknowledge.
* @param txn the transaction the acknowledgment is enlisted in.
*/
void acknowledgeCumulative(const MessageId& id, const Transaction& txn) {
core_.acknowledgeCumulative(id, txn);
}
/**
* 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 StreamConsumerBuilder;
StreamConsumer(detail::StreamConsumerCore 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());
}
Expected<Messages<T>> toTypedBatch(Expected<std::vector<detail::MessageCore>> r) const {
if (!r) return Expected<Messages<T>>(r.error());
std::vector<Message<T>> out;
out.reserve(r->size());
for (auto& core : *r) out.emplace_back(core, schema_);
return Messages<T>(std::move(out));
}
detail::StreamConsumerCore core_;
Schema<T> schema_;
};
/**
* Fluent builder for a `StreamConsumer<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 StreamConsumerBuilder {
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.
*/
StreamConsumerBuilder& 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.1).
* 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.
*/
StreamConsumerBuilder& 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.
*/
StreamConsumerBuilder& 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.
*/
StreamConsumerBuilder& subscriptionInitialPosition(SubscriptionInitialPosition p) {
config_.initialPosition = p;
return *this;
}
/**
* Attach key/value properties to the subscription itself (persisted broker-side).
* StreamConsumer only.
*
* @param p the subscription properties. Default empty.
* @return `*this` for chaining.
*/
StreamConsumerBuilder& subscriptionProperties(Properties p) {
config_.subscriptionProperties = std::move(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.
*/
StreamConsumerBuilder& consumerName(std::string n) {
config_.consumerName = std::move(n);
return *this;
}
/**
* Tune acknowledgment behavior (e.g. the ack-grouping/batching window).
*
* @note `AckPolicy::negativeAckRedeliveryDelay` does not apply to a StreamConsumer
* (it acknowledges cumulatively, with no negative-ack/redelivery path) and is
* silently ignored.
*
* @param policy the ack policy. Default-constructed `AckPolicy` when unset.
* @return `*this` for chaining.
*/
StreamConsumerBuilder& ackPolicy(AckPolicy policy) {
config_.ackPolicy = std::move(policy);
return *this;
}
/**
* Read the topic's compacted view (latest value per key) instead of the full log.
* StreamConsumer only.
*
* @param b `true` to read compacted. Default unset (broker default: uncompacted).
* @return `*this` for chaining.
*/
StreamConsumerBuilder& readCompacted(bool b) {
config_.readCompacted = b;
return *this;
}
/**
* Replicate the subscription's acknowledged position across geo-replication
* clusters. StreamConsumer only.
*
* @param b `true` to enable. Default unset (broker default: disabled).
* @return `*this` for chaining.
*/
StreamConsumerBuilder& replicateSubscriptionState(bool b) {
config_.replicateSubscriptionState = b;
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.
*/
StreamConsumerBuilder& 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 `StreamConsumer<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<StreamConsumer<T>> subscribe() { return subscribeAsync().get(); }
/**
* Create the consumer and subscribe without blocking.
*
* @return a `Future<StreamConsumer<T>>` completed with the live consumer, or with
* an `Error` on failure.
*/
Future<StreamConsumer<T>> subscribeAsync() {
Schema<T> schema = schema_;
StreamConsumerConfig config = config_;
config.schema = schema.info();
return client_.subscribeStreamAsync(std::move(config))
.thenApply(
[schema](const detail::StreamConsumerCore& core) { return StreamConsumer<T>(core, schema); });
}
private:
friend class PulsarClient;
StreamConsumerBuilder(detail::ClientCore client, Schema<T> schema)
: client_(std::move(client)), schema_(std::move(schema)) {}
detail::ClientCore client_;
Schema<T> schema_;
StreamConsumerConfig config_;
};
} // namespace pulsar::st