blob: d484997cf314828905bae3ab1a47e3fd571d203a [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/st/Expected.h>
#include <atomic>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
// The client-side model of a scalable topic's segment layout, built from the
// ScalableTopicDAG the broker sends on the DAG-watch session, plus the key ->
// segment router. Ported from the Java v5 client (ClientSegmentLayout /
// SegmentRouter / ScalableTopicHashing) so the two clients route identically.
namespace pulsar {
namespace proto {
class ScalableTopicDAG;
}
} // namespace pulsar
namespace pulsar::st {
/**
* Scalable-topic key hashing — the single source of truth for both segment
* routing and entry-bucketing.
*
* A single raw (unmasked) 32-bit Murmur3 hash of a key splits into two
* independent 16-bit halves: the HIGH half routes segments, the LOW half
* routes entry-buckets. The raw hash is required so the high half is
* full-range (the classic masked hash clears bit 31, which would confine the
* high half to [0, 0x7FFF]). Compute murmur() once per key and split it.
*/
struct ScalableTopicHashing {
/** The raw 32-bit Murmur3 hash of a key. */
static std::uint32_t murmur(std::string_view key);
/** The 16-bit segment-routing hash (high 16 bits) of a precomputed murmur(). */
static std::uint32_t segmentHash(std::uint32_t murmur) { return (murmur >> 16) & 0xFFFFu; }
/** The 16-bit entry-bucket hash (low 16 bits) of a precomputed murmur(). */
static std::uint32_t entryBucketHash(std::uint32_t murmur) { return murmur & 0xFFFFu; }
};
/** An inclusive hash range [start, end] within the 16-bit hash space (0x0000-0xFFFF). */
struct HashRange {
static constexpr std::uint32_t kMinHash = 0x0000;
static constexpr std::uint32_t kMaxHash = 0xFFFF;
std::uint32_t start = kMinHash;
std::uint32_t end = kMaxHash;
bool contains(std::uint32_t hash) const { return hash >= start && hash <= end; }
bool operator==(const HashRange&) const = default;
};
/**
* One segment of a scalable topic, as seen by the client: its identity, the
* hash range it owns, and the topic its per-segment producer/consumer attaches
* to.
*
* `legacyTopicName` is set for legacy segments — entries in a synthetic layout
* that wrap an existing, externally managed persistent:// topic (e.g. one
* partition of a not-yet-migrated regular topic). Those attach to the wrapped
* topic; regular controller-managed segments attach to the computed
* segment://... topic.
*/
struct Segment {
std::uint64_t segmentId = 0;
HashRange range;
/** The computed segment://tenant/ns/topic/xxxx-xxxx-id topic. */
std::string segmentTopicName;
/** Wrapped persistent:// topic for legacy segments; unset otherwise. */
std::optional<std::string> legacyTopicName;
/** Entry-bucket split points within the segment (empty = one bucket). */
std::vector<std::uint32_t> entryBucketSplits;
bool isLegacy() const { return legacyTopicName.has_value(); }
/** The topic a per-segment producer/consumer attaches to. */
const std::string& attachTopicName() const {
return legacyTopicName ? *legacyTopicName : segmentTopicName;
}
};
/**
* Immutable client-side view of a scalable topic's segment layout at one DAG
* epoch. Built from the ScalableTopicDAG the broker returns on the DAG-watch
* session; consumers of this class swap whole layouts as updates arrive (epoch
* monotonicity is enforced by the watch session, not here).
*/
class SegmentLayout {
public:
/** An empty layout: epoch 0, no segments. */
SegmentLayout() = default;
/**
* Build a layout from the broker-provided DAG.
*
* @param dag the DAG snapshot from CommandScalableTopicUpdate.
* @param resolvedTopicName the canonical "topic://tenant/ns/topic" identity
* (CommandScalableTopicUpdate.resolved_topic_name); used to compute
* the per-segment segment:// topic names.
* @return the layout, or an Error if the DAG is malformed (bad topic scheme
* or an out-of-bounds hash range).
*/
static Expected<SegmentLayout> fromProto(const pulsar::proto::ScalableTopicDAG& dag,
const std::string& resolvedTopicName);
/** The DAG generation this layout represents. */
std::uint64_t epoch() const { return epoch_; }
/** Active segments, sorted by hash-range start (the routing order). */
const std::vector<Segment>& activeSegments() const { return activeSegments_; }
/** Sealed segments still present in the DAG (finite, eventually drained), sorted by id. */
const std::vector<Segment>& sealedSegments() const { return sealedSegments_; }
/** The broker serving a segment, or nullptr if the DAG did not name one. */
const std::string* brokerUrl(std::uint64_t segmentId) const;
/** The TLS address of the broker serving a segment, or nullptr if absent. */
const std::string* brokerUrlTls(std::uint64_t segmentId) const;
/** The controller leader's address, when the DAG carries one. */
const std::optional<std::string>& controllerBrokerUrl() const { return controllerBrokerUrl_; }
const std::optional<std::string>& controllerBrokerUrlTls() const { return controllerBrokerUrlTls_; }
private:
struct BrokerAddress {
std::string url;
std::optional<std::string> urlTls;
};
std::uint64_t epoch_ = 0;
std::vector<Segment> activeSegments_;
std::vector<Segment> sealedSegments_;
std::unordered_map<std::uint64_t, BrokerAddress> segmentBrokers_;
std::optional<std::string> controllerBrokerUrl_;
std::optional<std::string> controllerBrokerUrlTls_;
};
/**
* Routes messages to segments.
*
* Keyed messages route by the 16-bit segment hash to the active segment whose
* range contains it. If EVERY active segment is a legacy segment (synthetic
* layout for a not-yet-migrated regular topic), routing switches to
* signSafeMod(classicMurmur3(key), N) over segment_id, so producers using this
* client route exactly like the classic partitioned-topic producers still
* attached to the same topic. Keyless messages route round-robin.
*/
class SegmentRouter {
public:
/**
* Route a keyed message.
*
* @return the owning segment's id, or an Error if the layout has no active
* segments (ResultServiceUnitNotReady) or no active segment covers
* the hash — a malformed DAG (ResultUnknownError).
*/
Expected<std::uint64_t> route(std::string_view key, const SegmentLayout& layout);
/** Route a keyless message round-robin across the active segments. */
Expected<std::uint64_t> routeRoundRobin(const SegmentLayout& layout);
private:
std::atomic<std::uint32_t> roundRobinCounter_{0};
};
} // namespace pulsar::st