chore: preparation step for lock fingerprints (#2899)

The main change here is introduction of the strong type LockTag
that differentiates from a string_view key.

Also, some testing improvements to improve the footprint of the next PR.

Signed-off-by: Roman Gershman <roman@dragonflydb.io>
This commit is contained in:
Roman Gershman 2024-04-16 19:23:50 +03:00 committed by GitHub
parent 4fe00a071e
commit 8030ee96b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 175 additions and 157 deletions

View file

@ -29,8 +29,14 @@ namespace dfly {
namespace heap_size_detail { namespace heap_size_detail {
template <class, class = void> struct has_marked_stackonly : std::false_type {};
template <class T>
struct has_marked_stackonly<T, std::void_t<typename T::is_stackonly>> : std::true_type {};
template <typename T> constexpr bool StackOnlyType() { template <typename T> constexpr bool StackOnlyType() {
return std::is_trivial_v<T> || std::is_same_v<T, std::string_view>; return std::is_trivial_v<T> || std::is_same_v<T, std::string_view> ||
has_marked_stackonly<T>::value;
} }
template <typename T, typename = void> struct has_used_mem : std::false_type {}; template <typename T, typename = void> struct has_used_mem : std::false_type {};

View file

@ -55,32 +55,8 @@ bool ClusterConfig::IsEmulated() {
return cluster_mode == ClusterMode::kEmulatedCluster; return cluster_mode == ClusterMode::kEmulatedCluster;
} }
string_view ClusterConfig::KeyTag(string_view key) {
auto options = KeyLockArgs::GetLockTagOptions();
if (!absl::StartsWith(key, options.prefix)) {
return key;
}
const size_t start = key.find(options.open_locktag);
if (start == key.npos) {
return key;
}
size_t end = start;
for (unsigned i = 0; i <= options.skip_n_end_delimiters; ++i) {
size_t next = end + 1;
end = key.find(options.close_locktag, next);
if (end == key.npos || end == next) {
return key;
}
}
return key.substr(start + 1, end - start - 1);
}
SlotId ClusterConfig::KeySlot(string_view key) { SlotId ClusterConfig::KeySlot(string_view key) {
string_view tag = KeyTag(key); string_view tag = LockTagOptions::instance().Tag(key);
return crc16(tag.data(), tag.length()) & kMaxSlotNum; return crc16(tag.data(), tag.length()) & kMaxSlotNum;
} }

View file

@ -64,12 +64,9 @@ class ClusterConfig {
} }
static bool IsShardedByTag() { static bool IsShardedByTag() {
return IsEnabledOrEmulated() || KeyLockArgs::GetLockTagOptions().enabled; return IsEnabledOrEmulated() || LockTagOptions::instance().enabled;
} }
// If the key contains the {...} pattern, return only the part between { and }
static std::string_view KeyTag(std::string_view key);
// Returns an instance with `config` if it is valid. // Returns an instance with `config` if it is valid.
// Returns heap-allocated object as it is too big for a stack frame. // Returns heap-allocated object as it is too big for a stack frame.
static std::shared_ptr<ClusterConfig> CreateFromConfig(std::string_view my_id, static std::shared_ptr<ClusterConfig> CreateFromConfig(std::string_view my_id,

View file

@ -27,43 +27,47 @@ class ClusterConfigTest : public BaseFamilyTest {
const string kMyId = "my-id"; const string kMyId = "my-id";
}; };
inline string_view GetTag(string_view key) {
return LockTagOptions::instance().Tag(key);
}
TEST_F(ClusterConfigTest, KeyTagTest) { TEST_F(ClusterConfigTest, KeyTagTest) {
SetTestFlag("lock_on_hashtags", "true"); SetTestFlag("lock_on_hashtags", "true");
EXPECT_EQ(ClusterConfig::KeyTag("{user1000}.following"), "user1000"); EXPECT_EQ(GetTag("{user1000}.following"), "user1000");
EXPECT_EQ(ClusterConfig::KeyTag("foo{{bar}}zap"), "{bar"); EXPECT_EQ(GetTag("foo{{bar}}zap"), "{bar");
EXPECT_EQ(ClusterConfig::KeyTag("foo{bar}{zap}"), "bar"); EXPECT_EQ(GetTag("foo{bar}{zap}"), "bar");
string_view key = " foo{}{bar}"; string_view key = " foo{}{bar}";
EXPECT_EQ(key, ClusterConfig::KeyTag(key)); EXPECT_EQ(key, GetTag(key));
key = "{}foo{bar}{zap}"; key = "{}foo{bar}{zap}";
EXPECT_EQ(key, ClusterConfig::KeyTag(key)); EXPECT_EQ(key, GetTag(key));
SetTestFlag("locktag_delimiter", ":"); SetTestFlag("locktag_delimiter", ":");
TEST_InvalidateLockTagOptions(); TEST_InvalidateLockTagOptions();
key = "{user1000}.following"; key = "{user1000}.following";
EXPECT_EQ(ClusterConfig::KeyTag(key), key); EXPECT_EQ(GetTag(key), key);
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue1:123"), "queue1"); EXPECT_EQ(GetTag("bull:queue1:123"), "queue1");
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:1:123"), "queue"); EXPECT_EQ(GetTag("bull:queue:1:123"), "queue");
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:1:123:456:789:1000"), "queue"); EXPECT_EQ(GetTag("bull:queue:1:123:456:789:1000"), "queue");
key = "bull::queue:1:123"; key = "bull::queue:1:123";
EXPECT_EQ(ClusterConfig::KeyTag(key), key); EXPECT_EQ(GetTag(key), key);
SetTestFlag("locktag_delimiter", ":"); SetTestFlag("locktag_delimiter", ":");
SetTestFlag("locktag_skip_n_end_delimiters", "0"); SetTestFlag("locktag_skip_n_end_delimiters", "0");
SetTestFlag("locktag_prefix", "bull"); SetTestFlag("locktag_prefix", "bull");
TEST_InvalidateLockTagOptions(); TEST_InvalidateLockTagOptions();
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:123"), "queue"); EXPECT_EQ(GetTag("bull:queue:123"), "queue");
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:123:456:789:1000"), "queue"); EXPECT_EQ(GetTag("bull:queue:123:456:789:1000"), "queue");
key = "not-bull:queue1:123"; key = "not-bull:queue1:123";
EXPECT_EQ(ClusterConfig::KeyTag(key), key); EXPECT_EQ(GetTag(key), key);
SetTestFlag("locktag_delimiter", ":"); SetTestFlag("locktag_delimiter", ":");
SetTestFlag("locktag_skip_n_end_delimiters", "1"); SetTestFlag("locktag_skip_n_end_delimiters", "1");
@ -71,19 +75,19 @@ TEST_F(ClusterConfigTest, KeyTagTest) {
TEST_InvalidateLockTagOptions(); TEST_InvalidateLockTagOptions();
key = "bull:queue1:123"; key = "bull:queue1:123";
EXPECT_EQ(ClusterConfig::KeyTag(key), key); EXPECT_EQ(GetTag(key), key);
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:1:123"), "queue:1"); EXPECT_EQ(GetTag("bull:queue:1:123"), "queue:1");
EXPECT_EQ(ClusterConfig::KeyTag("bull:queue:1:123:456:789:1000"), "queue:1"); EXPECT_EQ(GetTag("bull:queue:1:123:456:789:1000"), "queue:1");
key = "bull::queue:1:123"; key = "bull::queue:1:123";
EXPECT_EQ(ClusterConfig::KeyTag(key), key); EXPECT_EQ(GetTag(key), key);
SetTestFlag("locktag_delimiter", "|"); SetTestFlag("locktag_delimiter", "|");
SetTestFlag("locktag_skip_n_end_delimiters", "2"); SetTestFlag("locktag_skip_n_end_delimiters", "2");
SetTestFlag("locktag_prefix", ""); SetTestFlag("locktag_prefix", "");
TEST_InvalidateLockTagOptions(); TEST_InvalidateLockTagOptions();
EXPECT_EQ(ClusterConfig::KeyTag("|a|b|c|d|e"), "a|b|c"); EXPECT_EQ(GetTag("|a|b|c|d|e"), "a|b|c");
} }
TEST_F(ClusterConfigTest, ConfigSetInvalidEmpty) { TEST_F(ClusterConfigTest, ConfigSetInvalidEmpty) {

View file

@ -52,8 +52,10 @@ using namespace std;
using namespace util; using namespace util;
namespace { namespace {
// Thread-local cache with static linkage. // Thread-local cache with static linkage.
thread_local std::optional<LockTagOptions> locktag_lock_options; thread_local std::optional<LockTagOptions> locktag_lock_options;
} // namespace } // namespace
void TEST_InvalidateLockTagOptions() { void TEST_InvalidateLockTagOptions() {
@ -63,7 +65,7 @@ void TEST_InvalidateLockTagOptions() {
[](ShardId shard, ProactorBase* proactor) { locktag_lock_options = nullopt; }); [](ShardId shard, ProactorBase* proactor) { locktag_lock_options = nullopt; });
} }
/* static */ LockTagOptions KeyLockArgs::GetLockTagOptions() { const LockTagOptions& LockTagOptions::instance() {
if (!locktag_lock_options.has_value()) { if (!locktag_lock_options.has_value()) {
string delimiter = absl::GetFlag(FLAGS_locktag_delimiter); string delimiter = absl::GetFlag(FLAGS_locktag_delimiter);
if (delimiter.empty()) { if (delimiter.empty()) {
@ -87,12 +89,26 @@ void TEST_InvalidateLockTagOptions() {
return *locktag_lock_options; return *locktag_lock_options;
} }
string_view KeyLockArgs::GetLockKey(string_view key) { std::string_view LockTagOptions::Tag(std::string_view key) const {
if (GetLockTagOptions().enabled) { if (!absl::StartsWith(key, prefix)) {
return ClusterConfig::KeyTag(key); return key;
} }
return key; const size_t start = key.find(open_locktag);
if (start == key.npos) {
return key;
}
size_t end = start;
for (unsigned i = 0; i <= skip_n_end_delimiters; ++i) {
size_t next = end + 1;
end = key.find(close_locktag, next);
if (end == key.npos || end == next) {
return key;
}
}
return key.substr(start + 1, end - start - 1);
} }
atomic_uint64_t used_mem_peak(0); atomic_uint64_t used_mem_peak(0);
@ -455,4 +471,15 @@ std::ostream& operator<<(std::ostream& os, ArgSlice list) {
return os << "]"; return os << "]";
} }
LockTag::LockTag(std::string_view key) {
if (LockTagOptions::instance().enabled)
str_ = LockTagOptions::instance().Tag(key);
else
str_ = key;
}
LockFp LockTag::Fingerprint() const {
return XXH64(str_.data(), str_.size(), 0x1C69B3F74AC4AE35UL);
}
} // namespace dfly } // namespace dfly

View file

@ -48,6 +48,7 @@ using RdbTypeFreqMap = absl::flat_hash_map<unsigned, size_t>;
constexpr DbIndex kInvalidDbId = DbIndex(-1); constexpr DbIndex kInvalidDbId = DbIndex(-1);
constexpr ShardId kInvalidSid = ShardId(-1); constexpr ShardId kInvalidSid = ShardId(-1);
constexpr DbIndex kMaxDbId = 1024; // Reasonable starting point. constexpr DbIndex kMaxDbId = 1024; // Reasonable starting point.
using LockFp = uint64_t; // a key fingerprint used by the LockTable.
class CommandId; class CommandId;
class Transaction; class Transaction;
@ -59,14 +60,14 @@ struct LockTagOptions {
char close_locktag = '}'; char close_locktag = '}';
unsigned skip_n_end_delimiters = 0; unsigned skip_n_end_delimiters = 0;
std::string prefix; std::string prefix;
// Returns the tag according to the rules defined by this options object.
std::string_view Tag(std::string_view key) const;
static const LockTagOptions& instance();
}; };
struct KeyLockArgs { struct KeyLockArgs {
static LockTagOptions GetLockTagOptions();
// Before acquiring and releasing keys, one must "normalize" them via GetLockKey().
static std::string_view GetLockKey(std::string_view key);
DbIndex db_index = 0; DbIndex db_index = 0;
ArgSlice args; ArgSlice args;
unsigned key_step = 1; unsigned key_step = 1;
@ -117,6 +118,33 @@ struct OpArgs {
} }
}; };
// A strong type for a lock tag. Helps to disambiguide between keys and the parts of the
// keys that are used for locking.
class LockTag {
std::string_view str_;
public:
using is_stackonly = void; // marks that this object does not use heap.
LockTag() = default;
explicit LockTag(std::string_view key);
explicit operator std::string_view() const {
return str_;
}
LockFp Fingerprint() const;
// To make it hashable.
template <typename H> friend H AbslHashValue(H h, const LockTag& tag) {
return H::combine(std::move(h), tag.str_);
}
bool operator==(const LockTag& o) const {
return str_ == o.str_;
}
};
// Record non auto journal command with own txid and dbid. // Record non auto journal command with own txid and dbid.
void RecordJournal(const OpArgs& op_args, std::string_view cmd, ArgSlice args, void RecordJournal(const OpArgs& op_args, std::string_view cmd, ArgSlice args,
uint32_t shard_cnt = 1, bool multi_commands = false); uint32_t shard_cnt = 1, bool multi_commands = false);

View file

@ -236,7 +236,7 @@ size_t ConnectionState::ExecInfo::UsedMemory() const {
} }
size_t ConnectionState::ScriptInfo::UsedMemory() const { size_t ConnectionState::ScriptInfo::UsedMemory() const {
return dfly::HeapSize(keys) + async_cmds_heap_mem; return dfly::HeapSize(lock_tags) + async_cmds_heap_mem;
} }
size_t ConnectionState::SubscribeInfo::UsedMemory() const { size_t ConnectionState::SubscribeInfo::UsedMemory() const {

View file

@ -100,7 +100,7 @@ struct ConnectionState {
struct ScriptInfo { struct ScriptInfo {
size_t UsedMemory() const; size_t UsedMemory() const;
absl::flat_hash_set<std::string_view> keys; // declared keys absl::flat_hash_set<LockTag> lock_tags; // declared tags
size_t async_cmds_heap_mem = 0; // bytes used by async_cmds size_t async_cmds_heap_mem = 0; // bytes used by async_cmds
size_t async_cmds_heap_limit = 0; // max bytes allowed for async_cmds size_t async_cmds_heap_limit = 0; // max bytes allowed for async_cmds

View file

@ -198,7 +198,7 @@ unsigned PrimeEvictionPolicy::Evict(const PrimeTable::HotspotBuckets& eb, PrimeT
string scratch; string scratch;
string_view key = last_slot_it->first.GetSlice(&scratch); string_view key = last_slot_it->first.GetSlice(&scratch);
// do not evict locked keys // do not evict locked keys
if (lt.Find(KeyLockArgs::GetLockKey(key)).has_value()) if (lt.Find(LockTag(key)).has_value())
return 0; return 0;
// log the evicted keys to journal. // log the evicted keys to journal.
@ -998,16 +998,16 @@ bool DbSlice::Acquire(IntentLock::Mode mode, const KeyLockArgs& lock_args) {
bool lock_acquired = true; bool lock_acquired = true;
if (lock_args.args.size() == 1) { if (lock_args.args.size() == 1) {
string_view key = KeyLockArgs::GetLockKey(lock_args.args.front()); LockTag tag(lock_args.args.front());
lock_acquired = lt.Acquire(key, mode); lock_acquired = lt.Acquire(tag, mode);
uniq_keys_ = {key}; // needed only for tests. uniq_keys_ = {string_view(tag)}; // needed only for tests.
} else { } else {
uniq_keys_.clear(); uniq_keys_.clear();
for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) { for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) {
string_view s = KeyLockArgs::GetLockKey(lock_args.args[i]); LockTag tag(lock_args.args[i]);
if (uniq_keys_.insert(s).second) { if (uniq_keys_.insert(string_view(tag)).second) {
lock_acquired &= lt.Acquire(s, mode); lock_acquired &= lt.Acquire(tag, mode);
} }
} }
} }
@ -1018,13 +1018,12 @@ bool DbSlice::Acquire(IntentLock::Mode mode, const KeyLockArgs& lock_args) {
return lock_acquired; return lock_acquired;
} }
void DbSlice::ReleaseNormalized(IntentLock::Mode mode, DbIndex db_index, std::string_view key) { void DbSlice::ReleaseNormalized(IntentLock::Mode mode, DbIndex db_index, LockTag tag) {
DCHECK_EQ(key, KeyLockArgs::GetLockKey(key));
DVLOG(2) << "Release " << IntentLock::ModeName(mode) << " " DVLOG(2) << "Release " << IntentLock::ModeName(mode) << " "
<< " for " << key; << " for " << string_view(tag);
auto& lt = db_arr_[db_index]->trans_locks; auto& lt = db_arr_[db_index]->trans_locks;
lt.Release(KeyLockArgs::GetLockKey(key), mode); lt.Release(tag, mode);
} }
void DbSlice::Release(IntentLock::Mode mode, const KeyLockArgs& lock_args) { void DbSlice::Release(IntentLock::Mode mode, const KeyLockArgs& lock_args) {
@ -1034,15 +1033,15 @@ void DbSlice::Release(IntentLock::Mode mode, const KeyLockArgs& lock_args) {
DVLOG(2) << "Release " << IntentLock::ModeName(mode) << " for " << lock_args.args[0]; DVLOG(2) << "Release " << IntentLock::ModeName(mode) << " for " << lock_args.args[0];
if (lock_args.args.size() == 1) { if (lock_args.args.size() == 1) {
string_view key = KeyLockArgs::GetLockKey(lock_args.args.front()); string_view key = lock_args.args.front();
ReleaseNormalized(mode, lock_args.db_index, key); ReleaseNormalized(mode, lock_args.db_index, LockTag{key});
} else { } else {
auto& lt = db_arr_[lock_args.db_index]->trans_locks; auto& lt = db_arr_[lock_args.db_index]->trans_locks;
uniq_keys_.clear(); uniq_keys_.clear();
for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) { for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) {
string_view s = KeyLockArgs::GetLockKey(lock_args.args[i]); LockTag tag(lock_args.args[i]);
if (uniq_keys_.insert(s).second) { if (uniq_keys_.insert(string_view(tag)).second) {
lt.Release(s, mode); lt.Release(tag, mode);
} }
} }
} }
@ -1051,9 +1050,9 @@ void DbSlice::Release(IntentLock::Mode mode, const KeyLockArgs& lock_args) {
bool DbSlice::CheckLock(IntentLock::Mode mode, DbIndex dbid, string_view key) const { bool DbSlice::CheckLock(IntentLock::Mode mode, DbIndex dbid, string_view key) const {
const auto& lt = db_arr_[dbid]->trans_locks; const auto& lt = db_arr_[dbid]->trans_locks;
string_view s = KeyLockArgs::GetLockKey(key); LockTag tag(key);
auto lock = lt.Find(s); auto lock = lt.Find(tag);
if (lock) { if (lock) {
return lock->Check(mode); return lock->Check(mode);
} }
@ -1322,7 +1321,7 @@ void DbSlice::FreeMemWithEvictionStep(DbIndex db_ind, size_t increase_goal_bytes
// check if the key is locked by looking up transaction table. // check if the key is locked by looking up transaction table.
const auto& lt = db_table->trans_locks; const auto& lt = db_table->trans_locks;
string_view key = evict_it->first.GetSlice(&tmp); string_view key = evict_it->first.GetSlice(&tmp);
if (lt.Find(KeyLockArgs::GetLockKey(key)).has_value()) if (lt.Find(LockTag(key)).has_value())
continue; continue;
if (auto journal = owner_->journal(); journal) { if (auto journal = owner_->journal(); journal) {

View file

@ -477,8 +477,8 @@ class DbSlice {
void PerformDeletion(Iterator del_it, DbTable* table); void PerformDeletion(Iterator del_it, DbTable* table);
void PerformDeletion(PrimeIterator del_it, DbTable* table); void PerformDeletion(PrimeIterator del_it, DbTable* table);
// Releases a single key. `key` must have been normalized by GetLockKey(). // Releases a single tag.
void ReleaseNormalized(IntentLock::Mode m, DbIndex db_index, std::string_view key); void ReleaseNormalized(IntentLock::Mode m, DbIndex db_index, LockTag tag);
private: private:
void PreUpdate(DbIndex db_ind, Iterator it); void PreUpdate(DbIndex db_ind, Iterator it);

View file

@ -173,21 +173,19 @@ class RoundRobinSharder {
static fb2::Mutex mutex_; static fb2::Mutex mutex_;
}; };
bool HasContendedLocks(unsigned shard_id, Transaction* trx, const DbTable* table) { bool HasContendedLocks(ShardId shard_id, Transaction* trx, const DbTable* table) {
auto is_contended = [table](string_view key) { auto is_contended = [table](LockTag tag) { return table->trans_locks.Find(tag)->IsContended(); };
return table->trans_locks.Find(key)->IsContended();
};
if (trx->IsMulti()) { if (trx->IsMulti()) {
auto keys = trx->GetMultiKeys(); auto keys = trx->GetMultiKeys();
for (string_view key : keys) { for (string_view key : keys) {
if (Shard(key, shard_set->size()) == shard_id && is_contended(key)) if (Shard(key, shard_set->size()) == shard_id && is_contended(LockTag{key}))
return true; return true;
} }
} else { } else {
KeyLockArgs lock_args = trx->GetLockArgs(shard_id); KeyLockArgs lock_args = trx->GetLockArgs(shard_id);
for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) { for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) {
if (is_contended(KeyLockArgs::GetLockKey(lock_args.args[i]))) if (is_contended(LockTag{lock_args.args[i]}))
return true; return true;
} }
} }
@ -868,7 +866,7 @@ void EngineShardSet::TEST_EnableCacheMode() {
ShardId Shard(string_view v, ShardId shard_num) { ShardId Shard(string_view v, ShardId shard_num) {
if (ClusterConfig::IsShardedByTag()) { if (ClusterConfig::IsShardedByTag()) {
v = ClusterConfig::KeyTag(v); v = LockTagOptions::instance().Tag(v);
} }
XXH64_hash_t hash = XXH64(v.data(), v.size(), 120577240643ULL); XXH64_hash_t hash = XXH64(v.data(), v.size(), 120577240643ULL);

View file

@ -594,7 +594,7 @@ void ClusterHtmlPage(const http::QueryArgs& args, HttpContext* send, ClusterFami
: "Disabled"); : "Disabled");
if (ClusterConfig::IsEnabledOrEmulated()) { if (ClusterConfig::IsEnabledOrEmulated()) {
print_kb("Lock on hashtags", KeyLockArgs::GetLockTagOptions().enabled); print_kb("Lock on hashtags", LockTagOptions::instance().enabled);
} }
if (ClusterConfig::IsEnabled()) { if (ClusterConfig::IsEnabled()) {
@ -945,19 +945,18 @@ OpStatus CheckKeysDeclared(const ConnectionState::ScriptInfo& eval_info, const C
// TODO: Switch to transaction internal locked keys once single hop multi transactions are merged // TODO: Switch to transaction internal locked keys once single hop multi transactions are merged
// const auto& locked_keys = trans->GetMultiKeys(); // const auto& locked_keys = trans->GetMultiKeys();
const auto& locked_keys = eval_info.keys; const auto& locked_tags = eval_info.lock_tags;
const auto& key_index = *key_index_res; const auto& key_index = *key_index_res;
for (unsigned i = key_index.start; i < key_index.end; ++i) { for (unsigned i = key_index.start; i < key_index.end; ++i) {
string_view key = KeyLockArgs::GetLockKey(ArgS(args, i)); LockTag tag{ArgS(args, i)};
if (!locked_keys.contains(key)) { if (!locked_tags.contains(tag)) {
VLOG(1) << "Key " << key << " is not declared for command " << cid->name(); VLOG(1) << "Key " << string_view(tag) << " is not declared for command " << cid->name();
return OpStatus::KEY_NOTFOUND; return OpStatus::KEY_NOTFOUND;
} }
} }
if (key_index.bonus && if (key_index.bonus && !locked_tags.contains(LockTag{ArgS(args, *key_index.bonus)}))
!locked_keys.contains(KeyLockArgs::GetLockKey(ArgS(args, *key_index.bonus))))
return OpStatus::KEY_NOTFOUND; return OpStatus::KEY_NOTFOUND;
return OpStatus::OK; return OpStatus::OK;
@ -1867,7 +1866,7 @@ void Service::EvalInternal(CmdArgList args, const EvalArgs& eval_args, Interpret
// we can do it once during script insertion into script mgr. // we can do it once during script insertion into script mgr.
auto& sinfo = cntx->conn_state.script_info; auto& sinfo = cntx->conn_state.script_info;
sinfo = make_unique<ConnectionState::ScriptInfo>(); sinfo = make_unique<ConnectionState::ScriptInfo>();
sinfo->keys.reserve(eval_args.keys.size()); sinfo->lock_tags.reserve(eval_args.keys.size());
optional<ShardId> sid; optional<ShardId> sid;
@ -1875,7 +1874,7 @@ void Service::EvalInternal(CmdArgList args, const EvalArgs& eval_args, Interpret
for (size_t i = 0; i < eval_args.keys.size(); ++i) { for (size_t i = 0; i < eval_args.keys.size(); ++i) {
string_view key = ArgS(eval_args.keys, i); string_view key = ArgS(eval_args.keys, i);
slot_checker.Add(key); slot_checker.Add(key);
sinfo->keys.insert(KeyLockArgs::GetLockKey(key)); sinfo->lock_tags.insert(LockTag(key));
ShardId cur_sid = Shard(key, shard_count()); ShardId cur_sid = Shard(key, shard_count());
if (i == 0) { if (i == 0) {

View file

@ -791,20 +791,16 @@ TEST_F(StringFamilyTest, SetWithHashtagsNoCluster) {
SetTestFlag("lock_on_hashtags", "false"); SetTestFlag("lock_on_hashtags", "false");
ResetService(); ResetService();
auto fb = ExpectConditionWithSuspension( auto fb = ExpectUsedKeys({"{key}1"});
[&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}1"}; });
EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK"); EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK");
fb.Join(); fb.Join();
EXPECT_FALSE(service_->IsLocked(0, "{key}1")); EXPECT_FALSE(service_->IsLocked(0, "{key}1"));
fb = ExpectConditionWithSuspension( fb = ExpectUsedKeys({"{key}2"});
[&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}2"}; });
EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK"); EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK");
fb.Join(); fb.Join();
fb = ExpectConditionWithSuspension([&]() { fb = ExpectUsedKeys({"{key}1", "{key}2"});
return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}1", "{key}2"};
});
EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2"))); EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2")));
fb.Join(); fb.Join();
EXPECT_NE(1, GetDebugInfo().shards_count); EXPECT_NE(1, GetDebugInfo().shards_count);
@ -815,19 +811,15 @@ TEST_F(StringFamilyTest, SetWithHashtagsWithEmulatedCluster) {
SetTestFlag("lock_on_hashtags", "false"); SetTestFlag("lock_on_hashtags", "false");
ResetService(); ResetService();
auto fb = ExpectConditionWithSuspension( auto fb = ExpectUsedKeys({"{key}1"});
[&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}1"}; });
EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK"); EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK");
fb.Join(); fb.Join();
fb = ExpectConditionWithSuspension( fb = ExpectUsedKeys({"{key}2"});
[&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}2"}; });
EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK"); EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK");
fb.Join(); fb.Join();
fb = ExpectConditionWithSuspension([&]() { fb = ExpectUsedKeys({"{key}1", "{key}2"});
return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}1", "{key}2"};
});
EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2"))); EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2")));
fb.Join(); fb.Join();
EXPECT_EQ(1, GetDebugInfo().shards_count); EXPECT_EQ(1, GetDebugInfo().shards_count);
@ -838,16 +830,15 @@ TEST_F(StringFamilyTest, SetWithHashtagsWithHashtagLock) {
SetTestFlag("lock_on_hashtags", "true"); SetTestFlag("lock_on_hashtags", "true");
ResetService(); ResetService();
auto condition = [&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"key"}; }; auto fb = ExpectUsedKeys({"key"});
auto fb = ExpectConditionWithSuspension(condition);
EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK"); EXPECT_EQ(Run({"set", "{key}1", "val1"}), "OK");
fb.Join(); fb.Join();
fb = ExpectConditionWithSuspension(condition); fb = ExpectUsedKeys({"key"});
EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK"); EXPECT_EQ(Run({"set", "{key}2", "val2"}), "OK");
fb.Join(); fb.Join();
fb = ExpectConditionWithSuspension(condition); fb = ExpectUsedKeys({"key"});
EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2"))); EXPECT_THAT(Run({"mget", "{key}1", "{key}2"}), RespArray(ElementsAre("val1", "val2")));
fb.Join(); fb.Join();
EXPECT_EQ(1, GetDebugInfo().shards_count); EXPECT_EQ(1, GetDebugInfo().shards_count);
@ -858,10 +849,7 @@ TEST_F(StringFamilyTest, MultiSetWithHashtagsDontLockHashtags) {
SetTestFlag("lock_on_hashtags", "false"); SetTestFlag("lock_on_hashtags", "false");
ResetService(); ResetService();
auto condition = [&]() { auto fb = ExpectUsedKeys({"{key}1", "{key}2", "{key}3"});
return GetLastUsedKeys() == absl::flat_hash_set<string>{"{key}1", "{key}2", "{key}3"};
};
auto fb = ExpectConditionWithSuspension(condition);
EXPECT_EQ(Run({"multi"}), "OK"); EXPECT_EQ(Run({"multi"}), "OK");
EXPECT_EQ(Run({"set", "{key}1", "val1"}), "QUEUED"); EXPECT_EQ(Run({"set", "{key}1", "val1"}), "QUEUED");
@ -876,8 +864,7 @@ TEST_F(StringFamilyTest, MultiSetWithHashtagsLockHashtags) {
SetTestFlag("lock_on_hashtags", "true"); SetTestFlag("lock_on_hashtags", "true");
ResetService(); ResetService();
auto condition = [&]() { return GetLastUsedKeys() == absl::flat_hash_set<string>{"key"}; }; auto fb = ExpectUsedKeys({"key"});
auto fb = ExpectConditionWithSuspension(condition);
EXPECT_EQ(Run({"multi"}), "OK"); EXPECT_EQ(Run({"multi"}), "OK");
EXPECT_EQ(Run({"set", "{key}1", "val1"}), "QUEUED"); EXPECT_EQ(Run({"set", "{key}1", "val1"}), "QUEUED");

View file

@ -4,8 +4,6 @@
#include "server/table.h" #include "server/table.h"
#include <xxhash.h>
#include "base/flags.h" #include "base/flags.h"
#include "base/logging.h" #include "base/logging.h"
#include "server/cluster/cluster_config.h" #include "server/cluster/cluster_config.h"
@ -60,36 +58,23 @@ SlotStats& SlotStats::operator+=(const SlotStats& o) {
return *this; return *this;
} }
size_t LockTable::Size() const { std::optional<const IntentLock> LockTable::Find(LockTag tag) const {
return locks_.size(); LockFp fp = tag.Fingerprint();
}
LockFp LockTable::Fingerprint(string_view key) {
return XXH64(key.data(), key.size(), 0x1C69B3F74AC4AE35UL);
}
std::optional<const IntentLock> LockTable::Find(string_view key) const {
DCHECK_EQ(KeyLockArgs::GetLockKey(key), key);
LockFp fp = Fingerprint(key);
if (auto it = locks_.find(fp); it != locks_.end()) if (auto it = locks_.find(fp); it != locks_.end())
return it->second; return it->second;
return std::nullopt; return std::nullopt;
} }
bool LockTable::Acquire(string_view key, IntentLock::Mode mode) { bool LockTable::Acquire(LockTag tag, IntentLock::Mode mode) {
DCHECK_EQ(KeyLockArgs::GetLockKey(key), key); LockFp fp = tag.Fingerprint();
LockFp fp = Fingerprint(key);
auto [it, inserted] = locks_.try_emplace(fp); auto [it, inserted] = locks_.try_emplace(fp);
return it->second.Acquire(mode); return it->second.Acquire(mode);
} }
void LockTable::Release(string_view key, IntentLock::Mode mode) { void LockTable::Release(LockTag tag, IntentLock::Mode mode) {
DCHECK_EQ(KeyLockArgs::GetLockKey(key), key); LockFp fp = tag.Fingerprint();
LockFp fp = Fingerprint(key);
auto it = locks_.find(fp); auto it = locks_.find(fp);
DCHECK(it != locks_.end()) << key; DCHECK(it != locks_.end()) << string_view(tag);
it->second.Release(mode); it->second.Release(mode);
if (it->second.IsFree()) if (it->second.IsFree())

View file

@ -25,7 +25,6 @@ using PrimeValue = detail::PrimeValue;
using PrimeTable = DashTable<PrimeKey, PrimeValue, detail::PrimeTablePolicy>; using PrimeTable = DashTable<PrimeKey, PrimeValue, detail::PrimeTablePolicy>;
using ExpireTable = DashTable<PrimeKey, ExpirePeriod, detail::ExpireTablePolicy>; using ExpireTable = DashTable<PrimeKey, ExpirePeriod, detail::ExpireTablePolicy>;
using LockFp = uint64_t; // a key fingerprint used by the LockTable.
/// Iterators are invalidated when new keys are added to the table or some entries are deleted. /// Iterators are invalidated when new keys are added to the table or some entries are deleted.
/// Iterators are still valid if a different entry in the table was mutated. /// Iterators are still valid if a different entry in the table was mutated.
@ -79,18 +78,17 @@ struct DbTableStats {
DbTableStats& operator+=(const DbTableStats& o); DbTableStats& operator+=(const DbTableStats& o);
}; };
// Table for recording locks that uses string_views where possible. LockTable falls back to // Table for recording locks. Keys used with the lock table should be normalized with LockTag.
// strings for locks that are used by multiple transactions. Keys used with the lock table
// should be normalized with GetLockKey
class LockTable { class LockTable {
public: public:
size_t Size() const; size_t Size() const {
std::optional<const IntentLock> Find(std::string_view key) const; return locks_.size();
}
std::optional<const IntentLock> Find(LockTag tag) const;
std::optional<const IntentLock> Find(LockFp fp) const;
bool Acquire(std::string_view key, IntentLock::Mode mode); bool Acquire(LockTag tag, IntentLock::Mode mode);
void Release(std::string_view key, IntentLock::Mode mode); void Release(LockTag tag, IntentLock::Mode mode);
static LockFp Fingerprint(std::string_view key);
auto begin() const { auto begin() const {
return locks_.cbegin(); return locks_.cbegin();

View file

@ -677,6 +677,19 @@ fb2::Fiber BaseFamilyTest::ExpectConditionWithSuspension(const std::function<boo
return fb; return fb;
} }
util::fb2::Fiber BaseFamilyTest::ExpectUsedKeys(const std::vector<std::string_view>& keys) {
absl::flat_hash_set<string> own_keys;
for (const auto& k : keys) {
own_keys.insert(string(k));
}
auto cb = [=] {
auto last_keys = GetLastUsedKeys();
return last_keys == own_keys;
};
return ExpectConditionWithSuspension(std::move(cb));
}
void BaseFamilyTest::SetTestFlag(string_view flag_name, string_view new_value) { void BaseFamilyTest::SetTestFlag(string_view flag_name, string_view new_value) {
auto* flag = absl::FindCommandLineFlag(flag_name); auto* flag = absl::FindCommandLineFlag(flag_name);
CHECK_NE(flag, nullptr); CHECK_NE(flag, nullptr);

View file

@ -148,6 +148,7 @@ class BaseFamilyTest : public ::testing::Test {
static void ExpectConditionWithinTimeout(const std::function<bool()>& condition, static void ExpectConditionWithinTimeout(const std::function<bool()>& condition,
absl::Duration timeout = absl::Seconds(10)); absl::Duration timeout = absl::Seconds(10));
util::fb2::Fiber ExpectConditionWithSuspension(const std::function<bool()>& condition); util::fb2::Fiber ExpectConditionWithSuspension(const std::function<bool()>& condition);
util::fb2::Fiber ExpectUsedKeys(const std::vector<std::string_view>& keys);
static unsigned NumLocked(); static unsigned NumLocked();

View file

@ -84,7 +84,7 @@ uint16_t trans_id(const Transaction* ptr) {
bool CheckLocks(const DbSlice& db_slice, IntentLock::Mode mode, const KeyLockArgs& lock_args) { bool CheckLocks(const DbSlice& db_slice, IntentLock::Mode mode, const KeyLockArgs& lock_args) {
for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) { for (size_t i = 0; i < lock_args.args.size(); i += lock_args.key_step) {
string_view s = KeyLockArgs::GetLockKey(lock_args.args[i]); string_view s = lock_args.args[i];
if (!db_slice.CheckLock(mode, lock_args.db_index, s)) if (!db_slice.CheckLock(mode, lock_args.db_index, s))
return false; return false;
} }
@ -262,7 +262,7 @@ void Transaction::LaunderKeyStorage(CmdArgVec* keys) {
m_keys.reserve(keys->size()); m_keys.reserve(keys->size());
for (MutableSlice key : *keys) { for (MutableSlice key : *keys) {
string_view key_s = KeyLockArgs::GetLockKey(facade::ToSV(key)); string_view key_s = string_view(LockTag{facade::ToSV(key)});
// Insert copied string view, not original. This is why "try insert" is not allowed // Insert copied string view, not original. This is why "try insert" is not allowed
if (!m_keys_set.contains(key_s)) if (!m_keys_set.contains(key_s))
m_keys_set.insert(m_keys.emplace_back(key_s)); m_keys_set.insert(m_keys.emplace_back(key_s));
@ -633,7 +633,7 @@ bool Transaction::RunInShard(EngineShard* shard, bool txq_ooo) {
if (auto* bcontroller = shard->blocking_controller(); bcontroller) { if (auto* bcontroller = shard->blocking_controller(); bcontroller) {
if (awaked_prerun || was_suspended) { if (awaked_prerun || was_suspended) {
CHECK_EQ(largs.key_step, 1u); CHECK_EQ(largs.key_step, 1u);
bcontroller->FinalizeWatched(largs.args, this); bcontroller->FinalizeWatched(GetShardArgs(idx), this);
} }
// Wake only if no tx queue head is currently running // Wake only if no tx queue head is currently running
@ -1310,7 +1310,7 @@ void Transaction::UnlockMultiShardCb(absl::Span<const std::string_view> sharded_
shard->shard_lock()->Release(IntentLock::EXCLUSIVE); shard->shard_lock()->Release(IntentLock::EXCLUSIVE);
} else { } else {
for (const auto& key : sharded_keys) { for (const auto& key : sharded_keys) {
shard->db_slice().ReleaseNormalized(*multi_->lock_mode, db_index_, key); shard->db_slice().ReleaseNormalized(*multi_->lock_mode, db_index_, LockTag{key});
} }
} }