blob: efaf921cca000a9bda3d48550b056ea5c9b488f9 [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/factories/singleton.h"
#include <array>
#include <atomic>
#include <cstdint>
#include <thread>
#include <vector>
#include "gtest/gtest.h"
namespace paimon::test {
namespace {
constexpr int32_t kNumThreads = 32;
// Runs `worker(i)` on kNumThreads threads that are all blocked on a shared start
// flag and released at (nearly) the same time, so that they race on the first
// Singleton::GetInstance() publication. Joins all threads before returning.
template <typename Worker>
void RunStorm(const Worker& worker) {
std::atomic<bool> start{false};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int32_t i = 0; i < kNumThreads; ++i) {
threads.emplace_back([&start, &worker, i]() {
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
worker(i);
});
}
start.store(true, std::memory_order_release);
for (auto& thread : threads) {
thread.join();
}
}
// Local to this translation unit, so nothing else in the test binary can have
// instantiated Singleton<FirstPublicationTarget> before this test runs: the storm
// below is guaranteed to race on the *first* publication regardless of link order,
// --gtest_shuffle, or --gtest_filter. GetInstance() is defined in the header, so a
// translation-unit-local type can instantiate it.
class FirstPublicationTarget {
public:
FirstPublicationTarget() {
for (size_t i = 0; i < payload_.size(); ++i) {
payload_[i] = kMagic ^ (i * 0x9E3779B97F4A7C15ULL);
}
}
// The publication race let a reader observe the instance pointer before the
// constructor's stores were visible; this checks every word the ctor wrote.
bool IsFullyConstructed() const {
for (size_t i = 0; i < payload_.size(); ++i) {
if (payload_[i] != (kMagic ^ (i * 0x9E3779B97F4A7C15ULL))) {
return false;
}
}
return true;
}
private:
static constexpr uint64_t kMagic = 0xA5A5F00D12345678ULL;
std::array<uint64_t, 64> payload_{};
};
} // namespace
// Regression gate for the Singleton double-checked-locking publication race: 32
// threads race the first GetInstance() of a type local to this file, so the gate
// cannot silently degrade into exercising only the already-published fast path.
TEST(SingletonTest, TestConcurrentFirstPublication) {
std::array<FirstPublicationTarget*, kNumThreads> instances{};
std::array<bool, kNumThreads> fully_constructed{};
RunStorm([&instances, &fully_constructed](int32_t i) {
instances[i] = Singleton<FirstPublicationTarget>::GetInstance();
fully_constructed[i] = instances[i]->IsFullyConstructed();
});
FirstPublicationTarget* expected = instances[0];
ASSERT_NE(expected, nullptr);
for (int32_t i = 0; i < kNumThreads; ++i) {
ASSERT_EQ(expected, instances[i]);
ASSERT_TRUE(fully_constructed[i]);
}
}
} // namespace paimon::test