blob: 8848f18caaf0358f02394668b88a063f51cd3ff8 [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/Error.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/ProducerCore.h>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
namespace pulsar::st {
/**
* How a producer claims write access to a topic (spec Appendix A / §4.1).
*
* The access mode is fixed at creation time via `ProducerBuilder<T>::accessMode`
* and controls how the broker arbitrates between multiple producers on the same
* topic.
*/
enum class ProducerAccessMode
{
/** Multiple producers may publish to the topic concurrently. The default. */
Shared,
/** Only one producer may be active at a time; another producer requesting
* `Exclusive` access is rejected while one is already attached. */
Exclusive,
/** Like `Exclusive`, but a new producer fences out (evicts) any currently
* attached producer rather than being rejected. */
ExclusiveWithFencing,
/** Like `Exclusive`, but instead of failing when another producer holds the
* topic, this producer waits in line and becomes active once that one
* detaches. */
WaitForExclusive
};
/**
* Producer configuration accumulated by `ProducerBuilder<T>`.
*
* Applications do not populate this directly; the builder fills it in from its
* fluent setters and hands the completed config to the client when creating a
* producer. Field semantics, units and defaults mirror the corresponding builder
* setters.
*/
struct ProducerConfig {
/** Fully-qualified topic to produce to. REQUIRED; no default. */
std::string topic;
/** Optional producer name. Unset (the default) lets the broker assign one. */
std::optional<std::string> producerName;
/** Write-access arbitration mode. Defaults to `ProducerAccessMode::Shared`. */
ProducerAccessMode accessMode = ProducerAccessMode::Shared;
/** Per-message send timeout in milliseconds. Unset uses the SDK default. */
std::optional<int64_t> sendTimeoutMs;
/** When true (the default), block the caller while the send queue is full
* instead of failing fast with `ResultProducerQueueIsFull`. */
bool blockIfQueueFull = true;
/** Sequence id to assign to the first published message. Unset starts from
* the broker-tracked value (0 for a fresh producer). */
std::optional<int64_t> initialSequenceId;
/** Arbitrary user metadata attached to the producer. Empty by default. */
Properties properties;
/** Schema descriptor sent to the broker for compatibility checking. Filled in
* by the builder from `Schema<T>::info()`. */
SchemaInfo schema;
};
/**
* A byte-oriented outgoing message assembled by `MessageBuilder<T>`.
*
* This is the encoded, schema-agnostic form of a message: the typed value has
* already been serialized to `payload` bytes. The builder fills these fields from
* its fluent setters and hands the result to the producer core for publishing.
*
* Invariant (maintained by `MessageBuilder`): when `usesView` is true the payload is
* read from `payloadView` (which must stay valid until the send completes); otherwise
* it is read from `payload`.
*/
struct OutgoingMessage {
/** Encoded message payload — the value serialized to bytes through `Schema<T>`.
* Published unless `usesView` is set. */
std::vector<std::byte> payload;
/** Non-owning view of already-encoded bytes for zero-copy publishing
* (`Schema<BytesView>`); the caller keeps them valid until the send completes. */
std::span<const std::byte> payloadView;
/** When true, publish `payloadView` directly without copying; otherwise `payload`. */
bool usesView = false;
/** Partition/routing key; unset (nullopt, the default) means no key. */
std::optional<std::string> key;
/** Per-message user metadata. Empty by default. */
Properties properties;
/** Application event time; unset (nullopt) by default. */
std::optional<Timestamp> eventTime;
/** Explicit sequence id; unset = auto-assign. */
std::optional<int64_t> sequenceId;
/** Scheduled delivery time; unset = deliver immediately. */
std::optional<Timestamp> deliverAt;
/** Target clusters for geo-replication; empty applies the topic's default. */
std::vector<std::string> replicationClusters;
/** Enlisting transaction; unset = non-transactional. */
std::optional<Transaction> transaction;
};
template <typename T>
class Producer;
/**
* Fluent builder for a single message, obtained from `Producer<T>::newMessage()`.
*
* Each setter mutates the in-progress message and returns `*this`, so calls can
* be chained. The typed value is encoded through `Schema<T>` on `value()`; the
* terminal `send()` / `sendAsync()` hand the encoded message to the producer. A
* builder describes a single message and is consumed by its terminal call (the
* message is moved out), so it should not be reused afterwards.
*/
template <typename T>
class MessageBuilder {
public:
/**
* Set the message key, used for partition routing and key affinity.
*
* @param k the key; taken by value and moved into the message.
* @return `*this`, for chaining.
*/
MessageBuilder& key(std::string k) {
message_.key = std::move(k);
return *this;
}
/**
* Set the message value, encoding it to bytes through this producer's
* `Schema<T>`.
*
* For a zero-copy `Schema<BytesView>` producer the bytes are not copied — the
* view is published directly, so the caller must keep them valid until the send
* completes. A rare encoding failure (e.g. an unset schema) is not reported here,
* so the fluent chain stays unbroken; it surfaces from the terminal `send()` /
* `sendAsync()` instead.
*
* @param v the typed value to publish.
* @return `*this`, for chaining.
*/
MessageBuilder& value(const T& v) {
if constexpr (std::is_same_v<T, BytesView>) {
message_.payloadView = v;
message_.usesView = true;
} else {
auto r = schema_.encode(v, message_.payload);
encodeError_ = r ? std::nullopt : std::optional<Error>(r.error());
}
return *this;
}
/**
* Add or overwrite a single user property on the message.
*
* @param k property key.
* @param v property value.
* @return `*this`, for chaining.
*/
MessageBuilder& property(std::string k, std::string v) {
message_.properties.insert_or_assign(std::move(k), std::move(v));
return *this;
}
/**
* Replace all user properties on the message.
*
* @param p the full property map; taken by value and moved in.
* @return `*this`, for chaining.
*/
MessageBuilder& properties(Properties p) {
message_.properties = std::move(p);
return *this;
}
/**
* Set the application-defined event time of the message.
*
* @param t the event time as a wall-clock `Timestamp`. Unset by default (no
* event time is attached).
* @return `*this`, for chaining.
*/
MessageBuilder& eventTime(Timestamp t) {
message_.eventTime = t;
return *this;
}
/**
* Set an explicit sequence id for this message, overriding auto-assignment.
*
* @param s the sequence id. When left unset the producer assigns one
* automatically.
* @return `*this`, for chaining.
*/
MessageBuilder& sequenceId(int64_t s) {
message_.sequenceId = s;
return *this;
}
/**
* Request delayed delivery: deliver the message after `delay` has elapsed from
* now (spec §4 delayed delivery).
*
* @param delay delay relative to the current time. Computed into an absolute
* delivery time. Mutually exclusive with `deliverAt`; the last of the
* two called wins.
* @return `*this`, for chaining.
*/
MessageBuilder& deliverAfter(std::chrono::milliseconds delay) {
message_.deliverAt = std::chrono::system_clock::now() + delay;
return *this;
}
/**
* Request delayed delivery at a specific wall-clock time (spec §4 delayed
* delivery).
*
* @param t the absolute delivery time. A time in the past delivers immediately.
* Mutually exclusive with `deliverAfter`; the last of the two called
* wins.
* @return `*this`, for chaining.
*/
MessageBuilder& deliverAt(Timestamp t) {
message_.deliverAt = t;
return *this;
}
/**
* Enlist this publish in a transaction so it becomes visible only on commit
* (spec §9).
*
* @param txn the open transaction to enlist the publish in.
* @return `*this`, for chaining.
*/
MessageBuilder& transaction(const Transaction& txn) {
message_.transaction = txn;
return *this;
}
/**
* Restrict geo-replication of this message to the given clusters.
*
* @param clusters the target clusters; taken by value and moved in. Empty (the
* default) applies the topic's configured replication.
* @return `*this`, for chaining.
*/
MessageBuilder& replicationClusters(std::vector<std::string> clusters) {
message_.replicationClusters = std::move(clusters);
return *this;
}
/**
* Publish the message and block until the broker acknowledges it.
*
* @return the assigned `MessageId` on success, or the `Error` on failure. Call
* `.value()` on the result to throw `ClientException` instead.
*/
Expected<MessageId> send() { return sendAsync().get(); }
/**
* Publish the message asynchronously without blocking.
*
* @warning With a zero-copy `Schema<BytesView>` producer the payload is not
* copied, so the viewed bytes must stay valid until the send completes.
* The returned future is the only signal for that — discarding it
* (fire-and-forget) leaves no way to know when the bytes may be freed.
* For a fire-and-forget send, either keep the future, or use a copying
* `Schema<Bytes>` payload instead.
*
* @return a `Future<MessageId>` that completes with the assigned id on success
* or the failure. The future may be ignored for fire-and-forget sends
* (but see the warning above for `BytesView` payloads).
*/
Future<MessageId> sendAsync() {
if (encodeError_) {
detail::Promise<MessageId> promise;
promise.setError(*encodeError_);
return promise.getFuture();
}
return core_.sendAsync(std::move(message_));
}
private:
friend class Producer<T>;
MessageBuilder(detail::ProducerCore core, Schema<T> schema)
: core_(std::move(core)), schema_(std::move(schema)) {}
detail::ProducerCore core_;
Schema<T> schema_;
OutgoingMessage message_;
std::optional<Error> encodeError_;
};
/**
* A typed producer for a single scalable topic.
*
* Publishes values of type `T`, encoding each through its `Schema<T>`. A producer
* is a lightweight, copyable handle over shared state; a default-constructed
* producer is empty (see `operator bool`) and only a producer obtained from
* `ProducerBuilder<T>::create()` / `createAsync()` is live. All publish methods
* are thread-safe.
*/
template <typename T>
class Producer {
public:
/** Construct an empty producer; `operator bool` is false until assigned a
* live producer from `ProducerBuilder<T>`. */
Producer() = default;
/** Copyable, movable handle; copies share the underlying producer. */
Producer(const Producer&) = default;
Producer& operator=(const Producer&) = default;
Producer(Producer&&) = default;
Producer& operator=(Producer&&) = default;
/**
* Begin building a single message with per-message options (key, properties,
* event time, delayed delivery, transaction, ...).
*
* @return a fresh `MessageBuilder<T>` bound to this producer.
*/
MessageBuilder<T> newMessage() { return MessageBuilder<T>(core_, schema_); }
/**
* Publish a value and block until the broker acknowledges it. Convenience for
* `newMessage().value(value).send()`.
*
* @param value the value to publish.
* @return the assigned `MessageId` on success, or the `Error` on failure. Call
* `.value()` on the result to throw `ClientException` instead.
*/
Expected<MessageId> send(const T& value) { return newMessage().value(value).send(); }
/**
* Publish a value asynchronously without blocking. Convenience for
* `newMessage().value(value).sendAsync()`.
*
* @param value the value to publish.
* @return a `Future<MessageId>` that completes with the assigned id or the
* failure. May be ignored for fire-and-forget sends.
*/
Future<MessageId> sendAsync(const T& value) { return newMessage().value(value).sendAsync(); }
/** @return a view of the topic this producer publishes to, valid while the producer is alive. */
std::string_view topic() const { return core_.topic(); }
/** @return a view of the producer's name (broker-assigned when none was configured), valid
* while the producer is alive. */
std::string_view producerName() const { return core_.producerName(); }
/** @return the sequence id of the most recently published message, or
* std::nullopt if none has been published yet. */
std::optional<int64_t> lastSequenceId() const { return core_.lastSequenceId(); }
/**
* Block until all sends issued before this call have completed. Takes a
* snapshot of in-flight sends at the time of the call; sends issued afterwards
* are not awaited.
*
* @return success, or the first `Error` among the awaited sends. Call
* `.value()` to throw `ClientException` instead.
*/
Expected<void> flush() { return core_.flushAsync().get(); }
/**
* Asynchronously await all sends issued before this call (a snapshot of
* in-flight sends).
*
* @return a `Future<void>` that completes once those sends finish.
*/
Future<void> flushAsync() { return core_.flushAsync(); }
/**
* Block until pending sends complete, then release the producer. Idempotent:
* closing an already-closed or empty producer succeeds.
*
* @return success, or the `Error` if closing failed. Call `.value()` to throw
* `ClientException` instead.
*/
Expected<void> close() { return core_.closeAsync().get(); }
/**
* Asynchronously complete pending sends and release the producer. Idempotent.
*
* @return a `Future<void>` that completes once the producer is closed.
*/
Future<void> closeAsync() { return core_.closeAsync(); }
/** @return true if this is a live producer handle; false if empty (default
* constructed or moved-from). */
explicit operator bool() const { return static_cast<bool>(core_); }
private:
template <typename U>
friend class ProducerBuilder;
Producer(detail::ProducerCore core, Schema<T> schema)
: core_(std::move(core)), schema_(std::move(schema)) {}
detail::ProducerCore core_;
Schema<T> schema_;
};
/**
* Builder for a `Producer<T>`, obtained from `PulsarClient::newProducer`.
*
* Each setter returns `*this` for chaining. `topic` is the only required setting;
* the terminal `create()` / `createAsync()` produce the `Producer<T>`.
*/
template <typename T>
class ProducerBuilder {
public:
/**
* Set the topic to produce to. REQUIRED; there is no default.
*
* @param t the fully-qualified topic name; taken by value and moved in.
* @return `*this`, for chaining.
*/
ProducerBuilder& topic(std::string t) {
config_.topic = std::move(t);
return *this;
}
/**
* Set an explicit producer name.
*
* @param n the producer name; taken by value and moved in. Optional; when
* unset (the default) the broker assigns a name.
* @return `*this`, for chaining.
*/
ProducerBuilder& producerName(std::string n) {
config_.producerName = std::move(n);
return *this;
}
/**
* Set the write-access arbitration mode (spec Appendix A / §4.1).
*
* @param m the access mode. Defaults to `ProducerAccessMode::Shared`.
* @return `*this`, for chaining.
*/
ProducerBuilder& accessMode(ProducerAccessMode m) {
config_.accessMode = m;
return *this;
}
/**
* Set the per-message send timeout: how long a send may stay unacknowledged
* before failing.
*
* @param d the timeout. Optional; when unset the SDK default applies.
* @return `*this`, for chaining.
*/
ProducerBuilder& sendTimeout(std::chrono::milliseconds d) {
config_.sendTimeoutMs = d.count();
return *this;
}
/**
* Control behavior when the producer's send queue is full.
*
* @param b when true (the DEFAULT), block the caller until the queue drains;
* when false, fail fast with `ResultProducerQueueIsFull`.
* @return `*this`, for chaining.
*/
ProducerBuilder& blockIfQueueFull(bool b) {
config_.blockIfQueueFull = b;
return *this;
}
/**
* Set the sequence id assigned to the first published message.
*
* @param s the initial sequence id. Optional; when unset the producer starts
* from the broker-tracked value (0 for a fresh producer).
* @return `*this`, for chaining.
*/
ProducerBuilder& initialSequenceId(int64_t s) {
config_.initialSequenceId = s;
return *this;
}
/**
* Add or overwrite a single user property on the producer.
*
* @param k property key.
* @param v property value.
* @return `*this`, for chaining.
*/
ProducerBuilder& property(std::string k, std::string v) {
config_.properties.insert_or_assign(std::move(k), std::move(v));
return *this;
}
/**
* Create the producer, blocking until it is ready.
*
* @return the live `Producer<T>` on success, or the `Error` on failure. Call
* `.value()` on the result to throw `ClientException` instead.
*/
Expected<Producer<T>> create() { return createAsync().get(); }
/**
* Create the producer asynchronously without blocking.
*
* @return a `Future<Producer<T>>` that completes with the live producer on
* success or the failure.
*/
Future<Producer<T>> createAsync() {
Schema<T> schema = schema_;
ProducerConfig config = config_;
config.schema = schema.info();
return client_.createProducerAsync(std::move(config))
.thenApply([schema](const detail::ProducerCore& core) { return Producer<T>(core, schema); });
}
private:
friend class PulsarClient;
ProducerBuilder(detail::ClientCore client, Schema<T> schema)
: client_(std::move(client)), schema_(std::move(schema)) {}
detail::ClientCore client_;
Schema<T> schema_;
ProducerConfig config_;
};
} // namespace pulsar::st