mirror of
https://github.com/dragonflydb/dragonfly.git
synced 2025-05-11 18:35:46 +02:00
A simple case where both src and dest keys are located in the same shard. Signed-off-by: Roman Gershman <roman@dragonflydb.io>
1313 lines
38 KiB
C++
1313 lines
38 KiB
C++
// Copyright 2022, DragonflyDB authors. All rights reserved.
|
|
// See LICENSE for licensing terms.
|
|
//
|
|
#include "server/list_family.h"
|
|
|
|
extern "C" {
|
|
#include "redis/object.h"
|
|
#include "redis/sds.h"
|
|
}
|
|
|
|
#include <absl/strings/numbers.h>
|
|
|
|
#include "base/flags.h"
|
|
#include "base/logging.h"
|
|
#include "server/blocking_controller.h"
|
|
#include "server/command_registry.h"
|
|
#include "server/conn_context.h"
|
|
#include "server/container_utils.h"
|
|
#include "server/engine_shard_set.h"
|
|
#include "server/error.h"
|
|
#include "server/server_state.h"
|
|
#include "server/transaction.h"
|
|
|
|
/**
|
|
* The number of entries allowed per internal list node can be specified
|
|
* as a fixed maximum size or a maximum number of elements.
|
|
* For a fixed maximum size, use -5 through -1, meaning:
|
|
* -5: max size: 64 Kb <-- not recommended for normal workloads
|
|
* -4: max size: 32 Kb <-- not recommended
|
|
* -3: max size: 16 Kb <-- probably not recommended
|
|
* -2: max size: 8 Kb <-- good
|
|
* -1: max size: 4 Kb <-- good
|
|
* Positive numbers mean store up to _exactly_ that number of elements
|
|
* per list node.
|
|
* The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
|
|
* but if your use case is unique, adjust the settings as necessary.
|
|
*
|
|
*/
|
|
ABSL_FLAG(int32_t, list_max_listpack_size, -2, "Maximum listpack size, default is 8kb");
|
|
|
|
/**
|
|
* Lists may also be compressed.
|
|
* Compress depth is the number of quicklist listpack nodes from *each* side of
|
|
* the list to *exclude* from compression. The head and tail of the list
|
|
* are always uncompressed for fast push/pop operations. Settings are:
|
|
* 0: disable all list compression
|
|
* 1: depth 1 means "don't start compressing until after 1 node into the list,
|
|
* going from either the head or tail"
|
|
* So: [head]->node->node->...->node->[tail]
|
|
* [head], [tail] will always be uncompressed; inner nodes will compress.
|
|
* 2: [head]->[next]->node->node->...->node->[prev]->[tail]
|
|
* 2 here means: don't compress head or head->next or tail->prev or tail,
|
|
* but compress all nodes between them.
|
|
* 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
|
|
* etc.
|
|
*
|
|
*/
|
|
|
|
ABSL_FLAG(int32_t, list_compress_depth, 0, "Compress depth of the list. Default is no compression");
|
|
|
|
namespace dfly {
|
|
|
|
using namespace std;
|
|
using namespace facade;
|
|
using absl::GetFlag;
|
|
using time_point = Transaction::time_point;
|
|
|
|
namespace {
|
|
|
|
quicklist* GetQL(const PrimeValue& mv) {
|
|
return (quicklist*)mv.RObjPtr();
|
|
}
|
|
|
|
void* listPopSaver(unsigned char* data, size_t sz) {
|
|
return createStringObject((char*)data, sz);
|
|
}
|
|
|
|
string ListPop(ListDir dir, quicklist* ql) {
|
|
long long vlong;
|
|
robj* value = NULL;
|
|
|
|
int ql_where = (dir == ListDir::LEFT) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
|
|
|
|
// Empty list automatically removes the key (see below).
|
|
CHECK_EQ(1,
|
|
quicklistPopCustom(ql, ql_where, (unsigned char**)&value, NULL, &vlong, listPopSaver));
|
|
string res;
|
|
if (value) {
|
|
DCHECK(value->encoding == OBJ_ENCODING_EMBSTR || value->encoding == OBJ_ENCODING_RAW);
|
|
sds s = (sds)(value->ptr);
|
|
res = string{s, sdslen(s)};
|
|
decrRefCount(value);
|
|
} else {
|
|
res = absl::StrCat(vlong);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
bool ElemCompare(const quicklistEntry& entry, string_view elem) {
|
|
if (entry.value) {
|
|
return entry.sz == elem.size() &&
|
|
(entry.sz == 0 || memcmp(entry.value, elem.data(), entry.sz) == 0);
|
|
}
|
|
|
|
absl::AlphaNum an(entry.longval);
|
|
return elem == an.Piece();
|
|
}
|
|
|
|
using FFResult = pair<PrimeKey, unsigned>; // key, argument index.
|
|
|
|
struct ShardFFResult {
|
|
PrimeKey key;
|
|
ShardId sid = kInvalidSid;
|
|
};
|
|
|
|
OpResult<ShardFFResult> FindFirst(Transaction* trans) {
|
|
VLOG(2) << "FindFirst::Find " << trans->DebugId();
|
|
|
|
// Holds Find results: (iterator to a found key, and its index in the passed arguments).
|
|
// See DbSlice::FindFirst for more details.
|
|
// spans all the shards for now.
|
|
std::vector<OpResult<FFResult>> find_res(shard_set->size());
|
|
fill(find_res.begin(), find_res.end(), OpStatus::KEY_NOTFOUND);
|
|
|
|
auto cb = [&find_res](auto* t, EngineShard* shard) {
|
|
auto args = t->ShardArgsInShard(shard->shard_id());
|
|
|
|
OpResult<pair<PrimeIterator, unsigned>> ff_res =
|
|
shard->db_slice().FindFirst(t->db_context(), args);
|
|
|
|
if (ff_res) {
|
|
FFResult ff_result(ff_res->first->first.AsRef(), ff_res->second);
|
|
find_res[shard->shard_id()] = move(ff_result);
|
|
} else {
|
|
find_res[shard->shard_id()] = ff_res.status();
|
|
}
|
|
return OpStatus::OK;
|
|
};
|
|
|
|
trans->Execute(move(cb), false);
|
|
|
|
uint32_t min_arg_indx = UINT32_MAX;
|
|
|
|
ShardFFResult shard_result;
|
|
|
|
for (size_t sid = 0; sid < find_res.size(); ++sid) {
|
|
const auto& fr = find_res[sid];
|
|
auto status = fr.status();
|
|
if (status == OpStatus::KEY_NOTFOUND)
|
|
continue;
|
|
|
|
if (status == OpStatus::WRONG_TYPE) {
|
|
return status;
|
|
}
|
|
|
|
CHECK(fr);
|
|
|
|
const auto& it_pos = fr.value();
|
|
|
|
size_t arg_indx = trans->ReverseArgIndex(sid, it_pos.second);
|
|
if (arg_indx < min_arg_indx) {
|
|
min_arg_indx = arg_indx;
|
|
shard_result.sid = sid;
|
|
|
|
// we do not dereference the key, do not extract the string value, so it it
|
|
// ok to just move it. We can not dereference it due to limitations of SmallString
|
|
// that rely on thread-local data-structure for pointer translation.
|
|
shard_result.key = it_pos.first.AsRef();
|
|
}
|
|
}
|
|
|
|
if (shard_result.sid == kInvalidSid) {
|
|
return OpStatus::KEY_NOTFOUND;
|
|
}
|
|
|
|
return OpResult<ShardFFResult>{move(shard_result)};
|
|
}
|
|
|
|
class BPopper {
|
|
public:
|
|
explicit BPopper(ListDir dir);
|
|
|
|
// Returns WRONG_TYPE, OK.
|
|
// If OK is returned then use result() to fetch the value.
|
|
OpStatus Run(Transaction* t, unsigned msec);
|
|
|
|
// returns (key, value) pair.
|
|
auto result() const {
|
|
return make_pair<string_view, string_view>(key_, value_);
|
|
}
|
|
|
|
private:
|
|
OpStatus Pop(Transaction* t, EngineShard* shard);
|
|
|
|
ListDir dir_;
|
|
|
|
ShardFFResult ff_result_;
|
|
|
|
string key_;
|
|
string value_;
|
|
};
|
|
|
|
class BPopPusher {
|
|
public:
|
|
BPopPusher(string_view pop_key, string_view push_key, ListDir popdir, ListDir pushdir);
|
|
|
|
// Returns WRONG_TYPE, OK.
|
|
// If OK is returned then use result() to fetch the value.
|
|
OpResult<string> Run(Transaction* t, unsigned msec);
|
|
|
|
private:
|
|
OpResult<string> RunSingle(Transaction* t, time_point tp);
|
|
OpResult<string> RunPair(Transaction* t, time_point tp);
|
|
|
|
string_view pop_key_, push_key_;
|
|
ListDir popdir_, pushdir_;
|
|
};
|
|
|
|
BPopper::BPopper(ListDir dir) : dir_(dir) {
|
|
}
|
|
|
|
OpStatus BPopper::Run(Transaction* t, unsigned msec) {
|
|
time_point tp =
|
|
msec ? chrono::steady_clock::now() + chrono::milliseconds(msec) : time_point::max();
|
|
bool is_multi = t->IsMulti();
|
|
t->Schedule();
|
|
|
|
auto* stats = ServerState::tl_connection_stats();
|
|
|
|
OpResult<ShardFFResult> result = FindFirst(t);
|
|
|
|
if (result.status() == OpStatus::KEY_NOTFOUND) {
|
|
if (is_multi) {
|
|
// close transaction and return.
|
|
auto cb = [](Transaction* t, EngineShard* shard) { return OpStatus::OK; };
|
|
t->Execute(std::move(cb), true);
|
|
|
|
return OpStatus::TIMED_OUT;
|
|
}
|
|
|
|
// Block
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
auto keys = t->ShardArgsInShard(shard->shard_id());
|
|
return t->WatchInShard(keys, shard);
|
|
};
|
|
|
|
++stats->num_blocked_clients;
|
|
bool wait_succeeded = t->WaitOnWatch(tp, std::move(cb));
|
|
--stats->num_blocked_clients;
|
|
|
|
if (!wait_succeeded)
|
|
return OpStatus::TIMED_OUT;
|
|
|
|
// Now we have something for sure.
|
|
result = FindFirst(t); // retry - must find something.
|
|
}
|
|
|
|
// We got here
|
|
if (!result) {
|
|
// cleanups, locks removal etc.
|
|
auto cb = [](Transaction* t, EngineShard* shard) { return OpStatus::OK; };
|
|
t->Execute(std::move(cb), true);
|
|
|
|
return result.status();
|
|
}
|
|
|
|
VLOG(1) << "Popping an element " << t->DebugId();
|
|
ff_result_ = move(result.value());
|
|
|
|
auto cb = [this](Transaction* t, EngineShard* shard) { return Pop(t, shard); };
|
|
t->Execute(std::move(cb), true);
|
|
|
|
return OpStatus::OK;
|
|
}
|
|
|
|
OpStatus BPopper::Pop(Transaction* t, EngineShard* shard) {
|
|
if (shard->shard_id() == ff_result_.sid) {
|
|
ff_result_.key.GetString(&key_);
|
|
auto& db_slice = shard->db_slice();
|
|
auto it_res = db_slice.Find(t->db_context(), key_, OBJ_LIST);
|
|
CHECK(it_res); // must exist and must be ok.
|
|
PrimeIterator it = *it_res;
|
|
quicklist* ql = GetQL(it->second);
|
|
|
|
db_slice.PreUpdate(t->db_index(), it);
|
|
value_ = ListPop(dir_, ql);
|
|
db_slice.PostUpdate(t->db_index(), it, key_);
|
|
if (quicklistCount(ql) == 0) {
|
|
CHECK(shard->db_slice().Del(t->db_index(), it));
|
|
}
|
|
}
|
|
|
|
return OpStatus::OK;
|
|
}
|
|
|
|
OpResult<string> OpMoveSingleShard(const OpArgs& op_args, string_view src, string_view dest,
|
|
ListDir src_dir, ListDir dest_dir) {
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
auto src_res = db_slice.Find(op_args.db_cntx, src, OBJ_LIST);
|
|
if (!src_res)
|
|
return src_res.status();
|
|
|
|
PrimeIterator src_it = *src_res;
|
|
quicklist* src_ql = GetQL(src_it->second);
|
|
|
|
if (src == dest) { // simple case.
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, src_it);
|
|
string val = ListPop(src_dir, src_ql);
|
|
|
|
int pos = (dest_dir == ListDir::LEFT) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
|
|
quicklistPush(src_ql, val.data(), val.size(), pos);
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, src_it, src);
|
|
|
|
return val;
|
|
}
|
|
|
|
quicklist* dest_ql = nullptr;
|
|
PrimeIterator dest_it;
|
|
bool new_key = false;
|
|
try {
|
|
tie(dest_it, new_key) = db_slice.AddOrFind(op_args.db_cntx, dest);
|
|
} catch (bad_alloc&) {
|
|
return OpStatus::OUT_OF_MEMORY;
|
|
}
|
|
|
|
if (new_key) {
|
|
robj* obj = createQuicklistObject();
|
|
dest_ql = (quicklist*)obj->ptr;
|
|
quicklistSetOptions(dest_ql, GetFlag(FLAGS_list_max_listpack_size),
|
|
GetFlag(FLAGS_list_compress_depth));
|
|
dest_it->second.ImportRObj(obj);
|
|
|
|
// Insertion of dest could invalidate src_it. Find it again.
|
|
src_it = db_slice.GetTables(op_args.db_cntx.db_index).first->Find(src);
|
|
} else {
|
|
if (dest_it->second.ObjType() != OBJ_LIST)
|
|
return OpStatus::WRONG_TYPE;
|
|
|
|
dest_ql = GetQL(dest_it->second);
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, dest_it);
|
|
}
|
|
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, src_it);
|
|
|
|
string val = ListPop(src_dir, src_ql);
|
|
int pos = (dest_dir == ListDir::LEFT) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
|
|
quicklistPush(dest_ql, val.data(), val.size(), pos);
|
|
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, src_it, src);
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, dest_it, dest, !new_key);
|
|
|
|
if (quicklistCount(src_ql) == 0) {
|
|
CHECK(db_slice.Del(op_args.db_cntx.db_index, src_it));
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
// Read-only peek operation that determines whether the list exists and optionally
|
|
// returns the first from left/right value without popping it from the list.
|
|
OpResult<string> Peek(const OpArgs& op_args, string_view key, ListDir dir, bool fetch) {
|
|
auto it_res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res) {
|
|
return it_res.status();
|
|
}
|
|
|
|
if (!fetch)
|
|
return OpStatus::OK;
|
|
|
|
quicklist* ql = GetQL(it_res.value()->second);
|
|
quicklistEntry entry = container_utils::QLEntry();
|
|
quicklistIter* iter = (dir == ListDir::LEFT) ? quicklistGetIterator(ql, AL_START_HEAD)
|
|
: quicklistGetIterator(ql, AL_START_TAIL);
|
|
CHECK(quicklistNext(iter, &entry));
|
|
quicklistReleaseIterator(iter);
|
|
|
|
if (entry.value)
|
|
return string(reinterpret_cast<char*>(entry.value), entry.sz);
|
|
else
|
|
return absl::StrCat(entry.longval);
|
|
}
|
|
|
|
BPopPusher::BPopPusher(string_view pop_key, string_view push_key, ListDir popdir, ListDir pushdir)
|
|
: pop_key_(pop_key), push_key_(push_key), popdir_(popdir), pushdir_(pushdir) {
|
|
}
|
|
|
|
OpResult<string> BPopPusher::Run(Transaction* t, unsigned msec) {
|
|
time_point tp =
|
|
msec ? chrono::steady_clock::now() + chrono::milliseconds(msec) : time_point::max();
|
|
|
|
t->Schedule();
|
|
|
|
if (t->unique_shard_cnt() == 1) {
|
|
return RunSingle(t, tp);
|
|
}
|
|
|
|
return RunPair(t, tp);
|
|
}
|
|
|
|
OpResult<string> BPopPusher::RunSingle(Transaction* t, time_point tp) {
|
|
OpResult<string> op_res;
|
|
bool is_multi = t->IsMulti();
|
|
auto cb_move = [&](Transaction* t, EngineShard* shard) {
|
|
op_res = OpMoveSingleShard(t->GetOpArgs(shard), pop_key_, push_key_, popdir_, pushdir_);
|
|
return OpStatus::OK;
|
|
};
|
|
t->Execute(cb_move, false);
|
|
|
|
if (is_multi || op_res.status() != OpStatus::KEY_NOTFOUND) {
|
|
if (op_res.status() == OpStatus::KEY_NOTFOUND) {
|
|
op_res = OpStatus::TIMED_OUT;
|
|
}
|
|
auto cb = [](Transaction* t, EngineShard* shard) { return OpStatus::OK; };
|
|
t->Execute(std::move(cb), true);
|
|
return op_res;
|
|
}
|
|
|
|
auto* stats = ServerState::tl_connection_stats();
|
|
auto wcb = [&](Transaction* t, EngineShard* shard) {
|
|
ArgSlice keys{&this->pop_key_, 1};
|
|
return t->WatchInShard(keys, shard);
|
|
};
|
|
|
|
// Block
|
|
++stats->num_blocked_clients;
|
|
|
|
bool wait_succeeded = t->WaitOnWatch(tp, std::move(wcb));
|
|
--stats->num_blocked_clients;
|
|
|
|
if (!wait_succeeded)
|
|
return OpStatus::TIMED_OUT;
|
|
|
|
t->Execute(cb_move, true);
|
|
return op_res;
|
|
}
|
|
|
|
OpResult<string> BPopPusher::RunPair(Transaction* t, time_point tp) {
|
|
return OpStatus::TIMED_OUT;
|
|
}
|
|
|
|
OpResult<uint32_t> OpPush(const OpArgs& op_args, std::string_view key, ListDir dir,
|
|
bool skip_notexist, absl::Span<std::string_view> vals) {
|
|
EngineShard* es = op_args.shard;
|
|
PrimeIterator it;
|
|
bool new_key = false;
|
|
|
|
if (skip_notexist) {
|
|
auto it_res = es->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
it = *it_res;
|
|
} else {
|
|
try {
|
|
tie(it, new_key) = es->db_slice().AddOrFind(op_args.db_cntx, key);
|
|
} catch (bad_alloc&) {
|
|
return OpStatus::OUT_OF_MEMORY;
|
|
}
|
|
}
|
|
|
|
quicklist* ql = nullptr;
|
|
|
|
if (new_key) {
|
|
robj* o = createQuicklistObject();
|
|
ql = (quicklist*)o->ptr;
|
|
quicklistSetOptions(ql, GetFlag(FLAGS_list_max_listpack_size),
|
|
GetFlag(FLAGS_list_compress_depth));
|
|
it->second.ImportRObj(o);
|
|
} else {
|
|
if (it->second.ObjType() != OBJ_LIST)
|
|
return OpStatus::WRONG_TYPE;
|
|
es->db_slice().PreUpdate(op_args.db_cntx.db_index, it);
|
|
ql = GetQL(it->second);
|
|
}
|
|
|
|
// Left push is LIST_HEAD.
|
|
int pos = (dir == ListDir::LEFT) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
|
|
|
|
for (auto v : vals) {
|
|
es->tmp_str1 = sdscpylen(es->tmp_str1, v.data(), v.size());
|
|
quicklistPush(ql, es->tmp_str1, sdslen(es->tmp_str1), pos);
|
|
}
|
|
|
|
if (new_key) {
|
|
if (es->blocking_controller()) {
|
|
string tmp;
|
|
string_view key = it->first.GetSlice(&tmp);
|
|
es->blocking_controller()->AwakeWatched(op_args.db_cntx.db_index, key);
|
|
}
|
|
} else {
|
|
es->db_slice().PostUpdate(op_args.db_cntx.db_index, it, key, true);
|
|
}
|
|
|
|
return quicklistCount(ql);
|
|
}
|
|
|
|
OpResult<StringVec> OpPop(const OpArgs& op_args, string_view key, ListDir dir, uint32_t count,
|
|
bool return_results) {
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
OpResult<PrimeIterator> it_res = db_slice.Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
|
|
PrimeIterator it = *it_res;
|
|
quicklist* ql = GetQL(it->second);
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, it);
|
|
|
|
StringVec res;
|
|
if (quicklistCount(ql) < count) {
|
|
count = quicklistCount(ql);
|
|
}
|
|
res.reserve(count);
|
|
|
|
if (return_results) {
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
res.push_back(ListPop(dir, ql));
|
|
}
|
|
} else {
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
ListPop(dir, ql);
|
|
}
|
|
}
|
|
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, it, key);
|
|
|
|
if (quicklistCount(ql) == 0) {
|
|
CHECK(db_slice.Del(op_args.db_cntx.db_index, it));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
OpResult<uint32_t> OpLen(const OpArgs& op_args, std::string_view key) {
|
|
auto res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!res)
|
|
return res.status();
|
|
|
|
quicklist* ql = GetQL(res.value()->second);
|
|
|
|
return quicklistCount(ql);
|
|
}
|
|
|
|
OpResult<string> OpIndex(const OpArgs& op_args, std::string_view key, long index) {
|
|
auto res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!res)
|
|
return res.status();
|
|
quicklist* ql = GetQL(res.value()->second);
|
|
quicklistEntry entry = container_utils::QLEntry();
|
|
quicklistIter* iter = quicklistGetIteratorAtIdx(ql, AL_START_TAIL, index);
|
|
if (!iter)
|
|
return OpStatus::KEY_NOTFOUND;
|
|
|
|
quicklistNext(iter, &entry);
|
|
string str;
|
|
|
|
if (entry.value) {
|
|
str.assign(reinterpret_cast<char*>(entry.value), entry.sz);
|
|
} else {
|
|
str = absl::StrCat(entry.longval);
|
|
}
|
|
quicklistReleaseIterator(iter);
|
|
|
|
return str;
|
|
}
|
|
|
|
OpResult<vector<uint32_t>> OpPos(const OpArgs& op_args, std::string_view key,
|
|
std::string_view element, int rank, int count, int max_len) {
|
|
OpResult<PrimeIterator> it_res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res.ok())
|
|
return it_res.status();
|
|
|
|
int direction = AL_START_HEAD;
|
|
if (rank < 0) {
|
|
rank = -rank;
|
|
direction = AL_START_TAIL;
|
|
}
|
|
|
|
quicklist* ql = GetQL(it_res.value()->second);
|
|
quicklistIter* ql_iter = quicklistGetIterator(ql, direction);
|
|
quicklistEntry entry;
|
|
|
|
int index = 0;
|
|
int matched = 0;
|
|
vector<uint32_t> matches;
|
|
string str;
|
|
|
|
while (quicklistNext(ql_iter, &entry) && (max_len == 0 || index < max_len)) {
|
|
if (entry.value) {
|
|
str.assign(reinterpret_cast<char*>(entry.value), entry.sz);
|
|
} else {
|
|
str = absl::StrCat(entry.longval);
|
|
}
|
|
if (str == element) {
|
|
matched++;
|
|
auto k = (direction == AL_START_TAIL) ? ql->count - index - 1 : index;
|
|
if (matched >= rank) {
|
|
matches.push_back(k);
|
|
if (count && matched - rank + 1 >= count) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
index++;
|
|
}
|
|
quicklistReleaseIterator(ql_iter);
|
|
return matches;
|
|
}
|
|
|
|
OpResult<int> OpInsert(const OpArgs& op_args, string_view key, string_view pivot, string_view elem,
|
|
int insert_param) {
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
auto it_res = db_slice.Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
|
|
quicklist* ql = GetQL(it_res.value()->second);
|
|
quicklistEntry entry = container_utils::QLEntry();
|
|
quicklistIter* qiter = quicklistGetIterator(ql, AL_START_HEAD);
|
|
bool found = false;
|
|
|
|
while (quicklistNext(qiter, &entry)) {
|
|
if (ElemCompare(entry, pivot)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int res = -1;
|
|
if (found) {
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, *it_res);
|
|
if (insert_param == LIST_TAIL) {
|
|
quicklistInsertAfter(qiter, &entry, elem.data(), elem.size());
|
|
} else {
|
|
DCHECK_EQ(LIST_HEAD, insert_param);
|
|
quicklistInsertBefore(qiter, &entry, elem.data(), elem.size());
|
|
}
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, *it_res, key);
|
|
res = quicklistCount(ql);
|
|
}
|
|
quicklistReleaseIterator(qiter);
|
|
return res;
|
|
}
|
|
|
|
OpResult<uint32_t> OpRem(const OpArgs& op_args, string_view key, string_view elem, long count) {
|
|
DCHECK(!elem.empty());
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
auto it_res = db_slice.Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
|
|
PrimeIterator it = *it_res;
|
|
quicklist* ql = GetQL(it->second);
|
|
|
|
int iter_direction = AL_START_HEAD;
|
|
long long index = 0;
|
|
if (count < 0) {
|
|
count = -count;
|
|
iter_direction = AL_START_TAIL;
|
|
index = -1;
|
|
}
|
|
|
|
quicklistIter* qiter = quicklistGetIteratorAtIdx(ql, iter_direction, index);
|
|
quicklistEntry entry;
|
|
unsigned removed = 0;
|
|
const uint8_t* elem_ptr = reinterpret_cast<const uint8_t*>(elem.data());
|
|
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, it);
|
|
while (quicklistNext(qiter, &entry)) {
|
|
if (quicklistCompare(&entry, elem_ptr, elem.size())) {
|
|
quicklistDelEntry(qiter, &entry);
|
|
removed++;
|
|
if (count && removed == count)
|
|
break;
|
|
}
|
|
}
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, it, key);
|
|
|
|
quicklistReleaseIterator(qiter);
|
|
|
|
if (quicklistCount(ql) == 0) {
|
|
CHECK(db_slice.Del(op_args.db_cntx.db_index, it));
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
OpStatus OpSet(const OpArgs& op_args, string_view key, string_view elem, long index) {
|
|
DCHECK(!elem.empty());
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
auto it_res = db_slice.Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
|
|
PrimeIterator it = *it_res;
|
|
quicklist* ql = GetQL(it->second);
|
|
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, it);
|
|
int replaced = quicklistReplaceAtIndex(ql, index, elem.data(), elem.size());
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, it, key);
|
|
|
|
if (!replaced) {
|
|
return OpStatus::OUT_OF_RANGE;
|
|
}
|
|
return OpStatus::OK;
|
|
}
|
|
|
|
OpStatus OpTrim(const OpArgs& op_args, string_view key, long start, long end) {
|
|
auto& db_slice = op_args.shard->db_slice();
|
|
auto it_res = db_slice.Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!it_res)
|
|
return it_res.status();
|
|
|
|
PrimeIterator it = *it_res;
|
|
quicklist* ql = GetQL(it->second);
|
|
long llen = quicklistCount(ql);
|
|
|
|
/* convert negative indexes */
|
|
if (start < 0)
|
|
start = llen + start;
|
|
if (end < 0)
|
|
end = llen + end;
|
|
if (start < 0)
|
|
start = 0;
|
|
|
|
long ltrim, rtrim;
|
|
|
|
/* Invariant: start >= 0, so this test will be true when end < 0.
|
|
* The range is empty when start > end or start >= length. */
|
|
if (start > end || start >= llen) {
|
|
/* Out of range start or start > end result in empty list */
|
|
ltrim = llen;
|
|
rtrim = 0;
|
|
} else {
|
|
if (end >= llen)
|
|
end = llen - 1;
|
|
ltrim = start;
|
|
rtrim = llen - end - 1;
|
|
}
|
|
|
|
db_slice.PreUpdate(op_args.db_cntx.db_index, it);
|
|
quicklistDelRange(ql, 0, ltrim);
|
|
quicklistDelRange(ql, -rtrim, rtrim);
|
|
db_slice.PostUpdate(op_args.db_cntx.db_index, it, key);
|
|
|
|
if (quicklistCount(ql) == 0) {
|
|
CHECK(db_slice.Del(op_args.db_cntx.db_index, it));
|
|
}
|
|
return OpStatus::OK;
|
|
}
|
|
|
|
OpResult<StringVec> OpRange(const OpArgs& op_args, std::string_view key, long start, long end) {
|
|
auto res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_LIST);
|
|
if (!res)
|
|
return res.status();
|
|
|
|
quicklist* ql = GetQL(res.value()->second);
|
|
long llen = quicklistCount(ql);
|
|
|
|
/* convert negative indexes */
|
|
if (start < 0)
|
|
start = llen + start;
|
|
if (end < 0)
|
|
end = llen + end;
|
|
if (start < 0)
|
|
start = 0;
|
|
|
|
/* Invariant: start >= 0, so this test will be true when end < 0.
|
|
* The range is empty when start > end or start >= length. */
|
|
if (start > end || start >= llen) {
|
|
/* Out of range start or start > end result in empty list */
|
|
return StringVec{};
|
|
}
|
|
|
|
StringVec str_vec;
|
|
container_utils::IterateList(
|
|
res.value()->second,
|
|
[&str_vec](container_utils::ContainerEntry ce) {
|
|
str_vec.emplace_back(ce.ToString());
|
|
return true;
|
|
},
|
|
start, end);
|
|
|
|
return str_vec;
|
|
}
|
|
|
|
OpResult<string> MoveTwoShards(Transaction* trans, string_view src, string_view dest,
|
|
ListDir src_dir, ListDir dest_dir) {
|
|
DCHECK_EQ(2u, trans->unique_shard_cnt());
|
|
|
|
OpResult<string> find_res[2];
|
|
OpResult<string> result;
|
|
|
|
// Transaction is comprised of 2 hops:
|
|
// 1 - check for entries existence, their types and if possible -
|
|
// read the value we may move from the source list.
|
|
// 2. If everything is ok, pop from source and push the peeked value into
|
|
// the destination.
|
|
//
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
auto args = t->ShardArgsInShard(shard->shard_id());
|
|
DCHECK_EQ(1u, args.size());
|
|
bool is_dest = args.front() == dest;
|
|
find_res[is_dest] = Peek(t->GetOpArgs(shard), args.front(), src_dir, !is_dest);
|
|
return OpStatus::OK;
|
|
};
|
|
|
|
trans->Execute(move(cb), false);
|
|
|
|
if (!find_res[0] || find_res[1].status() == OpStatus::WRONG_TYPE) {
|
|
auto cb = [&](Transaction* t, EngineShard* shard) { return OpStatus::OK; };
|
|
trans->Execute(move(cb), true);
|
|
result = find_res[0] ? find_res[1] : find_res[0];
|
|
} else {
|
|
// Everything is ok, lets proceed with the mutations.
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
auto args = t->ShardArgsInShard(shard->shard_id());
|
|
bool is_dest = args.front() == dest;
|
|
OpArgs op_args = t->GetOpArgs(shard);
|
|
|
|
if (is_dest) {
|
|
string_view val{find_res[0].value()};
|
|
absl::Span<string_view> span{&val, 1};
|
|
OpPush(op_args, args.front(), dest_dir, false, span);
|
|
} else {
|
|
OpPop(op_args, args.front(), src_dir, 1, false);
|
|
}
|
|
return OpStatus::OK;
|
|
};
|
|
trans->Execute(move(cb), true);
|
|
result = std::move(find_res[0].value());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void MoveGeneric(ConnectionContext* cntx, string_view src, string_view dest, ListDir src_dir,
|
|
ListDir dest_dir) {
|
|
OpResult<string> result;
|
|
|
|
if (cntx->transaction->unique_shard_cnt() == 1) {
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpMoveSingleShard(t->GetOpArgs(shard), src, dest, src_dir, dest_dir);
|
|
};
|
|
|
|
result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
} else {
|
|
cntx->transaction->Schedule();
|
|
result = MoveTwoShards(cntx->transaction, src, dest, src_dir, dest_dir);
|
|
}
|
|
|
|
if (result) {
|
|
return (*cntx)->SendBulkString(*result);
|
|
}
|
|
|
|
switch (result.status()) {
|
|
case OpStatus::KEY_NOTFOUND:
|
|
(*cntx)->SendNull();
|
|
break;
|
|
|
|
default:
|
|
(*cntx)->SendError(result.status());
|
|
break;
|
|
}
|
|
}
|
|
|
|
void RPopLPush(CmdArgList args, ConnectionContext* cntx) {
|
|
string_view src = ArgS(args, 1);
|
|
string_view dest = ArgS(args, 2);
|
|
|
|
MoveGeneric(cntx, src, dest, ListDir::RIGHT, ListDir::LEFT);
|
|
}
|
|
|
|
void BRPopLPush(CmdArgList args, ConnectionContext* cntx) {
|
|
string_view src = ArgS(args, 1);
|
|
string_view dest = ArgS(args, 2);
|
|
string_view timeout_str = ArgS(args, 3);
|
|
|
|
float timeout;
|
|
if (!absl::SimpleAtof(timeout_str, &timeout)) {
|
|
return (*cntx)->SendError("timeout is not a float or out of range");
|
|
}
|
|
|
|
if (timeout < 0) {
|
|
return (*cntx)->SendError("timeout is negative");
|
|
}
|
|
|
|
BPopPusher bpop_pusher(src, dest, ListDir::RIGHT, ListDir::LEFT);
|
|
OpResult<string> op_res = bpop_pusher.Run(cntx->transaction, unsigned(timeout * 1000));
|
|
|
|
if (op_res) {
|
|
return (*cntx)->SendBulkString(*op_res);
|
|
}
|
|
|
|
switch (op_res.status()) {
|
|
case OpStatus::TIMED_OUT:
|
|
return (*cntx)->SendNull();
|
|
break;
|
|
|
|
default:
|
|
return (*cntx)->SendError(op_res.status());
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ListFamily::LPush(CmdArgList args, ConnectionContext* cntx) {
|
|
return PushGeneric(ListDir::LEFT, false, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::LPushX(CmdArgList args, ConnectionContext* cntx) {
|
|
return PushGeneric(ListDir::LEFT, true, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::LPop(CmdArgList args, ConnectionContext* cntx) {
|
|
return PopGeneric(ListDir::LEFT, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::RPush(CmdArgList args, ConnectionContext* cntx) {
|
|
return PushGeneric(ListDir::RIGHT, false, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::RPushX(CmdArgList args, ConnectionContext* cntx) {
|
|
return PushGeneric(ListDir::RIGHT, true, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::RPop(CmdArgList args, ConnectionContext* cntx) {
|
|
return PopGeneric(ListDir::RIGHT, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::LLen(CmdArgList args, ConnectionContext* cntx) {
|
|
auto key = ArgS(args, 1);
|
|
auto cb = [&](Transaction* t, EngineShard* shard) { return OpLen(t->GetOpArgs(shard), key); };
|
|
OpResult<uint32_t> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (result) {
|
|
(*cntx)->SendLong(result.value());
|
|
} else if (result.status() == OpStatus::KEY_NOTFOUND) {
|
|
(*cntx)->SendLong(0);
|
|
} else {
|
|
(*cntx)->SendError(result.status());
|
|
}
|
|
}
|
|
|
|
void ListFamily::LPos(CmdArgList args, ConnectionContext* cntx) {
|
|
string_view key = ArgS(args, 1);
|
|
string_view elem = ArgS(args, 2);
|
|
|
|
int rank = 1;
|
|
uint32_t count = 1;
|
|
uint32_t max_len = 0;
|
|
bool skip_count = true;
|
|
|
|
for (size_t i = 3; i < args.size(); i++) {
|
|
ToUpper(&args[i]);
|
|
const auto& arg_v = ArgS(args, i);
|
|
if (arg_v == "RANK") {
|
|
if (!absl::SimpleAtoi(ArgS(args, (i + 1)), &rank) || rank == 0) {
|
|
return (*cntx)->SendError(kInvalidIntErr);
|
|
}
|
|
}
|
|
if (arg_v == "COUNT") {
|
|
if (!absl::SimpleAtoi(ArgS(args, (i + 1)), &count)) {
|
|
return (*cntx)->SendError(kInvalidIntErr);
|
|
}
|
|
skip_count = false;
|
|
}
|
|
if (arg_v == "MAXLEN") {
|
|
if (!absl::SimpleAtoi(ArgS(args, (i + 1)), &max_len)) {
|
|
return (*cntx)->SendError(kInvalidIntErr);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpPos(t->GetOpArgs(shard), key, elem, rank, count, max_len);
|
|
};
|
|
|
|
Transaction* trans = cntx->transaction;
|
|
OpResult<vector<uint32_t>> result = trans->ScheduleSingleHopT(std::move(cb));
|
|
|
|
if (result.status() == OpStatus::WRONG_TYPE) {
|
|
return (*cntx)->SendError(result.status());
|
|
} else if (result.status() == OpStatus::INVALID_VALUE) {
|
|
return (*cntx)->SendError(result.status());
|
|
}
|
|
|
|
if (skip_count) {
|
|
if (result->empty()) {
|
|
(*cntx)->SendNull();
|
|
} else {
|
|
(*cntx)->SendLong((*result)[0]);
|
|
}
|
|
} else {
|
|
(*cntx)->StartArray(result->size());
|
|
const auto& array = result.value();
|
|
for (const auto& v : array) {
|
|
(*cntx)->SendLong(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ListFamily::LIndex(CmdArgList args, ConnectionContext* cntx) {
|
|
std::string_view key = ArgS(args, 1);
|
|
std::string_view index_str = ArgS(args, 2);
|
|
int32_t index;
|
|
if (!absl::SimpleAtoi(index_str, &index)) {
|
|
(*cntx)->SendError(kInvalidIntErr);
|
|
return;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpIndex(t->GetOpArgs(shard), key, index);
|
|
};
|
|
|
|
OpResult<string> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (result) {
|
|
(*cntx)->SendBulkString(result.value());
|
|
} else if (result.status() == OpStatus::WRONG_TYPE) {
|
|
(*cntx)->SendError(result.status());
|
|
} else {
|
|
(*cntx)->SendNull();
|
|
}
|
|
}
|
|
|
|
/* LINSERT <key> (BEFORE|AFTER) <pivot> <element> */
|
|
void ListFamily::LInsert(CmdArgList args, ConnectionContext* cntx) {
|
|
string_view key = ArgS(args, 1);
|
|
string_view param = ArgS(args, 2);
|
|
string_view pivot = ArgS(args, 3);
|
|
string_view elem = ArgS(args, 4);
|
|
int where;
|
|
|
|
ToUpper(&args[2]);
|
|
if (param == "AFTER") {
|
|
where = LIST_TAIL;
|
|
} else if (param == "BEFORE") {
|
|
where = LIST_HEAD;
|
|
} else {
|
|
return (*cntx)->SendError(kSyntaxErr);
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpInsert(t->GetOpArgs(shard), key, pivot, elem, where);
|
|
};
|
|
|
|
OpResult<int> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (result) {
|
|
return (*cntx)->SendLong(result.value());
|
|
}
|
|
|
|
(*cntx)->SendError(result.status());
|
|
}
|
|
|
|
void ListFamily::LTrim(CmdArgList args, ConnectionContext* cntx) {
|
|
string_view key = ArgS(args, 1);
|
|
string_view s_str = ArgS(args, 2);
|
|
string_view e_str = ArgS(args, 3);
|
|
int32_t start, end;
|
|
|
|
if (!absl::SimpleAtoi(s_str, &start) || !absl::SimpleAtoi(e_str, &end)) {
|
|
(*cntx)->SendError(kInvalidIntErr);
|
|
return;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpTrim(t->GetOpArgs(shard), key, start, end);
|
|
};
|
|
cntx->transaction->ScheduleSingleHop(std::move(cb));
|
|
(*cntx)->SendOk();
|
|
}
|
|
|
|
void ListFamily::LRange(CmdArgList args, ConnectionContext* cntx) {
|
|
std::string_view key = ArgS(args, 1);
|
|
std::string_view s_str = ArgS(args, 2);
|
|
std::string_view e_str = ArgS(args, 3);
|
|
int32_t start, end;
|
|
|
|
if (!absl::SimpleAtoi(s_str, &start) || !absl::SimpleAtoi(e_str, &end)) {
|
|
(*cntx)->SendError(kInvalidIntErr);
|
|
return;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpRange(t->GetOpArgs(shard), key, start, end);
|
|
};
|
|
|
|
auto res = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (!res && res.status() != OpStatus::KEY_NOTFOUND) {
|
|
return (*cntx)->SendError(res.status());
|
|
}
|
|
|
|
(*cntx)->SendStringArr(*res);
|
|
}
|
|
|
|
// lrem key 5 foo, will remove foo elements from the list if exists at most 5 times.
|
|
void ListFamily::LRem(CmdArgList args, ConnectionContext* cntx) {
|
|
std::string_view key = ArgS(args, 1);
|
|
std::string_view index_str = ArgS(args, 2);
|
|
std::string_view elem = ArgS(args, 3);
|
|
int32_t count;
|
|
|
|
if (!absl::SimpleAtoi(index_str, &count)) {
|
|
(*cntx)->SendError(kInvalidIntErr);
|
|
return;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpRem(t->GetOpArgs(shard), key, elem, count);
|
|
};
|
|
OpResult<uint32_t> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (result) {
|
|
(*cntx)->SendLong(result.value());
|
|
} else {
|
|
(*cntx)->SendLong(0);
|
|
}
|
|
}
|
|
|
|
void ListFamily::LSet(CmdArgList args, ConnectionContext* cntx) {
|
|
std::string_view key = ArgS(args, 1);
|
|
std::string_view index_str = ArgS(args, 2);
|
|
std::string_view elem = ArgS(args, 3);
|
|
int32_t count;
|
|
|
|
if (!absl::SimpleAtoi(index_str, &count)) {
|
|
(*cntx)->SendError(kInvalidIntErr);
|
|
return;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpSet(t->GetOpArgs(shard), key, elem, count);
|
|
};
|
|
OpResult<void> result = cntx->transaction->ScheduleSingleHop(std::move(cb));
|
|
if (result) {
|
|
(*cntx)->SendOk();
|
|
} else {
|
|
(*cntx)->SendError(result.status());
|
|
}
|
|
}
|
|
|
|
void ListFamily::BLPop(CmdArgList args, ConnectionContext* cntx) {
|
|
BPopGeneric(ListDir::LEFT, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::BRPop(CmdArgList args, ConnectionContext* cntx) {
|
|
BPopGeneric(ListDir::RIGHT, std::move(args), cntx);
|
|
}
|
|
|
|
void ListFamily::LMove(CmdArgList args, ConnectionContext* cntx) {
|
|
std::string_view src = ArgS(args, 1);
|
|
std::string_view dest = ArgS(args, 2);
|
|
std::string_view src_dir_str = ArgS(args, 3);
|
|
std::string_view dest_dir_str = ArgS(args, 4);
|
|
|
|
ToUpper(&args[3]);
|
|
ToUpper(&args[4]);
|
|
|
|
ListDir src_dir;
|
|
ListDir dest_dir;
|
|
if (src_dir_str == "LEFT") {
|
|
src_dir = ListDir::LEFT;
|
|
} else if (src_dir_str == "RIGHT") {
|
|
src_dir = ListDir::RIGHT;
|
|
} else {
|
|
return (*cntx)->SendError(kSyntaxErr);
|
|
}
|
|
if (dest_dir_str == "LEFT") {
|
|
dest_dir = ListDir::LEFT;
|
|
} else if (dest_dir_str == "RIGHT") {
|
|
dest_dir = ListDir::RIGHT;
|
|
} else {
|
|
return (*cntx)->SendError(kSyntaxErr);
|
|
}
|
|
|
|
MoveGeneric(cntx, src, dest, src_dir, dest_dir);
|
|
}
|
|
|
|
void ListFamily::BPopGeneric(ListDir dir, CmdArgList args, ConnectionContext* cntx) {
|
|
DCHECK_GE(args.size(), 3u);
|
|
|
|
float timeout;
|
|
auto timeout_str = ArgS(args, args.size() - 1);
|
|
if (!absl::SimpleAtof(timeout_str, &timeout)) {
|
|
return (*cntx)->SendError("timeout is not a float or out of range");
|
|
}
|
|
if (timeout < 0) {
|
|
return (*cntx)->SendError("timeout is negative");
|
|
}
|
|
VLOG(1) << "BLPop start " << timeout;
|
|
|
|
Transaction* transaction = cntx->transaction;
|
|
BPopper popper(dir);
|
|
OpStatus result = popper.Run(transaction, unsigned(timeout * 1000));
|
|
|
|
if (result == OpStatus::OK) {
|
|
auto res = popper.result();
|
|
|
|
VLOG(1) << "BLPop returned from " << res.first; // key.
|
|
|
|
std::string_view str_arr[2] = {res.first, res.second};
|
|
|
|
return (*cntx)->SendStringArr(str_arr);
|
|
}
|
|
|
|
switch (result) {
|
|
case OpStatus::WRONG_TYPE:
|
|
return (*cntx)->SendError(kWrongTypeErr);
|
|
case OpStatus::TIMED_OUT:
|
|
return (*cntx)->SendNullArray();
|
|
default:
|
|
LOG(ERROR) << "Unexpected error " << result;
|
|
}
|
|
return (*cntx)->SendNullArray();
|
|
}
|
|
|
|
void ListFamily::PushGeneric(ListDir dir, bool skip_notexists, CmdArgList args,
|
|
ConnectionContext* cntx) {
|
|
std::string_view key = ArgS(args, 1);
|
|
vector<std::string_view> vals(args.size() - 2);
|
|
for (size_t i = 2; i < args.size(); ++i) {
|
|
vals[i - 2] = ArgS(args, i);
|
|
}
|
|
absl::Span<std::string_view> span{vals.data(), vals.size()};
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpPush(t->GetOpArgs(shard), key, dir, skip_notexists, span);
|
|
};
|
|
|
|
OpResult<uint32_t> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
if (result) {
|
|
return (*cntx)->SendLong(result.value());
|
|
}
|
|
|
|
return (*cntx)->SendError(result.status());
|
|
}
|
|
|
|
void ListFamily::PopGeneric(ListDir dir, CmdArgList args, ConnectionContext* cntx) {
|
|
string_view key = ArgS(args, 1);
|
|
int32_t count = 1;
|
|
bool return_arr = false;
|
|
|
|
if (args.size() > 2) {
|
|
if (args.size() > 3) {
|
|
ToLower(&args[0]);
|
|
return (*cntx)->SendError(WrongNumArgsError(ArgS(args, 0)));
|
|
}
|
|
|
|
string_view count_s = ArgS(args, 2);
|
|
if (!absl::SimpleAtoi(count_s, &count)) {
|
|
return (*cntx)->SendError(kInvalidIntErr);
|
|
}
|
|
|
|
if (count < 0) {
|
|
return (*cntx)->SendError(kUintErr);
|
|
}
|
|
return_arr = true;
|
|
}
|
|
|
|
auto cb = [&](Transaction* t, EngineShard* shard) {
|
|
return OpPop(t->GetOpArgs(shard), key, dir, count, true);
|
|
};
|
|
|
|
OpResult<StringVec> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
|
|
|
|
switch (result.status()) {
|
|
case OpStatus::KEY_NOTFOUND:
|
|
return (*cntx)->SendNull();
|
|
case OpStatus::WRONG_TYPE:
|
|
return (*cntx)->SendError(kWrongTypeErr);
|
|
default:;
|
|
}
|
|
|
|
if (return_arr) {
|
|
if (result->empty()) {
|
|
(*cntx)->SendNullArray();
|
|
} else {
|
|
(*cntx)->StartArray(result->size());
|
|
for (const auto& k : *result) {
|
|
(*cntx)->SendBulkString(k);
|
|
}
|
|
}
|
|
} else {
|
|
DCHECK_EQ(1u, result->size());
|
|
(*cntx)->SendBulkString(result->front());
|
|
}
|
|
}
|
|
|
|
using CI = CommandId;
|
|
|
|
#define HFUNC(x) SetHandler(&ListFamily::x)
|
|
|
|
void ListFamily::Register(CommandRegistry* registry) {
|
|
*registry << CI{"LPUSH", CO::WRITE | CO::FAST | CO::DENYOOM, -3, 1, 1, 1}.HFUNC(LPush)
|
|
<< CI{"LPUSHX", CO::WRITE | CO::FAST | CO::DENYOOM, -3, 1, 1, 1}.HFUNC(LPushX)
|
|
<< CI{"LPOP", CO::WRITE | CO::FAST | CO::DENYOOM, -2, 1, 1, 1}.HFUNC(LPop)
|
|
<< CI{"RPUSH", CO::WRITE | CO::FAST | CO::DENYOOM, -3, 1, 1, 1}.HFUNC(RPush)
|
|
<< CI{"RPUSHX", CO::WRITE | CO::FAST | CO::DENYOOM, -3, 1, 1, 1}.HFUNC(RPushX)
|
|
<< CI{"RPOP", CO::WRITE | CO::FAST | CO::DENYOOM, -2, 1, 1, 1}.HFUNC(RPop)
|
|
<< CI{"RPOPLPUSH", CO::WRITE | CO::FAST | CO::DENYOOM, 3, 1, 2, 1}.SetHandler(RPopLPush)
|
|
<< CI{"BRPOPLPUSH", CO::WRITE | CO::NOSCRIPT | CO::BLOCKING, 4, 1, 2, 1}.SetHandler(
|
|
BRPopLPush)
|
|
<< CI{"BLPOP", CO::WRITE | CO::NOSCRIPT | CO::BLOCKING, -3, 1, -2, 1}.HFUNC(BLPop)
|
|
<< CI{"BRPOP", CO::WRITE | CO::NOSCRIPT | CO::BLOCKING, -3, 1, -2, 1}.HFUNC(BRPop)
|
|
<< CI{"LLEN", CO::READONLY | CO::FAST, 2, 1, 1, 1}.HFUNC(LLen)
|
|
<< CI{"LPOS", CO::READONLY | CO::FAST, -3, 1, 1, 1}.HFUNC(LPos)
|
|
<< CI{"LINDEX", CO::READONLY, 3, 1, 1, 1}.HFUNC(LIndex)
|
|
<< CI{"LINSERT", CO::WRITE, 5, 1, 1, 1}.HFUNC(LInsert)
|
|
<< CI{"LRANGE", CO::READONLY, 4, 1, 1, 1}.HFUNC(LRange)
|
|
<< CI{"LSET", CO::WRITE | CO::DENYOOM, 4, 1, 1, 1}.HFUNC(LSet)
|
|
<< CI{"LTRIM", CO::WRITE, 4, 1, 1, 1}.HFUNC(LTrim)
|
|
<< CI{"LREM", CO::WRITE, 4, 1, 1, 1}.HFUNC(LRem)
|
|
<< CI{"LMOVE", CO::WRITE | CO::DENYOOM, 5, 1, 2, 1}.HFUNC(LMove);
|
|
}
|
|
|
|
} // namespace dfly
|