blob: 75bcfb546089c69f347ce580db0d67206b2f2f37 [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.
*/
#include "paimon/common/utils/dense_hash_table.h"
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <vector>
#include "gtest/gtest.h"
#include "paimon/common/io/byte_array_output_stream.h"
#include "paimon/common/io/memory_segment_output_stream.h"
#include "paimon/common/memory/bytes_utils.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/testing/mock/mock_file_system.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
namespace {
using HashTable = DenseHashTable<uint64_t, int64_t>;
template <typename Key, typename Value>
Result<std::shared_ptr<Bytes>> FlushHashTable(const DenseHashTable<Key, Value>& hash_table,
const std::shared_ptr<MemoryPool>& pool) {
ByteArrayOutputStream output(std::make_unique<MemorySegmentOutputStream>(1024, pool));
PAIMON_RETURN_NOT_OK(hash_table.WriteTo(&output));
PAIMON_ASSIGN_OR_RAISE(PAIMON_UNIQUE_PTR<Bytes> bytes, output.Finish(pool.get()));
std::shared_ptr<Bytes> result = AllocateBytesKeepingPoolAlive(bytes->size(), pool);
std::memcpy(result->data(), bytes->data(), bytes->size());
return result;
}
class FailingOutputStream : public MockOutputStream {
public:
Result<int64_t> Write(const char*, int64_t) override {
return Status::IOError("injected dense hash table write failure");
}
};
} // namespace
TEST(DenseHashTableTest, TestProbingWraparoundAndCapacity) {
// Three entries at 50% occupancy give six buckets. These keys all start at bucket five.
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(3, /*occupancy_pct=*/50, GetDefaultPool()));
ASSERT_EQ(hash_table->Capacity(), 3);
ASSERT_OK(hash_table->Insert(5, 10));
ASSERT_OK(hash_table->Insert(11, 20));
ASSERT_OK(hash_table->Insert(17, 30));
ASSERT_EQ(hash_table->Find(5), 10);
ASSERT_EQ(hash_table->Find(11), 20);
ASSERT_EQ(hash_table->Find(17), 30);
ASSERT_FALSE(hash_table->Find(23));
ASSERT_NOK_WITH_MSG(hash_table->Insert(23, 40), "full");
ASSERT_OK(hash_table->Insert(5, -1));
ASSERT_EQ(hash_table->Find(5), -1);
ASSERT_EQ(hash_table->Size(), 3);
}
TEST(DenseHashTableTest, TestAllKeyAndValueBitPatterns) {
const std::vector<uint64_t> keys = {0,
1,
0xFFFFFFFFFFFFFEFFULL,
0xFFFFFFFFFFFFFDFFULL,
std::numeric_limits<uint64_t>::max(),
uint64_t{1} << 63};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(keys.size(), /*occupancy_pct=*/65, GetDefaultPool()));
for (size_t index = 0; index < keys.size(); ++index) {
ASSERT_FALSE(hash_table->Find(keys[index]));
ASSERT_OK(hash_table->Insert(keys[index], static_cast<int64_t>(index) - 3));
}
for (size_t index = 0; index < keys.size(); ++index) {
ASSERT_EQ(hash_table->Find(keys[index]), static_cast<int64_t>(index) - 3);
}
ASSERT_OK(hash_table->Insert(keys[2], std::numeric_limits<int64_t>::min()));
ASSERT_OK(hash_table->Insert(keys[3], std::numeric_limits<int64_t>::max()));
ASSERT_EQ(hash_table->Find(keys[2]), std::numeric_limits<int64_t>::min());
ASSERT_EQ(hash_table->Find(keys[3]), std::numeric_limits<int64_t>::max());
ASSERT_EQ(hash_table->Size(), keys.size());
ASSERT_NOK(hash_table->Insert(2, 4));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> full,
HashTable::Create(1, /*occupancy_pct=*/65, GetDefaultPool()));
ASSERT_OK(full->Insert(1, 1));
ASSERT_NOK(full->Insert(keys[2], 2));
ASSERT_NOK(full->Insert(keys[3], 3));
ASSERT_FALSE(full->Find(keys[2]));
ASSERT_FALSE(full->Find(keys[3]));
}
TEST(DenseHashTableTest, TestEmptyAndInvalidArguments) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(0, /*occupancy_pct=*/65, GetDefaultPool()));
ASSERT_EQ(hash_table->Size(), 0);
ASSERT_FALSE(hash_table->Find(0));
ASSERT_FALSE(hash_table->Find(0xFFFFFFFFFFFFFEFFULL));
ASSERT_FALSE(hash_table->Find(0xFFFFFFFFFFFFFDFFULL));
ASSERT_NOK_WITH_MSG(hash_table->Insert(0, 0), "full");
ASSERT_NOK(HashTable::Create(1, 65, nullptr));
ASSERT_NOK(HashTable::Create(1, 0, GetDefaultPool()));
ASSERT_NOK(HashTable::Create(1, -1, GetDefaultPool()));
ASSERT_NOK(HashTable::Create(1, 81, GetDefaultPool()));
ASSERT_NOK(HashTable::Create(std::numeric_limits<uint64_t>::max(), 65, GetDefaultPool()));
ASSERT_NOK(HashTable::Create(std::numeric_limits<int64_t>::max(), 80, GetDefaultPool()));
}
TEST(DenseHashTableTest, TestFlushAndReadOnlyMountKeepPoolAlive) {
std::shared_ptr<MemoryPool> pool = GetMemoryPool();
std::weak_ptr<MemoryPool> weak_pool = pool;
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(5000, /*occupancy_pct=*/65, pool));
for (uint64_t key = 0; key < 5000; ++key) {
ASSERT_OK(hash_table->Insert(key, key + 10));
}
ASSERT_EQ(pool->CurrentUsage(), hash_table->MemoryUsage());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> bytes, FlushHashTable(*hash_table, pool));
ASSERT_EQ(bytes->size(), hash_table->MemoryUsage());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> mounted, HashTable::MountForRead(bytes));
hash_table.reset();
bytes.reset();
pool.reset();
ASSERT_FALSE(weak_pool.expired());
ASSERT_EQ(mounted->Size(), 5000);
for (uint64_t key = 0; key < 5000; ++key) {
ASSERT_EQ(mounted->Find(key), static_cast<int64_t>(key + 10));
}
ASSERT_FALSE(mounted->Find(5000));
ASSERT_NOK_WITH_MSG(mounted->Insert(0, 0), "read-only");
mounted.reset();
ASSERT_TRUE(weak_pool.expired());
}
TEST(DenseHashTableTest, TestRejectMalformedBuffers) {
ASSERT_NOK(HashTable::MountForRead(nullptr));
ASSERT_NOK(HashTable::MountForRead(AllocateBytesKeepingPoolAlive(15, GetDefaultPool())));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(1, /*occupancy_pct=*/65, GetDefaultPool()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> bytes,
FlushHashTable(*hash_table, GetDefaultPool()));
const uint64_t zero = 0;
const uint64_t overflow = std::numeric_limits<uint64_t>::max();
std::memcpy(bytes->data(), &zero, sizeof(zero));
ASSERT_NOK_WITH_MSG(HashTable::MountForRead(bytes), "bucket count");
std::memcpy(bytes->data(), &overflow, sizeof(overflow));
ASSERT_NOK_WITH_MSG(HashTable::MountForRead(bytes), "bucket count");
ASSERT_OK_AND_ASSIGN(bytes, FlushHashTable(*hash_table, GetDefaultPool()));
std::memcpy(bytes->data() + sizeof(uint64_t), &overflow, sizeof(overflow));
ASSERT_NOK_WITH_MSG(HashTable::MountForRead(bytes), "key count");
ASSERT_OK_AND_ASSIGN(bytes, FlushHashTable(*hash_table, GetDefaultPool()));
const uint64_t invalid_state = 2;
std::memcpy(bytes->data() + bytes->size() - 32, &invalid_state, sizeof(invalid_state));
ASSERT_NOK_WITH_MSG(HashTable::MountForRead(bytes), "special bucket");
ASSERT_OK_AND_ASSIGN(bytes, FlushHashTable(*hash_table, GetDefaultPool()));
std::shared_ptr<Bytes> truncated =
AllocateBytesKeepingPoolAlive(bytes->size() - 1, GetDefaultPool());
std::memcpy(truncated->data(), bytes->data(), truncated->size());
ASSERT_NOK_WITH_MSG(HashTable::MountForRead(truncated), "length");
}
TEST(DenseHashTableTest, TestWriteErrors) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<HashTable> hash_table,
HashTable::Create(1, /*occupancy_pct=*/65, GetDefaultPool()));
ASSERT_NOK(hash_table->WriteTo(nullptr));
MockOutputStream short_output;
ASSERT_NOK_WITH_MSG(hash_table->WriteTo(&short_output), "short write");
FailingOutputStream failed_output;
ASSERT_NOK_WITH_MSG(hash_table->WriteTo(&failed_output),
"injected dense hash table write failure");
}
} // namespace paimon::test