blob: d14902a968ab47a6861dd8c87848f501ce995712 [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 <limits.h>
#include <unistd.h>
#include <cstdlib>
#include <thread>
#include <vector>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include "butil/time.h"
#include "bthread/parking_lot.h"
namespace {
using bthread::ParkingLot;
const int64_t kTimeoutUs = 5 * 1000 * 1000;
template <typename Predicate>
bool wait_until(Predicate predicate) {
const int64_t deadline_us = butil::cpuwide_time_us() + kTimeoutUs;
int spins = 0;
while (!predicate()) {
if (butil::cpuwide_time_us() >= deadline_us) {
return false;
}
// Keep short handshakes responsive so the signaler can race with waiter
// registration instead of usually arriving after the waiter has slept.
if (++spins < 256) {
std::this_thread::yield();
} else {
usleep(50);
}
}
return true;
}
// ParkingLot blocks OS threads, so use native threads rather than bthreads.
// All assertions are made before cleanup: rescue wakeups must not make a lost
// wakeup test pass. The guard also cleans up on a fatal assertion/early return.
class ParkingLotThreads {
public:
explicit ParkingLotThreads(ParkingLot* lot)
: _lot(lot), _cancelled(false), _finished(0) {}
~ParkingLotThreads() {
_cancelled.store(true, butil::memory_order_release);
const bool joined = wait_until([this] {
if (_finished.load(butil::memory_order_acquire) == _threads.size()) {
return true;
}
// Do not depend on signal() or stop() working to rescue a failure.
// Changing the value also rescues a thread not yet in futex_wait.
_lot->_pending_signal.fetch_add(2, butil::memory_order_release);
bthread::futex_wake_private(&_lot->_pending_signal, INT_MAX);
return false;
});
if (!joined) {
ADD_FAILURE() << "ParkingLot test threads could not be rescued";
// Never detach threads referencing stack objects or hang the suite.
std::abort();
}
for (auto& thread : _threads) {
thread.join();
}
}
template <typename Function>
void start(Function function) {
_threads.emplace_back([this, function] {
function();
_finished.fetch_add(1, butil::memory_order_release);
});
}
bool cancelled() const {
return _cancelled.load(butil::memory_order_acquire);
}
bool finished() const {
return _finished.load(butil::memory_order_acquire) == _threads.size();
}
private:
ParkingLot* _lot;
butil::atomic<bool> _cancelled;
butil::atomic<size_t> _finished;
std::vector<std::thread> _threads;
};
class ParkingLotTest : public ::testing::TestWithParam<bool> {
protected:
void SetUp() override {
bthread::FLAGS_parking_lot_no_signal_when_no_waiter = GetParam();
}
// Test builds use -fno-access-control. Inspect state and waiter bookkeeping
// without adding test-only hooks to the production synchronization path.
void check_wake_count(int num_waiters, int num_task, int expected_woken) {
ParkingLot lot;
butil::atomic<int> ready(0);
butil::atomic<int> returned(0);
ParkingLotThreads threads(&lot);
for (int i = 0; i < num_waiters; ++i) {
threads.start([&] {
ready.fetch_add(1, butil::memory_order_release);
while (!threads.cancelled()) {
const auto state = lot.get_state();
lot.wait(state);
returned.fetch_add(1, butil::memory_order_release);
}
});
}
ASSERT_TRUE(wait_until([&] { return ready.load() == num_waiters; }));
if (GetParam()) {
ASSERT_TRUE(wait_until([&] {
return lot._waiter_num.load() == num_waiters;
}));
}
// Neither ready nor _waiter_num proves that the kernel has enqueued a
// waiter. Retry until the actual futex return value confirms the desired
// batch. This avoids assuming that a sleep is a scheduling barrier.
int nwoken = -1;
ASSERT_TRUE(wait_until([&] {
nwoken = lot.signal(num_task);
return nwoken == expected_woken || nwoken < 0 ||
nwoken > expected_woken;
}));
ASSERT_EQ(expected_woken, nwoken);
ASSERT_TRUE(wait_until([&] { return returned.load() >= nwoken; }));
}
private:
GFLAGS_NAMESPACE::FlagSaver _flag_saver;
};
TEST_P(ParkingLotTest, initial_state) {
ParkingLot::State state;
ASSERT_EQ(0, state.val);
ASSERT_FALSE(state.stopped());
ParkingLot lot;
ASSERT_EQ(0, lot.get_state().val);
ASSERT_FALSE(lot.get_state().stopped());
ASSERT_EQ(0, lot._waiter_num.load());
ASSERT_EQ(GetParam(), lot._no_signal_when_no_waiter);
}
TEST_P(ParkingLotTest, configuration_is_captured_at_construction) {
ParkingLot first;
bthread::FLAGS_parking_lot_no_signal_when_no_waiter = !GetParam();
ParkingLot second;
ASSERT_EQ(GetParam(), first._no_signal_when_no_waiter);
ASSERT_EQ(!GetParam(), second._no_signal_when_no_waiter);
ASSERT_EQ(0, first.signal(1));
ASSERT_EQ(0, second.signal(1));
ASSERT_EQ(2, first.get_state().val);
ASSERT_EQ(2, second.get_state().val);
}
TEST_P(ParkingLotTest, signal_without_waiters_updates_state) {
ParkingLot lot;
const auto initial = lot.get_state();
ASSERT_EQ(0, lot.signal(1));
const auto first = lot.get_state();
ASSERT_EQ(2, first.val);
ASSERT_EQ(0, lot.signal(3));
ASSERT_EQ(8, lot.get_state().val);
ASSERT_EQ(0, lot.signal(0));
ASSERT_EQ(8, lot.get_state().val);
ASSERT_FALSE(lot.get_state().stopped());
// Snapshots are values, not references to the live signal counter.
ASSERT_EQ(0, initial.val);
ASSERT_EQ(2, first.val);
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, stale_state_does_not_wait_or_consume_signals) {
ParkingLot lot;
const auto initial = lot.get_state();
lot.signal(1);
const auto first = lot.get_state();
lot.signal(2);
ParkingLotThreads threads(&lot);
threads.start([&] {
for (int i = 0; i < 100 && !threads.cancelled(); ++i) {
lot.wait(initial);
lot.wait(first);
}
});
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_EQ(6, lot.get_state().val);
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, current_state_waits_even_after_previous_signals) {
ParkingLot lot;
lot.signal(3);
const auto current = lot.get_state();
butil::atomic<bool> ready(false);
butil::atomic<int> returned(0);
ParkingLotThreads threads(&lot);
threads.start([&] {
ready.store(true, butil::memory_order_release);
// Allow spurious wakeups without mistaking them for a new signal.
while (!threads.cancelled() && lot.get_state().val == current.val) {
lot.wait(current);
}
returned.store(1, butil::memory_order_release);
});
ASSERT_TRUE(wait_until([&] { return ready.load(); }));
if (GetParam()) {
ASSERT_TRUE(wait_until([&] { return lot._waiter_num.load() == 1; }));
}
usleep(10 * 1000);
ASSERT_EQ(0, returned.load());
const int nwoken = lot.signal(1);
ASSERT_GE(nwoken, 0);
ASSERT_LE(nwoken, 1);
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_EQ(1, returned.load());
ASSERT_EQ(8, lot.get_state().val);
ASSERT_EQ(0, lot._waiter_num.load());
ASSERT_EQ(0, lot.signal(1));
}
TEST_P(ParkingLotTest, signal_wakes_one_waiter) {
check_wake_count(1, 1, 1);
}
TEST_P(ParkingLotTest, signal_wakes_a_limited_batch) {
check_wake_count(4, 2, 2);
}
TEST_P(ParkingLotTest, signal_is_limited_by_available_waiters) {
check_wake_count(4, 10, 4);
}
TEST_P(ParkingLotTest, stop_preserves_signal_count_and_is_idempotent) {
ParkingLot lot;
lot.signal(3);
const auto before_stop = lot.get_state();
lot.stop();
const auto stopped = lot.get_state();
ASSERT_TRUE(stopped.stopped());
ASSERT_EQ(7, stopped.val);
lot.stop();
ASSERT_EQ(stopped.val, lot.get_state().val);
ASSERT_EQ(0, lot.signal(2));
ASSERT_EQ(11, lot.get_state().val);
ASSERT_TRUE(lot.get_state().stopped());
ASSERT_FALSE(before_stop.stopped());
ASSERT_EQ(6, before_stop.val);
ASSERT_EQ(7, stopped.val);
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, stop_before_wait_invalidates_old_snapshots) {
ParkingLot lot;
const auto initial = lot.get_state();
lot.signal(2);
const auto before_stop = lot.get_state();
lot.stop();
ParkingLotThreads threads(&lot);
threads.start([&] {
// Callers must check State::stopped() before waiting. Only snapshots
// taken before stop(), not a fresh stopped snapshot, are waitable here.
lot.wait(initial);
lot.wait(before_stop);
});
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_TRUE(lot.get_state().stopped());
ASSERT_EQ(5, lot.get_state().val);
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, stop_wakes_all_waiters) {
const int num_waiters = 8;
ParkingLot lot;
const auto initial = lot.get_state();
butil::atomic<int> ready(0);
ParkingLotThreads threads(&lot);
for (int i = 0; i < num_waiters; ++i) {
threads.start([&] {
ready.fetch_add(1, butil::memory_order_release);
while (!threads.cancelled() && !lot.get_state().stopped()) {
lot.wait(initial);
}
});
}
ASSERT_TRUE(wait_until([&] { return ready.load() == num_waiters; }));
if (GetParam()) {
ASSERT_TRUE(wait_until([&] {
return lot._waiter_num.load() == num_waiters;
}));
}
lot.stop();
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_TRUE(lot.get_state().stopped());
ASSERT_EQ(1, lot.get_state().val);
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, concurrent_signals_are_not_lost) {
const int num_signalers = 4;
const int iterations = 1000;
ParkingLot lot;
butil::atomic<int> bad_returns(0);
ParkingLotThreads threads(&lot);
for (int i = 1; i <= num_signalers; ++i) {
threads.start([&, i] {
for (int j = 0; j < iterations && !threads.cancelled(); ++j) {
if (lot.signal(i) != 0) {
bad_returns.fetch_add(1);
}
}
});
}
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_EQ(0, bad_returns.load());
ASSERT_EQ(iterations * num_signalers * (num_signalers + 1),
lot.get_state().val);
ASSERT_FALSE(lot.get_state().stopped());
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, get_state_acquires_published_data) {
ParkingLot lot;
int payload = 0;
ParkingLotThreads threads(&lot);
threads.start([&] {
payload = 42;
lot.signal(1);
});
// Observe the release in signal() through get_state(), not thread.join()
// or the helper's completion counter, before reading the non-atomic data.
ASSERT_TRUE(wait_until([&] { return lot.get_state().val != 0; }));
ASSERT_EQ(42, payload);
}
TEST_P(ParkingLotTest, signal_counter_wraparound_preserves_stop_bit) {
ParkingLot lot;
// Atomic signed fetch_add wraps without signed-overflow UB. Keep num_task
// small enough that the separate signed left shift in signal() is valid.
ASSERT_EQ(0, lot.signal(INT_MAX / 2));
ASSERT_EQ(INT_MAX - 1, lot.get_state().val);
ASSERT_EQ(0, lot.signal(1));
ASSERT_EQ(INT_MIN, lot.get_state().val);
ASSERT_FALSE(lot.get_state().stopped());
lot.stop();
ASSERT_EQ(INT_MIN + 1, lot.get_state().val);
ASSERT_EQ(0, lot.signal(INT_MAX / 2));
ASSERT_EQ(-1, lot.get_state().val);
ASSERT_EQ(0, lot.signal(1));
ASSERT_EQ(1, lot.get_state().val);
ASSERT_TRUE(lot.get_state().stopped());
}
TEST_P(ParkingLotTest, stop_races_with_signals_and_wait) {
const int iterations = 1000;
ParkingLot lot;
butil::atomic<bool> start(false);
ParkingLotThreads threads(&lot);
threads.start([&] {
while (!threads.cancelled()) {
const auto state = lot.get_state();
if (state.stopped()) {
return;
}
lot.wait(state);
}
});
threads.start([&] {
while (!start.load(butil::memory_order_acquire)) {
if (threads.cancelled()) {
return;
}
std::this_thread::yield();
}
for (int i = 0; i < iterations && !threads.cancelled(); ++i) {
lot.signal(1);
}
});
threads.start([&] {
while (!start.load(butil::memory_order_acquire)) {
if (threads.cancelled()) {
return;
}
std::this_thread::yield();
}
for (int i = 0; i < iterations && !threads.cancelled(); ++i) {
lot.stop();
}
});
start.store(true, butil::memory_order_release);
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_EQ(iterations * 2 + 1, lot.get_state().val);
ASSERT_TRUE(lot.get_state().stopped());
ASSERT_EQ(0, lot._waiter_num.load());
}
TEST_P(ParkingLotTest, racing_signal_and_wait_do_not_lose_wakeups) {
const int iterations = 2000;
ParkingLot lot;
butil::atomic<int> start(0);
butil::atomic<int> ready(0);
butil::atomic<int> completed(0);
ParkingLotThreads threads(&lot);
threads.start([&] {
for (int round = 1; round <= iterations; ++round) {
while (start.load(butil::memory_order_acquire) != round) {
if (threads.cancelled()) {
return;
}
std::this_thread::yield();
}
const auto state = lot.get_state();
ready.store(round, butil::memory_order_release);
if (round % 3 == 0) {
std::this_thread::yield();
}
lot.wait(state);
completed.store(round, butil::memory_order_release);
}
});
for (int round = 1; round <= iterations; ++round) {
SCOPED_TRACE(round);
start.store(round, butil::memory_order_release);
ASSERT_TRUE(wait_until([&] {
return ready.load(butil::memory_order_acquire) == round;
}));
if (round % 3 == 1) {
std::this_thread::yield();
}
// The handshake precedes BOTH the waiter registration and signal().
// Do not wait for _waiter_num here: that would remove the Dekker race.
// Exactly one signal per round; no later signal may rescue this round.
const int nwoken = lot.signal(1);
ASSERT_GE(nwoken, 0);
ASSERT_LE(nwoken, 1);
ASSERT_TRUE(wait_until([&] {
return completed.load(butil::memory_order_acquire) == round;
})) << "Possible lost wakeup";
ASSERT_EQ(0, lot._waiter_num.load());
}
ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
ASSERT_EQ(iterations * 2, lot.get_state().val);
ASSERT_EQ(0, lot.signal(1));
// This is a real-implementation stress regression, not a deterministic
// proof of weak-memory ordering. Removing a fence need not fail on x86.
}
INSTANTIATE_TEST_SUITE_P(WaiterCheckModes, ParkingLotTest, ::testing::Bool());
} // namespace