Skip to content

Commit 5539ebe

Browse files
committed
feat(realtime): add hash-based offset indexes with data file rolling
1 parent fff5a58 commit 5539ebe

28 files changed

Lines changed: 2489 additions & 364 deletions

‎src/paimon/CMakeLists.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ set(PAIMON_CORE_SRCS
295295
core/io/data_file_meta_write_cols_legacy_serializer.cpp
296296
core/io/data_file_path_factory.cpp
297297
core/io/data_file_index_writer.cpp
298+
core/io/data_file_key_offset_index_writer.cpp
299+
core/io/key_offset_index_utils.cpp
298300
core/io/file_index_options.cpp
299301
core/io/append_data_file_writer_factory.cpp
300302
core/io/blob_data_file_writer_factory.cpp
@@ -705,6 +707,9 @@ if(PAIMON_BUILD_TESTS)
705707
common/utils/arrow/mem_utils_test.cpp
706708
common/utils/arrow/status_utils_test.cpp
707709
common/utils/concurrent_hash_map_test.cpp
710+
common/utils/hash_map_test.cpp
711+
common/utils/dense_hash_table_test.cpp
712+
common/utils/dense_hash_table_file_reader_test.cpp
708713
common/utils/projected_row_test.cpp
709714
common/utils/projected_array_test.cpp
710715
common/utils/bit_set_test.cpp
@@ -850,6 +855,7 @@ if(PAIMON_BUILD_TESTS)
850855
core/io/data_file_meta_test.cpp
851856
core/io/data_file_writer_base_test.cpp
852857
core/io/data_file_index_writer_test.cpp
858+
core/io/key_offset_index_utils_test.cpp
853859
core/io/file_index_options_test.cpp
854860
core/io/file_index_evaluator_test.cpp
855861
core/io/single_file_writer_test.cpp
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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+
#pragma once
21+
22+
#include <algorithm>
23+
#include <cstddef>
24+
#include <cstdint>
25+
#include <cstring>
26+
#include <limits>
27+
#include <memory>
28+
#include <optional>
29+
#include <type_traits>
30+
#include <utility>
31+
32+
#include "paimon/common/memory/bytes_utils.h"
33+
#include "paimon/fs/file_system.h"
34+
#include "paimon/macros.h"
35+
#include "paimon/result.h"
36+
37+
namespace paimon {
38+
39+
template <typename Key, typename Value>
40+
class DenseHashTableFileReader;
41+
42+
/// A fixed-capacity, linear-probing hash table for already-hashed unsigned keys.
43+
/// Adapted from:
44+
/// https://github.com/alibaba/havenask/blob/main/aios/storage/indexlib/index/common/hash_table/DenseHashTable.h
45+
/// All key and value bit patterns are supported.
46+
/// Mutation requires external synchronization; mounted hash tables are read-only.
47+
///
48+
/// The native layout is [uint64 bucket_count, uint64 key_count], followed by fixed-size
49+
/// [Key, Value] buckets and two special-key buckets. Integers use host byte order and fields have
50+
/// no padding. Dumped buffers require matching key/value types and endianness when read. This is
51+
/// a container layout, not a versioned Paimon index format.
52+
template <typename Key, typename Value>
53+
class DenseHashTable {
54+
static_assert(std::is_same_v<Key, uint32_t> || std::is_same_v<Key, uint64_t>);
55+
static_assert(std::is_integral_v<Value> && !std::is_same_v<Value, bool>);
56+
57+
public:
58+
static Result<std::unique_ptr<DenseHashTable>> Create(uint64_t capacity, int32_t occupancy_pct,
59+
const std::shared_ptr<MemoryPool>& pool) {
60+
if (!pool) {
61+
return Status::Invalid("dense hash table requires a memory pool");
62+
}
63+
PAIMON_ASSIGN_OR_RAISE(uint64_t bucket_count, BucketCount(capacity, occupancy_pct));
64+
PAIMON_ASSIGN_OR_RAISE(uint64_t bytes, RequiredBytes(bucket_count));
65+
auto hash_table = std::unique_ptr<DenseHashTable>(new DenseHashTable());
66+
hash_table->data_ = AllocateBytesKeepingPoolAlive(bytes, pool);
67+
hash_table->header_ = Header{bucket_count, 0};
68+
hash_table->capacity_ = capacity;
69+
hash_table->writable_ = true;
70+
hash_table->WriteHeader();
71+
for (uint64_t index = 0; index < bucket_count; ++index) {
72+
hash_table->WriteBucket(index, Bucket{kEmptyKey, Value{}});
73+
}
74+
hash_table->WriteBucket(bucket_count, Bucket{0, Value{}});
75+
hash_table->WriteBucket(bucket_count + 1, Bucket{0, Value{}});
76+
return hash_table;
77+
}
78+
79+
/// Mounts a shared buffer without copying. Its bytes must not be mutated while mounted.
80+
/// The returned hash table retains the buffer; its allocator must also outlive the buffer.
81+
static Result<std::unique_ptr<DenseHashTable>> MountForRead(std::shared_ptr<const Bytes> data) {
82+
if (!data || data->size() < kHeaderSize) {
83+
return Status::Invalid("dense hash table buffer is shorter than its header");
84+
}
85+
Header header;
86+
std::memcpy(&header, data->data(), kHeaderSize);
87+
PAIMON_RETURN_NOT_OK(ValidateHeader(header, data->size()));
88+
auto hash_table = std::unique_ptr<DenseHashTable>(new DenseHashTable());
89+
hash_table->data_ = std::move(data);
90+
hash_table->header_ = header;
91+
hash_table->capacity_ = header.bucket_count + 2;
92+
for (uint64_t index = header.bucket_count; index < header.bucket_count + 2; ++index) {
93+
if (hash_table->ReadBucket(index).key > 1) {
94+
return Status::Invalid("invalid dense hash table special bucket");
95+
}
96+
}
97+
return hash_table;
98+
}
99+
100+
DenseHashTable(const DenseHashTable&) = delete;
101+
DenseHashTable& operator=(const DenseHashTable&) = delete;
102+
103+
/// New keys fail once capacity is reached. Updating an existing key still succeeds.
104+
Status Insert(Key key, Value value) {
105+
if (!writable_) {
106+
return Status::Invalid("cannot insert into a read-only dense hash table");
107+
}
108+
if (key == kEmptyKey || key == kOtherSpecialKey) {
109+
const uint64_t index = header_.bucket_count + (key == kOtherSpecialKey);
110+
return SetBucket(index, Bucket{1, value}, ReadBucket(index).key == 0);
111+
}
112+
uint64_t index = key % header_.bucket_count;
113+
for (uint64_t probe = 0; probe < header_.bucket_count; ++probe) {
114+
const Bucket bucket = ReadBucket(index);
115+
if (bucket.key == kEmptyKey || bucket.key == key) {
116+
return SetBucket(index, Bucket{key, value}, bucket.key == kEmptyKey);
117+
}
118+
index = (index + 1) % header_.bucket_count;
119+
}
120+
return Status::Invalid("dense hash table is full");
121+
}
122+
123+
std::optional<Value> Find(Key key) const {
124+
if (key == kEmptyKey || key == kOtherSpecialKey) {
125+
const Bucket bucket = ReadBucket(header_.bucket_count + (key == kOtherSpecialKey));
126+
return bucket.key == 0 ? std::nullopt : std::optional<Value>(bucket.value);
127+
}
128+
uint64_t index = key % header_.bucket_count;
129+
for (uint64_t probe = 0; probe < header_.bucket_count; ++probe) {
130+
const Bucket bucket = ReadBucket(index);
131+
if (bucket.key == kEmptyKey) {
132+
return std::nullopt;
133+
}
134+
if (bucket.key == key) {
135+
return bucket.value;
136+
}
137+
index = (index + 1) % header_.bucket_count;
138+
}
139+
return std::nullopt;
140+
}
141+
142+
/// Writes the container layout at the stream's current position. The caller owns flush/close.
143+
Status WriteTo(OutputStream* output) const {
144+
if (!output) {
145+
return Status::Invalid("dense hash table output stream is null");
146+
}
147+
const int64_t length = static_cast<int64_t>(data_->size());
148+
PAIMON_ASSIGN_OR_RAISE(int64_t written, output->Write(data_->data(), length));
149+
if (written != length) {
150+
return Status::IOError("short write for dense hash table");
151+
}
152+
return Status::OK();
153+
}
154+
155+
uint64_t Size() const {
156+
return header_.key_count;
157+
}
158+
159+
/// Returns the writable key limit, or the physical bucket limit for a read-only mount.
160+
uint64_t Capacity() const {
161+
return capacity_;
162+
}
163+
164+
uint64_t MemoryUsage() const {
165+
return data_->size();
166+
}
167+
168+
private:
169+
friend class DenseHashTableFileReader<Key, Value>;
170+
171+
struct Header {
172+
uint64_t bucket_count;
173+
uint64_t key_count;
174+
};
175+
176+
struct Bucket {
177+
Key key;
178+
Value value;
179+
};
180+
181+
static constexpr Key kEmptyKey = static_cast<Key>(0xFFFFFFFFFFFFFEFFULL);
182+
static constexpr Key kOtherSpecialKey = static_cast<Key>(0xFFFFFFFFFFFFFDFFULL);
183+
static constexpr uint64_t kHeaderSize = sizeof(Header);
184+
static constexpr uint64_t kBucketSize = sizeof(Key) + sizeof(Value);
185+
static constexpr uint64_t kMaxBytes = std::numeric_limits<int64_t>::max();
186+
187+
DenseHashTable() = default;
188+
189+
static Result<uint64_t> BucketCount(uint64_t capacity, int32_t occupancy_pct) {
190+
if (occupancy_pct <= 0 || occupancy_pct > 80) {
191+
return Status::Invalid("dense hash table occupancy must be between 1 and 80");
192+
}
193+
const uint64_t quotient = capacity / occupancy_pct;
194+
if (quotient > kMaxBytes / 100) {
195+
return Status::Invalid("dense hash table capacity overflow");
196+
}
197+
const uint64_t remainder =
198+
(capacity % occupancy_pct * 100 + occupancy_pct - 1) / occupancy_pct;
199+
return std::max<uint64_t>(1, quotient * 100 + remainder);
200+
}
201+
202+
static Result<uint64_t> RequiredBytes(uint64_t bucket_count) {
203+
if (bucket_count == 0 || bucket_count > (kMaxBytes - kHeaderSize) / kBucketSize - 2) {
204+
return Status::Invalid("invalid dense hash table bucket count");
205+
}
206+
return kHeaderSize + (bucket_count + 2) * kBucketSize;
207+
}
208+
209+
static Status ValidateHeader(const Header& header, uint64_t length) {
210+
PAIMON_ASSIGN_OR_RAISE(uint64_t bytes, RequiredBytes(header.bucket_count));
211+
if (header.key_count > header.bucket_count + 2 || bytes != length) {
212+
return Status::Invalid(
213+
"dense hash table header does not match its length or key count");
214+
}
215+
return Status::OK();
216+
}
217+
218+
static Bucket DecodeBucket(const char* data) {
219+
Bucket bucket{};
220+
std::memcpy(&bucket.key, data, sizeof(Key));
221+
std::memcpy(&bucket.value, data + sizeof(Key), sizeof(Value));
222+
return bucket;
223+
}
224+
225+
Bucket ReadBucket(uint64_t index) const {
226+
return DecodeBucket(data_->data() + kHeaderSize + index * kBucketSize);
227+
}
228+
229+
void WriteBucket(uint64_t index, const Bucket& bucket) {
230+
char* data = data_->data() + kHeaderSize + index * kBucketSize;
231+
std::memcpy(data, &bucket.key, sizeof(Key));
232+
std::memcpy(data + sizeof(Key), &bucket.value, sizeof(Value));
233+
}
234+
235+
void WriteHeader() {
236+
std::memcpy(data_->data(), &header_, kHeaderSize);
237+
}
238+
239+
Status SetBucket(uint64_t index, const Bucket& bucket, bool is_new) {
240+
if (is_new && Size() >= capacity_) {
241+
return Status::Invalid("dense hash table is full");
242+
}
243+
WriteBucket(index, bucket);
244+
if (is_new) {
245+
++header_.key_count;
246+
WriteHeader();
247+
}
248+
return Status::OK();
249+
}
250+
251+
std::shared_ptr<const Bytes> data_;
252+
Header header_{};
253+
uint64_t capacity_ = 0;
254+
bool writable_ = false;
255+
};
256+
257+
} // namespace paimon

0 commit comments

Comments
 (0)