Files
scylladb/test/manual/row_locker_test.cc
Avi Kivity f3eade2f62 treewide: relicense to ScyllaDB-Source-Available-1.0
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/
2024-12-18 17:45:13 +02:00

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();
});
}