Drop the AGPL license in favor of a source-available license. See the blog post [1] for details. [1] https://www.scylladb.com/2024/12/18/why-were-moving-to-a-source-available-license/
316 lines
14 KiB
C++
316 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2018-present ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
*/
|
|
|
|
#include <seastar/testing/test_case.hh>
|
|
|
|
#include <seastar/core/thread.hh>
|
|
|
|
#include "db/view/row_locking.hh"
|
|
#include "schema/schema_builder.hh"
|
|
#include "dht/i_partitioner.hh"
|
|
|
|
static row_locker::stats row_locker_stats;
|
|
|
|
static schema_ptr make_schema()
|
|
{
|
|
return schema_builder("ks", "cf")
|
|
.with_column("pk", bytes_type, column_kind::partition_key)
|
|
.with_column("ck", bytes_type, column_kind::clustering_key)
|
|
.with_column("s", bytes_type, column_kind::static_column)
|
|
.with_column("r", bytes_type)
|
|
.build();
|
|
}
|
|
|
|
dht::decorated_key make_pk(const schema_ptr& s, const sstring& pk) {
|
|
return dht::decorate_key(*s,
|
|
partition_key::from_single_value(*s, to_bytes(pk)));
|
|
}
|
|
|
|
clustering_key_prefix make_ck(const schema_ptr&s, const sstring& ck) {
|
|
return clustering_key::from_single_value(*s, to_bytes(ck));
|
|
}
|
|
|
|
// Tests for locks that succeed without blocking:
|
|
// Note that these test doesn't check any conditions. Not crashing and
|
|
// not hanging is a success :-)
|
|
|
|
// Test that we can lock a row with an exclusive lock and unlock it.
|
|
// Check that we can do that again (so after releasing the lock, the row is
|
|
// again unlocked).
|
|
// Also check for locking entire partition, and for locking different rows
|
|
// in the same partition.
|
|
SEASTAR_TEST_CASE(test_nonblock_exclusive) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto ck = make_ck(s, "ck1") ;
|
|
auto lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto ignore = [] (auto) { };
|
|
// move out the lock object, thereby releasing the lock
|
|
ignore(std::move(lock));
|
|
// after we released the lock, we can take it again.
|
|
lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
// now do the same, but locking an entire partition. Should
|
|
// be fine after we unlocked the row.
|
|
lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
// After we unlock the partition, we can lock the row
|
|
lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
// Check that we can hold an exclusive lock for two rows in the
|
|
// same partition, and it doesn't hang.
|
|
auto ck2 = make_ck(s, "ck2") ;
|
|
lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto lock2 = rl.lock_ck(pk, ck2, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
ignore(std::move(lock2));
|
|
BOOST_REQUIRE(rl.empty() == true);
|
|
});
|
|
}
|
|
|
|
// Various tests for shared locks which do not need to block:
|
|
// Test that we can lock with a shared lock multiple times.
|
|
// Test that we can share-lock a partition and one of its row (the row can be
|
|
// locked with either exclusive or shared lock).
|
|
SEASTAR_TEST_CASE(test_nonblock_shared) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto ck = make_ck(s, "ck1") ;
|
|
// Check that we can lock the same row multiple times with a shared lock:
|
|
auto lock1 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto lock2 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto lock3 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock1));
|
|
ignore(std::move(lock2));
|
|
ignore(std::move(lock3));
|
|
// Check that after unlocking, we can lock again. Also for exclusive lock:
|
|
lock1 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
lock1 = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
// Same test but for the partition lock level
|
|
lock1 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
lock2 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
lock3 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
ignore(std::move(lock2));
|
|
ignore(std::move(lock3));
|
|
lock1 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
lock1 = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
// Check that we can hold a shared lock for a partition and a row
|
|
// in it concurrently.
|
|
lock1 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
lock2 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
ignore(std::move(lock2));
|
|
// Check that the above is fine also if the row lock is exclusive
|
|
// (the "exclusivity" is only for the row).
|
|
lock1 = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
lock2 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock1));
|
|
ignore(std::move(lock2));
|
|
BOOST_REQUIRE(rl.empty() == true);
|
|
});
|
|
}
|
|
|
|
// Test adding a lot of locks on different rows, and different partitions,
|
|
// concurrently (the previous tests only tried one or two concurrent locks,
|
|
// see we're not limited to that).
|
|
SEASTAR_TEST_CASE(test_nonblock_many) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
std::vector<row_locker::lock_holder> locks;
|
|
constexpr int N = 100;
|
|
constexpr int M = 100;
|
|
for (int i = 0; i < N; i++) {
|
|
if (i % 2) {
|
|
// lock the entire partition
|
|
auto lock = rl.lock_pk(make_pk(s, to_sstring(i)), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
if (i % 4) {
|
|
// drop half of locks immediately, half kept until end.
|
|
locks.push_back(std::move(lock));
|
|
}
|
|
} else {
|
|
// lock M rows, drop half of the locks immediately, keep half
|
|
// until the end.
|
|
for (int j = 0; j < M; j++) {
|
|
auto lock = rl.lock_ck(make_pk(s, to_sstring(i)), make_ck(s, to_sstring(j)), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
if (j % 2) {
|
|
locks.push_back(std::move(lock));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// drop all the locks still held, now
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(locks));
|
|
});
|
|
}
|
|
|
|
static schema_ptr make_alternative_schema()
|
|
{
|
|
return schema_builder("ks", "cf")
|
|
.with_column("pk", bytes_type, column_kind::partition_key)
|
|
.with_column("ck", bytes_type, column_kind::clustering_key)
|
|
.with_column("s0", bytes_type, column_kind::static_column)
|
|
.with_column("s1", bytes_type, column_kind::static_column)
|
|
.with_column("r", bytes_type)
|
|
.with_column("r2", bytes_type)
|
|
.build();
|
|
}
|
|
|
|
// Test schema change and upgrade (nonblocking test)
|
|
SEASTAR_TEST_CASE(test_nonblock_upgrade) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
auto s2 = make_alternative_schema();
|
|
row_locker rl(s);
|
|
auto lock = rl.lock_ck(make_pk(s, "pk1"), make_ck(s, "ck1"), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
rl.upgrade(s2);
|
|
// verify that the row_locker does not not keep a reference to s any
|
|
// more, so the only remaining reference is ours.
|
|
BOOST_REQUIRE(s.use_count() == 1);
|
|
lock = rl.lock_ck(make_pk(s2, "pk1"), make_ck(s2, "ck1"), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
// Same test, but upgrade the schema while a lock is still taken
|
|
lock = rl.lock_ck(make_pk(s2, "pk1"), make_ck(s2, "ck1"), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
rl.upgrade(s);
|
|
BOOST_REQUIRE(s2.use_count() == 1);
|
|
ignore(std::move(lock));
|
|
lock = rl.lock_ck(make_pk(s, "pk1"), make_ck(s, "ck1"), true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
ignore(std::move(lock));
|
|
BOOST_REQUIRE(rl.empty() == true);
|
|
});
|
|
}
|
|
|
|
// Test for blocking cases of row_locker, e.g., trying to take the same lock
|
|
// twice with an exclusive lock, trying an exclusive lock together with a
|
|
// shared lock, trying to exclusively lock a partition while any lock is
|
|
// taken on a row, etc.
|
|
|
|
// Trying to lock the same row a second time with an exclusive lock should
|
|
// block until the first lock is released.
|
|
SEASTAR_TEST_CASE(test_block_exclusive_twice_row) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto ck = make_ck(s, "ck1") ;
|
|
auto lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
// If we try to lock again *cannot* be ready now. It will
|
|
// become ready (and get() won't hang) when we drop
|
|
// the first lock
|
|
auto flock1 = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
});
|
|
}
|
|
// Trying to lock the same partition a second time with an exclusive lock should
|
|
// block until the first lock is released.
|
|
SEASTAR_TEST_CASE(test_block_exclusive_twice_partition) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto flock1 = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
});
|
|
}
|
|
// Trying to shared lock together with an exclusive lock (in either order)
|
|
// should block the second lock until the first one is released.
|
|
SEASTAR_TEST_CASE(test_block_exclusive_and_shared_row) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto ck = make_ck(s, "ck1") ;
|
|
// shared lock first, exclusive lock second:
|
|
auto lock = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto flock1 = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
// exclusive lock first, shared lock second
|
|
lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
flock1 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
});
|
|
}
|
|
SEASTAR_TEST_CASE(test_block_exclusive_and_shared_partition) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto lock = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto flock1 = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
flock1 = rl.lock_pk(pk, false, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
});
|
|
}
|
|
// Trying to lock the a row either exclusive or shared while its partition
|
|
// is locked exclusive should block. And also in opposite order.
|
|
SEASTAR_TEST_CASE(test_block_partition_row) {
|
|
return seastar::async([&] {
|
|
auto s = make_schema();
|
|
row_locker rl(s);
|
|
auto pk = make_pk(s, "pk1");
|
|
auto ck = make_ck(s, "ck1") ;
|
|
auto lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
auto flock1 = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats); // try exclusive row lock
|
|
BOOST_REQUIRE(!flock1.available());
|
|
auto ignore = [] (auto) { };
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
lock = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
flock1 = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats); // also try shared row lock
|
|
BOOST_REQUIRE(!flock1.available());
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
// Now try the same in opposite order (the row lock first, then the
|
|
// partition lock).
|
|
lock = rl.lock_ck(pk, ck, true, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
flock1 = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
lock = rl.lock_ck(pk, ck, false, db::timeout_clock::time_point::max(), row_locker_stats).get();
|
|
flock1 = rl.lock_pk(pk, true, db::timeout_clock::time_point::max(), row_locker_stats);
|
|
BOOST_REQUIRE(!flock1.available());
|
|
ignore(std::move(lock));
|
|
flock1.get();
|
|
});
|
|
}
|