Skip to content

Commit cff0cf4

Browse files
authored
Scalable Topics: new typed C++ SDK (pulsar::st) — API definition (#598)
* Scalable Topics: typed C++ SDK public API (pulsar::st) Header-only public API for the scalable-topics SDK under a new pulsar::st namespace (PIP-460/468/483): client, producers, the three consumer modes, transactions, schemas (reflect-cpp JSON/Avro and protobuf), and the Expected<T>/Future<T> result types, plus examples under examples/st. API definition only -- no lib/st implementation or C API yet. The new API requires C++20; the rest of the client stays C++17. Signed-off-by: Matteo Merli <mmerli@apache.org> * Fix CI: clang-format the st sources; make reflect-cpp optional - Apply clang-format-11 to the new pulsar::st headers and examples (the Formatting Check uses clang-format 11; local 18 formats differently). - examples/CMakeLists.txt: build the four dependency-free st samples unconditionally and add the reflect-cpp JSON sample only when reflectcpp is found (find_package CONFIG QUIET instead of REQUIRED), so configure no longer fails where reflect-cpp is absent (e.g. the CodeQL/Analyze job). - vcpkg.json: drop the reflectcpp dependency for now; it returns with the lib/st implementation that actually exercises the JSON/Avro schemas. Signed-off-by: Matteo Merli <mmerli@apache.org> * Fix CI: give st config-struct fields default member initializers GCC's -Wmissing-field-initializers (-Wextra, and the build is -Werror) fires on a partial designated-initializer such as .deadLetterPolicy({.maxRedeliverCount = 5}) for every omitted member that lacks a default member initializer. clang does not warn, so this was missed locally. Give every optional field in the user-facing policy/ack/DLQ structs an '= std::nullopt' NSDMI so designated-init of any subset is warning-clean. Verified with gcc:13 -Wextra -Werror against all four st examples. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: decode returns Expected<T> and takes std::span<const char> Addresses PR review feedback on the Schema decode signature. The SerDe seam now takes a std::span<const char> instead of (const char*, size_t), and returns Expected<T> instead of T -- so malformed bytes or an unset schema are error values rather than a non-opt-in throw, consistent with the rest of the API. Message<T>::value() returns Expected<T> accordingly. - built-in numeric codecs report a short payload as ResultInvalidMessage; - the reflect-cpp JSON/Avro SerDes map a parse failure to an Error instead of letting rfl's .value() throw; - the protobuf SerDe now checks ParseFromArray's result; - a custom SerDe may still return a plain T (infallible) -- it converts implicitly to Expected<T>. encode keeps throwing on an unset schema (a configuration error). Examples updated to check the decoded value. Verified with clang + gcc:13 (-Wextra -Werror) and clang-format-11. Signed-off-by: Matteo Merli <mmerli@apache.org> * Revert "st: decode returns Expected<T> and takes std::span<const char>" This reverts commit 46d3f6d. * st: byte-buffer SerDe seam + zero-copy BytesView Per PR review on the Schema encode/decode signatures. SerDe seam (Schema<T> + JSON/Avro/protobuf factories): - encode writes into a caller-provided, reusable std::vector<std::byte>& (no per-message allocation) and returns Expected<void>; - decode takes std::span<const std::byte> and returns Expected<T>, so malformed input / an unset schema are error values rather than throws; - Bytes is now std::vector<std::byte>. Client-facing API unchanged: Message<T>::value() still returns T (decode failures are handled inside the SDK), Producer::send(const T&) and the examples are as before; a rare encode error is stashed in the builder and surfaces from send()/sendAsync(). Zero-copy bytes: new BytesView = std::span<const std::byte>. Schema<BytesView> is the zero-copy counterpart of Schema<Bytes> -- Producer<BytesView> publishes the caller's bytes without copying (the caller keeps them valid until the send completes) and Message<BytesView>::value() returns a view into the message buffer. OutgoingMessage carries an optional non-owning view. Verified with clang + gcc:13 (-Wextra -Werror), clang-format-11, and a runtime check that decode returns a view at the same address. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: return std::string_view from string accessors Per PR review: the string accessors return views instead of owning references/copies, so the lib/st impl is not forced to store a std::string per field -- it can return a view into whatever it already holds. - consumer/producer topic() / subscription() / consumerName() / name() and Message::topic() now return std::string_view (Message::topic() previously copied); the detail::*Core declarations they forward to return string_view too. - Message::key() / producerName() / replicatedFrom() now return std::optional<std::string_view>. - Error::message() stays const std::string& (an Error is usually a temporary, so auto-capturing a const ref copies safely whereas a view would dangle). Returned views are valid while the source object (message / consumer / producer) is alive. All within pulsar::st; the old API is untouched. Verified with clang + gcc:13 (-Wextra -Werror), static_asserts on the return types, and clang-format-11. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: serialize MessageId/Checkpoint as bytes, not std::string toByteArray() returns std::vector<std::byte> and fromByteArray() takes std::span<const std::byte>, instead of std::string -- byte-correct and consistent with Bytes/BytesView. The round-trip stays implicit: a std::vector<std::byte> from toByteArray() converts to the span parameter. Example updated. All within pulsar::st. Verified with clang + gcc:13 (-Wextra -Werror) and clang-format-11. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: make property() a by-value sink (review item K1) property(const std::string& k, const std::string& v) becomes property(std::string k, std::string v) with insert_or_assign(std::move(k), std::move(v)), across MessageBuilder, ProducerBuilder, and the three consumer builders -- consistent with the other by-value-sink setters (topic / subscriptionName / etc.). Verified with clang + gcc:13 (-Wextra -Werror) and clang-format-11. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: fix await_suspend coroutine resume race (review B1) await_suspend now returns bool and uses SharedState::addListenerOrReady, which atomically registers the resume continuation or reports the result is already available -- so the coroutine resumes via await_resume instead of being resumed from inside await_suspend (which could run/destroy the awaiter before it returns). Verified with a co_await runtime test on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: receive() does not surface decode errors -- doc fix (review B2) A message whose payload cannot be decoded is handled internally by the SDK and never delivered, so decode is not a receive failure. Dropped it from the receive failure lists on all three consumers and added a clarifying note. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: uppercase primitive schema names STRING/DOUBLE (review B4) Match the existing client's canonical primitive names (lib/Schema.cc: STRING/INT32/INT64/FLOAT/DOUBLE/BYTES); StringCodec/DoubleCodec used mixed-case 'String'/'Double'. The name is sent to the broker. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: reset encodeError_ on a successful encode (review B5) MessageBuilder::value() now clears encodeError_ on success instead of leaving a prior failure sticky, so a later successful value() doesn't surface a stale error at send()/sendAsync(). Signed-off-by: Matteo Merli <mmerli@apache.org> * st: model event time as std::optional<Timestamp> (review B3) OutgoingMessage::eventTime and MessageCore::eventTime() are now std::optional<Timestamp> instead of an int64 epoch-ms with a 0=unset sentinel, so an event time of exactly the Unix epoch is no longer indistinguishable from unset. The int64 epoch-ms is just the wire encoding (converted in lib/st); MessageBuilder::eventTime and Message::eventTime() simplify accordingly. Verified epoch != unset at runtime on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: add CheckpointConsumer::consumerName() (review G2) Parity with Stream/QueueConsumer -- the consumerName config field and builder setter existed, but the getter did not, so the name could be set but not read. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: [[nodiscard]] on MessageId/Checkpoint serialization + sentinels (review P3) toByteArray() / fromByteArray() / earliest() / latest() return values that must not be silently discarded. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: document Message::properties() view lifetime (review P4) It returns a reference into the message, like the other view-returning getters. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: model deliverAt as std::optional<Timestamp> too (review B3 follow-on) OutgoingMessage::deliverAt is now std::optional<Timestamp> (was int64 epoch-ms with 0=immediate), matching eventTime; deliverAfter/deliverAt set it directly, and the now-unused toEpochMs helper is removed. Verified on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: model publishTime as Timestamp, not int64 epoch-ms MessageCore::publishTime() now returns Timestamp (was int64_t publishTimeMs()); Message::publishTime() forwards it directly. Consistent with the eventTime / deliverAt Timestamp modeling; the int64 epoch-ms is just the wire encoding (converted in lib/st). Signed-off-by: Matteo Merli <mmerli@apache.org> * st: producer review items P1, G3, Q1 - P1: rename Producer::name() -> producerName() (+ ProducerCore), consistent with Message::producerName() and the producerName builder setter. - G3: add MessageBuilder::replicationClusters() setter for the previously unreachable OutgoingMessage::replicationClusters field. - Q1: drop the 'ordering key' framing from the message-key docs -- it is a routing / partition key; ordering is provided by the StreamConsumer. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: add float/int8/int16 primitive codecs (review G4/Q4) FloatCodec (FLOAT, big-endian IEEE-754), Int8Codec (INT8), Int16Codec (INT16), wired into the default Schema<T> ctor; canonical uppercase names match the existing client. Round-trip verified on clang + gcc:13. bool is NOT added: the existing pulsar::SchemaType enum has no BOOLEAN value (Java has it at 5; the C++ port skipped it), and adding it would mean touching the old API. Deferred pending a decision on extending the old enum. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: Message::data() returns BytesView, not const char* + size() MessageCore::data() / Message<T>::data() now return std::span<const std::byte> (BytesView), carrying pointer and length together; the separate size() accessor is removed (use data().size()), and Message<T>::value() simplifies accordingly. Consistent with the Bytes/BytesView byte modeling. Verified on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: group loose client settings into policies by scope (review Q3) PulsarClientBuilder drops the top-level ioThreads / messageListenerThreads / memoryLimit / listenerName setters. Grouped by scope: - listenerName -> ConnectionPolicy - ioThreads + messageListenerThreads -> new ThreadPolicy - memoryLimit -> new MemoryPolicy with threadPolicy() / memoryPolicy() builder setters. Verified on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: MessageCore optional accessors, drop hasX() bools MessageCore::key() / producerName() / replicatedFrom() now return std::optional<std::string_view> directly instead of a paired hasX() bool + string_view accessor -- the optional carries the present/absent signal. Message<T>'s wrappers collapse to direct forwards. Verified on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: receive cores take std::chrono::milliseconds, not int64_t timeoutMs detail::*Core receiveAsync/receiveMultiAsync now take std::chrono::milliseconds (matching the public receive() signatures), so the public methods forward the typed timeout directly instead of calling .count(). <cstdint> swapped for <chrono> in the cores (int64_t was only the timeout). Verified on clang + gcc:13. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: OutgoingMessage key -> std::optional<std::string> Replace the bool hasKey + std::string key pair on OutgoingMessage with a single std::optional<std::string> key, mirroring the read-side MessageCore::key() -> std::optional<std::string_view>. nullopt means no routing key. MessageBuilder::key() now just assigns the optional. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: OutgoingMessage sequenceId -> std::optional<int64_t> Drop the -1 sentinel on OutgoingMessage::sequenceId in favor of std::optional<int64_t>; unset means auto-assign. Avoids a custom in-band encoding of 'no explicit sequence id'. MessageBuilder::sequenceId() just assigns the optional. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: Producer::lastSequenceId() -> std::optional<int64_t> Drop the -1 sentinel on the read side too: lastSequenceId() now returns std::nullopt when nothing has been published yet, instead of -1. Updates detail::ProducerCore to match. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P2 - guard rfl encode() against throwing rfl::{json,avro}::write() can throw, which would escape encode()'s Expected<void> non-throwing contract. Wrap the body in try/catch and report failures as unexpected(ResultInvalidMessage, ...), mirroring the existing decode() guard. info() (schema derivation) has no error channel and stays off the non-throwing path; document that on the factories. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P5 - warn about fire-and-forget + BytesView dangling A zero-copy Schema<BytesView> send publishes the viewed bytes directly, so they must outlive the send. The returned future is the only completion signal; discarding it (fire-and-forget) leaves no safe point to free the bytes. Document this on sendAsync(). Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P6 - std::hash<MessageId> + Checkpoint operator<< MessageId could not be used as a key in unordered_map/unordered_set. Add a std::hash<MessageId> specialization (operator() defined in lib/st, consistent with operator==: equal ids hash equal); befriend it so it can read the impl. Give Checkpoint a hidden-friend operator<< mirroring MessageId's, so both opaque position types stream the same way for logging/debugging. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P7 - explicitly default copy/move on handles PulsarClient, Producer, the three consumers, and Transaction are shared-state handles that must stay cheaply copyable and movable. They relied on implicitly-generated special members, which a later user-declared destructor would silently suppress (turning the move into a copy or deleting it). Declare copy/move = default explicitly on all six to lock in handle value semantics and make the intent visible. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P8 - thenApply supports void-returning and move-only mappers thenApply assumed a non-void, copyable mapper: it called setValue(f(...)) (ill-formed when f returns void) and moved f straight into the std::function listener (ill-formed when f is move-only, since std::function requires a copyable target). Branch on the result type with if constexpr - a void mapper runs and then completes the Future<void> via setSuccess() - and hold f in a shared_ptr so the copyable listener can carry a move-only mapper. Verified at runtime (normal, void, move-only, and error-propagation paths) on clang and gcc. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P9 - fail the future when a Promise is abandoned A detail::Promise dropped without being completed left its SharedState forever pending, so Future::get() (and listeners / co_await) blocked indefinitely. Add a Guard shared by every copy of a Promise: when the last copy is destroyed it completes the state with an error (ResultUnknownError, "promise abandoned before completion") unless something already fulfilled it. complete() is idempotent, so a normally-completed promise is unaffected, and destroying one copy among several does not trip it. Verified at runtime (single/copied/void abandonment, partial-copy safety, completed no-op) on clang and gcc. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P10 - note negativeAckRedeliveryDelay is inert on StreamConsumer AckPolicy::negativeAckRedeliveryDelay only applies to a QueueConsumer. A StreamConsumer acknowledges cumulatively and has no negative-ack path, so the field is silently ignored there. Document that on the StreamConsumer config field and the ackPolicy() setter. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P11 - document the invalid/rejected default consumer target The topic-vs-namespace target is a bool + two strings, so the POD config can represent invalid combinations the type system does not prevent - including the default-constructed value (single-topic mode with an empty topic). Document that such states (no target, or missing subscriptionName) are rejected by create()/createAsync() with an Error, and that fields not selected by useNamespace are ignored. (A variant target could make these unrepresentable, but that diverges from the POD-config + designated-init pattern used across the API.) Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P12 - rename ClientCore::createCheckpointAsync -> createCheckpointConsumerAsync Match the create<Thing>Async naming of its siblings (createProducerAsync) and the CheckpointConsumer type it returns. Internal detail rename; no public API change. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: P13 - rvalue overloads for Expected monadic ops and value_or value_or, and_then, transform, and or_else were const&-only: they copied the contained value into the continuation, and value_or/and_then/transform would not even compile for a move-only T. Add &&-qualified overloads that move the contained value (and forward the error by move), so a move-only or expensive-to-copy T flows through the chain without a copy. value() and operator* already had ref-qualified overloads. Verified at runtime with a move-only payload (unique_ptr) on clang and gcc, plus an lvalue regression pass. Signed-off-by: Matteo Merli <mmerli@apache.org> * st: review nits N1-N7 N1 Expected operator*/operator-> are not UB on an error: operator* is noexcept + std::get so it terminates; operator-> returns nullptr. Correct the docs to say so. N2 Drop redundant unit prose ("in milliseconds"/"in seconds") from std::chrono fields/params in Policies, Consumer (AckPolicy) and the sendTimeout setter; the type already states the unit. (ProducerConfig's int64 sendTimeoutMs keeps its "milliseconds" note - it is not a chrono type.) N3 decodeBigEndian: replace the dead `i < data.size()` guard (all codecs length-check first) with an assert of that precondition. N4 ProtobufNativeSchema: guard the size_t->int narrowing in encode/decode, rejecting messages larger than INT_MAX instead of passing a wrapped size. N5 OutgoingMessage: one-line note for the usesView<->payloadView invariant. N7 Wrap the SerDeFor concept in clang-format off/on so clang-format-11 stops mangling the `{ expr } -> Concept;` compound requirements. N6 Normalize config-struct field docs to the dominant /** */-before style (OutgoingMessage, CheckpointConsumerConfig, Stream/QueueConsumerConfig); enum-value ///< trailing docs are left as-is. Verified: clang-format-11 clean; examples compile (clang); N3 runtime test and N4 (protobuf stub) pass on clang and gcc. Signed-off-by: Matteo Merli <mmerli@apache.org> --------- Signed-off-by: Matteo Merli <mmerli@apache.org>
1 parent 0711b31 commit cff0cf4

33 files changed

Lines changed: 5311 additions & 0 deletions

examples/CMakeLists.txt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,42 @@ target_link_libraries(SampleReaderCApi ${CLIENT_LIBS} pulsarShar
105105
target_link_libraries(SampleKeyValueSchemaConsumer ${CLIENT_LIBS} pulsarShared)
106106
target_link_libraries(SampleKeyValueSchemaProducer ${CLIENT_LIBS} pulsarShared)
107107
target_link_libraries(SampleCustomLoggerCApi ${CLIENT_LIBS} pulsarShared)
108+
109+
# --- Scalable topics (pulsar::st) examples ---------------------------------
110+
# These use the new typed scalable-topics API under include/pulsar/st. Its
111+
# implementation (lib/st) does not exist yet, so the examples are COMPILED here
112+
# for header/API verification but are NOT linked into executables (there are no
113+
# symbols to link against). Building this OBJECT library on every build keeps the
114+
# examples from bit-rotting while the API is reviewed.
115+
#
116+
# TODO(scalable-topics): once lib/st lands, replace this with one
117+
# add_executable + target_link_libraries(... pulsarShared) per file, exactly like
118+
# the samples above.
119+
# The core samples are header-only previews of the pulsar::st API and build with
120+
# no extra dependency.
121+
set(SAMPLE_ST_SOURCES
122+
st/SampleStProducer.cc
123+
st/SampleStStreamConsumer.cc
124+
st/SampleStQueueConsumer.cc
125+
st/SampleStCheckpointConsumer.cc
126+
)
127+
# reflect-cpp powers jsonSchema<T>() (reflection-based JSON SerDe + schema). It is
128+
# optional for this API-only PR: when the package is present the JSON sample is
129+
# added and linked against it; when absent, only that one sample is skipped. (The
130+
# reflectcpp vcpkg port does not yet ship an Avro backend, so it is not yet wired
131+
# into the manifest; it will be added with the lib/st implementation.)
132+
find_package(reflectcpp CONFIG QUIET)
133+
if (reflectcpp_FOUND)
134+
list(APPEND SAMPLE_ST_SOURCES st/SampleStJsonSchema.cc)
135+
endif ()
136+
137+
add_library(StExamples OBJECT ${SAMPLE_ST_SOURCES})
138+
# The scalable-topics (pulsar::st) API targets C++20; the rest of the client stays
139+
# C++17. Set the standard per-target so only this code requires C++20.
140+
set_target_properties(StExamples PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON)
141+
# PRIVATE link gives the object sources pulsarShared's include directories; an
142+
# OBJECT library is not itself linked, so the missing lib/st symbols are fine.
143+
target_link_libraries(StExamples PRIVATE ${CLIENT_LIBS} pulsarShared)
144+
if (reflectcpp_FOUND)
145+
target_link_libraries(StExamples PRIVATE reflectcpp::reflectcpp)
146+
endif ()

examples/st/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Scalable Topics (`pulsar::st`) — API preview examples
2+
3+
These examples exercise the new typed scalable-topics C++ API under
4+
[`include/pulsar/st/`](../../include/pulsar/st). They illustrate the proposed
5+
surface and exist to gather community feedback.
6+
7+
> **Status: API definition only.** The implementation (`lib/st/`) does not exist
8+
> yet, so these examples **compile but do not yet link**. They are wired into the
9+
> CMake build as a compile-only `OBJECT` library (`StExamples` in
10+
> [`examples/CMakeLists.txt`](../CMakeLists.txt)) — header-verified on every build,
11+
> but not linked. Once `lib/st` lands they become normal `add_executable` targets.
12+
13+
The `pulsar::st` API requires **C++20** (the rest of the client stays C++17).
14+
Syntax-check an example against the headers (no linking):
15+
16+
```sh
17+
clang++ -std=c++20 -I ../../include -Wall -fsyntax-only SampleStProducer.cc
18+
```
19+
20+
| File | Shows |
21+
|---|---|
22+
| `SampleStProducer.cc` | blocking + asynchronous publishing, transactions |
23+
| `SampleStStreamConsumer.cc` | ordered (per-key) delivery, cumulative ack |
24+
| `SampleStQueueConsumer.cc` | parallel delivery, individual ack + nack, dead-letter |
25+
| `SampleStCheckpointConsumer.cc`| externally held position via `Checkpoint` |
26+
| `SampleStJsonSchema.cc` | a struct as JSON with zero boilerplate (`jsonSchema<T>()`, reflect-cpp) |
27+
28+
## API at a glance
29+
30+
- **Typed builders** off one `PulsarClient`: `newProducer` / `newStreamConsumer` /
31+
`newQueueConsumer` / `newCheckpointConsumer`, each taking a `Schema<T>`.
32+
- **Synchronous calls return `Expected<T>`** (a stand-in for `std::expected`,
33+
which is C++23): check it, or call `.value()` to throw `ClientException`.
34+
`Expected<T>` is `[[nodiscard]]`, so a failure cannot be silently dropped.
35+
- **Asynchronous calls return `Future<T>`**: `addListener(...)` to react on
36+
completion without blocking, `get()` to block, or `co_await` it.
37+
- **Schemas**: primitives are built in; structured types use `jsonSchema<T>()` /
38+
`avroSchema<T>()` (reflect-cpp derives the SerDe **and** the declared schema from
39+
the struct — no boilerplate), `protobufNativeSchema<T>()`, or a custom
40+
`Schema<T>(serde)`. reflect-cpp is a required dependency of `pulsar::st`.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Scalable-topics CheckpointConsumer: the application owns the position. Read,
21+
// snapshot a Checkpoint, persist it externally, and later resume from it.
22+
23+
#include <pulsar/st/Client.h>
24+
25+
#include <cstddef>
26+
#include <iostream>
27+
#include <string>
28+
#include <vector>
29+
30+
using namespace pulsar::st;
31+
32+
int main() {
33+
auto clientResult = PulsarClient::builder().serviceUrl("pulsar://localhost:6650").build();
34+
if (!clientResult) {
35+
std::cerr << "failed to build client: " << clientResult.error() << "\n";
36+
return 1;
37+
}
38+
PulsarClient client = std::move(clientResult).value();
39+
40+
// Restore from a previously stored checkpoint if you have one; else start at
41+
// the earliest message. (Checkpoint::fromByteArray(savedBytes) to resume.)
42+
auto consumerResult = client.newCheckpointConsumer(Schema<std::string>{})
43+
.topic("topic://public/default/orders")
44+
.startPosition(Checkpoint::earliest())
45+
.create(); // NOTE: create(), not subscribe()
46+
if (!consumerResult) {
47+
std::cerr << "failed to create consumer: " << consumerResult.error() << "\n";
48+
return 1;
49+
}
50+
CheckpointConsumer<std::string> consumer = std::move(consumerResult).value();
51+
52+
for (int i = 0; i < 5; i++) {
53+
auto msg = consumer.receive(std::chrono::seconds(5));
54+
if (!msg) {
55+
if (msg.error().result == ResultTimeout) break;
56+
std::cerr << "receive failed: " << msg.error() << "\n";
57+
break;
58+
}
59+
std::cout << "read: " << msg->value() << "\n";
60+
}
61+
62+
// Atomic position snapshot across all segments. Store the bytes yourself
63+
// (Flink/Spark state backend, a file, etc.) — there is no broker-side cursor.
64+
Checkpoint checkpoint = consumer.checkpoint();
65+
std::vector<std::byte> persisted = checkpoint.toByteArray(); // store these bytes yourself
66+
std::cout << "checkpoint is " << persisted.size() << " bytes\n";
67+
68+
(void)consumer.close();
69+
(void)client.close();
70+
return 0;
71+
}

examples/st/SampleStJsonSchema.cc

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Passing a struct as JSON: `jsonSchema<T>()` derives both the SerDe and the
21+
// declared schema from the struct's fields (via reflect-cpp) — NO macros, NO base
22+
// class, NO schema string, NO serializer. Nested structs and containers included.
23+
// `avroSchema<T>()` is identical for Avro.
24+
25+
#include <pulsar/st/Client.h>
26+
#include <pulsar/st/JsonSchema.h>
27+
28+
#include <iostream>
29+
#include <string>
30+
#include <vector>
31+
32+
// Plain value types — that is the entire schema "declaration".
33+
struct Address {
34+
std::string street;
35+
std::string city;
36+
};
37+
struct Order {
38+
std::string orderId;
39+
int quantity;
40+
double unitPrice;
41+
Address shipTo; // nested struct — handled automatically
42+
std::vector<std::string> tags; // container — handled automatically
43+
};
44+
45+
using namespace pulsar::st;
46+
47+
int main() {
48+
auto clientResult = PulsarClient::builder().serviceUrl("pulsar://localhost:6650").build();
49+
if (!clientResult) {
50+
std::cerr << clientResult.error() << "\n";
51+
return 1;
52+
}
53+
PulsarClient client = std::move(clientResult).value();
54+
55+
auto producerResult =
56+
client.newProducer(jsonSchema<Order>()).topic("topic://public/default/orders").create();
57+
if (!producerResult) {
58+
std::cerr << producerResult.error() << "\n";
59+
return 1;
60+
}
61+
Producer<Order> producer = std::move(producerResult).value();
62+
63+
Order order{"ord-1", 3, 9.99, {"1 Main St", "Springfield"}, {"priority", "gift"}};
64+
if (auto sent = producer.send(order); sent) {
65+
std::cout << "sent " << *sent << "\n";
66+
}
67+
68+
auto consumerResult = client.newStreamConsumer(jsonSchema<Order>())
69+
.topic("topic://public/default/orders")
70+
.subscriptionName("orders-sub")
71+
.subscribe();
72+
if (consumerResult) {
73+
StreamConsumer<Order> consumer = std::move(consumerResult).value();
74+
if (auto msg = consumer.receive(std::chrono::seconds(5))) {
75+
Order received = msg->value(); // decoded straight back into the struct
76+
std::cout << received.orderId << " -> " << received.shipTo.city << "\n";
77+
consumer.acknowledgeCumulative(msg->id());
78+
}
79+
(void)consumer.close();
80+
}
81+
82+
(void)producer.close();
83+
(void)client.close();
84+
return 0;
85+
}

examples/st/SampleStProducer.cc

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Scalable-topics producer: blocking and asynchronous publishing.
21+
22+
#include <pulsar/st/Client.h>
23+
24+
#include <iostream>
25+
26+
using namespace pulsar::st;
27+
28+
int main() {
29+
// One client per application; keep it for the whole lifetime.
30+
auto clientResult = PulsarClient::builder().serviceUrl("pulsar://localhost:6650").build();
31+
if (!clientResult) {
32+
std::cerr << "failed to build client: " << clientResult.error() << "\n";
33+
return 1;
34+
}
35+
PulsarClient client = std::move(clientResult).value();
36+
37+
auto producerResult = client.newProducer(Schema<std::string>{})
38+
.topic("topic://public/default/orders")
39+
.sendTimeout(std::chrono::seconds(30))
40+
.create();
41+
if (!producerResult) {
42+
std::cerr << "failed to create producer: " << producerResult.error() << "\n";
43+
return 1;
44+
}
45+
Producer<std::string> producer = std::move(producerResult).value();
46+
47+
// Blocking send: returns Expected<MessageId> (must be checked — [[nodiscard]]).
48+
for (int i = 0; i < 10; i++) {
49+
auto sent = producer.newMessage()
50+
.key("order-" + std::to_string(i % 4)) // per-key ordering
51+
.value("payload-" + std::to_string(i))
52+
.property("attempt", "1")
53+
.send();
54+
if (sent) {
55+
std::cout << "sent " << *sent << "\n";
56+
} else {
57+
std::cerr << "send failed: " << sent.error() << "\n";
58+
}
59+
}
60+
61+
// Asynchronous send: react on completion without blocking.
62+
producer.newMessage()
63+
.key("order-async")
64+
.value("async-payload")
65+
.sendAsync()
66+
.addListener([](const Expected<MessageId>& result) {
67+
if (result) {
68+
std::cout << "async sent " << *result << "\n";
69+
} else {
70+
std::cerr << "async send failed: " << result.error() << "\n";
71+
}
72+
});
73+
74+
// Transaction: produced messages become visible atomically on commit.
75+
if (auto txnResult = client.newTransaction()) {
76+
Transaction txn = *txnResult;
77+
auto a = producer.newMessage().value("tx-a").transaction(txn).send();
78+
auto b = producer.newMessage().value("tx-b").transaction(txn).send();
79+
if (a && b) {
80+
if (auto committed = txn.commit(); !committed) {
81+
std::cerr << "commit failed: " << committed.error() << "\n";
82+
}
83+
} else {
84+
(void)txn.abort();
85+
}
86+
}
87+
88+
(void)producer.flush(); // await all sends issued before this call
89+
if (auto closed = producer.close(); !closed) {
90+
std::cerr << "close failed: " << closed.error() << "\n";
91+
}
92+
(void)client.close();
93+
return 0;
94+
}

0 commit comments

Comments
 (0)