Skip to content

Commit 1418e23

Browse files
Aetherancegit-hulkaleksraiden
authored
feat(keyspace): emit set and del notifications (#3541)
Implement initial keyspace notifications for set and del events, compatible with Redis notify-keyspace-events. This PR adds the initial notification emitters and configuration support: - Support `K`, `E`, `g`, `$`, and `A` flags, where `A` currently expands to the implemented event classes `g$`. - Emit `set` notifications only when the shared `Set` path actually applies a write, including conditional `SET` variants. - Emit `del` notifications only for keys that are actually deleted through the shared delete path. - Deduplicate repeated keys in a single delete operation, so the same key is deleted and notified once. - Queue notifications inside `MULTI/EXEC` and publish them after a successful commit. - Map notification DB names correctly: - default namespace uses DB `0` - `redis-databases` namespaces map back to Redis DB indexes This PR only adds the initial set and del event paths on primary nodes, keeping the initial patch small and reviewable. Commands sharing the same underlying mutation APIs may also produce these events. Replica-side notifications and additional event types, if supported, can be added in follow-up PRs. Unsupported notification classes are rejected for now until their emitters are implemented. Ref Proposal: #3533 Tracking Issue: #2915 Assisted by Codex/GPT-5.5. --------- Co-authored-by: hulk <hulk.website@gmail.com> Co-authored-by: Aleks Lozovyuk <aleks.raiden@gmail.com>
1 parent a4febb5 commit 1418e23

16 files changed

Lines changed: 738 additions & 3 deletions

File tree

kvrocks.conf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,23 @@ lua-strict-key-accessing no
610610
#
611611
# tls-replication yes
612612

613+
############################# KEYSPACE NOTIFICATIONS ##########################
614+
615+
# Keyspace notifications publish key changes to SUBSCRIBE and PSUBSCRIBE clients.
616+
# Supported flags:
617+
# K keyspace channels
618+
# E keyevent channels
619+
# g generic events, currently del
620+
# $ string events, currently set
621+
# A same as g$, without K or E
622+
#
623+
# Default namespace uses db 0. Redis database namespaces use db indexes.
624+
# Other namespaces use their original names.
625+
# Notifications are emitted only when at least one channel flag (K or E) and one event class are enabled.
626+
#
627+
# Default: "" disabled
628+
notify-keyspace-events ""
629+
613630
################################## SLOW LOG ###################################
614631

615632
# The Kvrocks Slow Log is a mechanism to log queries that exceeded a specified

src/commands/cmd_txn.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ class CommandExec : public Commander {
9090
s = storage->CommitTxn();
9191
}
9292

93+
// Publish queued notifications after a successful commit.
94+
if (s.IsOK()) {
95+
conn->FlushKeyspaceEvents();
96+
}
97+
9398
conn->ResetMultiExec();
9499
reset_multiexec.Disable();
95100

src/common/keyspace_events.cc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*
19+
*/
20+
21+
#include "keyspace_events.h"
22+
23+
#include <cstring>
24+
25+
#include "config/config.h"
26+
#include "fmt/format.h"
27+
28+
StatusOr<std::pair<KeyspaceEventChannel, KeyspaceEventType>> ParseNotifyKeyspaceEventsFlags(std::string_view input) {
29+
int channel_flags = 0;
30+
int type_flags = 0;
31+
for (const char c : input) {
32+
switch (c) {
33+
case 'K':
34+
channel_flags |= kNotifyKeyspace;
35+
break;
36+
case 'E':
37+
channel_flags |= kNotifyKeyevent;
38+
break;
39+
case 'A':
40+
type_flags |= kNotifyAll;
41+
break;
42+
case 'g':
43+
type_flags |= kNotifyGeneric;
44+
break;
45+
case '$':
46+
type_flags |= kNotifyString;
47+
break;
48+
default:
49+
return {Status::NotOK, fmt::format("unsupported notify-keyspace-events flag: '{}'", c)};
50+
}
51+
}
52+
53+
return std::pair{static_cast<KeyspaceEventChannel>(channel_flags), static_cast<KeyspaceEventType>(type_flags)};
54+
}
55+
56+
std::string FormatKeyspaceNotificationScope(const std::string &ns, int redis_databases) {
57+
if (ns == kDefaultNamespace) {
58+
return "0";
59+
}
60+
if (redis_databases > 0 && ns.rfind(kDatabaseNamespacePrefix, 0) == 0) {
61+
return ns.substr(strlen(kDatabaseNamespacePrefix));
62+
}
63+
return ns;
64+
}

src/common/keyspace_events.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*
19+
*/
20+
21+
#pragma once
22+
23+
#include <string>
24+
#include <string_view>
25+
#include <utility>
26+
27+
#include "status.h"
28+
29+
enum KeyspaceEventChannel {
30+
kNotifyNoChannel = 0,
31+
kNotifyKeyspace = 1 << 0, // K, keyspace channels
32+
kNotifyKeyevent = 1 << 1, // E, keyevent channels
33+
};
34+
35+
// Event type flags for notify-keyspace-events, separate from RedisType.
36+
enum KeyspaceEventType {
37+
kNotifyNoType = 0,
38+
kNotifyGeneric = 1 << 0, // g, emits del
39+
kNotifyString = 1 << 1, // $, emits set
40+
// A, supported data classes without K or E.
41+
kNotifyAll = kNotifyGeneric | kNotifyString,
42+
};
43+
44+
struct KeyspaceEvent {
45+
KeyspaceEvent(KeyspaceEventType type_flag, std::string_view event, KeyspaceEventChannel channel_flags,
46+
std::string_view ns, std::string_view key)
47+
: type_flag(type_flag), channel_flags(channel_flags), event(event), ns(ns), key(key) {}
48+
49+
KeyspaceEventType type_flag;
50+
KeyspaceEventChannel channel_flags;
51+
std::string event;
52+
std::string ns;
53+
std::string key;
54+
};
55+
56+
// Parses notify-keyspace-events flags into channel flags followed by event type flags.
57+
StatusOr<std::pair<KeyspaceEventChannel, KeyspaceEventType>> ParseNotifyKeyspaceEventsFlags(std::string_view input);
58+
59+
// Formats the namespace or database scope used in keyspace notification channel names.
60+
// Default namespace maps to 0; database namespaces map back to db indexes when redis-databases is enabled.
61+
// Other namespaces use their original names.
62+
std::string FormatKeyspaceNotificationScope(const std::string &ns, int redis_databases);

src/config/config.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <utility>
3636
#include <vector>
3737

38+
#include "common/keyspace_events.h"
3839
#include "common/string_util.h"
3940
#include "config_type.h"
4041
#include "config_util.h"
@@ -192,6 +193,7 @@ Config::Config() {
192193
{"compact-cron", false, new StringField(&compact_cron_str_, "")},
193194
{"bgsave-cron", false, new StringField(&bgsave_cron_str_, "")},
194195
{"dbsize-scan-cron", false, new StringField(&dbsize_scan_cron_str_, "")},
196+
{"notify-keyspace-events", false, new StringField(&notify_keyspace_events_str_, "")},
195197
{"replica-announce-ip", false, new StringField(&replica_announce_ip, "")},
196198
{"replica-announce-port", false, new UInt32Field(&replica_announce_port, 0, 0, PORT_LIMIT)},
197199
{"compaction-checker-range", false, new StringField(&compaction_checker_range_str_, "")},
@@ -379,6 +381,10 @@ void Config::initFieldValidator() {
379381
}
380382
return Status::OK();
381383
}},
384+
{"notify-keyspace-events",
385+
[]([[maybe_unused]] const std::string &k, const std::string &v) -> Status {
386+
return ParseNotifyKeyspaceEventsFlags(v).ToStatus();
387+
}},
382388
{"compact-cron",
383389
[this]([[maybe_unused]] const std::string &k, const std::string &v) -> Status {
384390
std::vector<std::string> args = util::Split(v, " \t");
@@ -556,6 +562,13 @@ void Config::initFieldCallback() {
556562
srv->AdjustWorkerThreads();
557563
return Status::OK();
558564
}},
565+
{"notify-keyspace-events",
566+
[this]([[maybe_unused]] Server *srv, [[maybe_unused]] const std::string &k, const std::string &v) -> Status {
567+
const auto flags = GET_OR_RET(ParseNotifyKeyspaceEventsFlags(v));
568+
notify_keyspace_event_channels = flags.first;
569+
notify_keyspace_event_types = flags.second;
570+
return Status::OK();
571+
}},
559572
{"dir",
560573
[this]([[maybe_unused]] Server *srv, [[maybe_unused]] const std::string &k,
561574
[[maybe_unused]] const std::string &v) -> Status {

src/config/config.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include "config_type.h"
3434
#include "cron.h"
35+
#include "keyspace_events.h"
3536
#include "spdlog/common.h"
3637
#include "status.h"
3738
#include "storage/redis_metadata.h"
@@ -219,6 +220,10 @@ struct Config {
219220
// Enable transactional mode in engine::Context
220221
bool txn_context_enabled = false;
221222

223+
// Parsed notify-keyspace-events flags.
224+
KeyspaceEventChannel notify_keyspace_event_channels = kNotifyNoChannel;
225+
KeyspaceEventType notify_keyspace_event_types = kNotifyNoType;
226+
222227
bool skip_block_cache_deallocation_on_close = false;
223228

224229
bool lua_strict_key_accessing = false;
@@ -315,6 +320,7 @@ struct Config {
315320
std::string compaction_checker_cron_str_;
316321
std::string profiling_sample_commands_str_;
317322
std::string client_output_buffer_limit_str_;
323+
std::string notify_keyspace_events_str_;
318324
std::map<std::string, std::unique_ptr<ConfigField>> fields_;
319325
std::vector<std::string> rename_command_;
320326
std::string histogram_bucket_boundaries_str_;

src/server/redis_connection.cc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include <mutex>
2525
#include <nonstd/span.hpp>
26+
#include <optional>
2627
#include <shared_mutex>
2728

2829
#include "commands/commander.h"
@@ -634,6 +635,7 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
634635
}
635636

636637
SetLastCmd(cmd_name);
638+
std::vector<KeyspaceEvent> keyspace_events;
637639
{
638640
std::optional<MultiLockGuard> guard;
639641
if (cmd_flags & kCmdWrite) {
@@ -652,6 +654,9 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
652654
guard.emplace(srv_->storage->GetLockManager(), lock_keys);
653655
}
654656
engine::Context ctx(srv_->storage);
657+
if (cmd_flags & kCmdWrite) {
658+
ctx.EnableKeyspaceEventCollection(config->notify_keyspace_event_channels, config->notify_keyspace_event_types);
659+
}
655660

656661
std::vector<GlobalIndexer::RecordResult> index_records;
657662
if (!srv_->index_mgr.index_map.empty() && IsCmdForIndexing(cmd_flags, attributes->category) &&
@@ -679,6 +684,13 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
679684
WARN("[connection] index updating failed for key: {}", record.key);
680685
}
681686
}
687+
if (ctx.HasKeyspaceEvents()) {
688+
keyspace_events = ctx.TakeKeyspaceEvents();
689+
}
690+
}
691+
// Nested Lua and function commands reuse their outer context. Publish only after index updates and key unlocking.
692+
if (!keyspace_events.empty()) {
693+
queueOrPublishKeyspaceEvents(std::move(keyspace_events));
682694
}
683695

684696
if (!(cmd_flags & redis::kCmdSkipMonitor)) {
@@ -709,10 +721,40 @@ void Connection::ExecuteCommands(std::deque<CommandTokens> *to_process_cmds) {
709721
}
710722
}
711723

724+
void Connection::queueOrPublishKeyspaceEvents(std::vector<KeyspaceEvent> &&events) {
725+
if (events.empty()) return;
726+
727+
if (in_exec_) {
728+
// Queue transaction events until commit.
729+
for (auto &event : events) {
730+
pending_keyspace_events_.emplace_back(std::move(event));
731+
}
732+
return;
733+
}
734+
735+
for (const auto &event : events) {
736+
srv_->NotifyKeyspaceEvent(event);
737+
}
738+
}
739+
740+
void Connection::FlushKeyspaceEvents() {
741+
for (const auto &e : pending_keyspace_events_) {
742+
srv_->NotifyKeyspaceEvent(e);
743+
}
744+
pending_keyspace_events_.clear();
745+
}
746+
712747
void Connection::ResetMultiExec() {
713748
in_exec_ = false;
714749
multi_error_ = false;
715750
multi_cmds_.clear();
751+
// Drop events from failed or aborted transactions.
752+
pending_keyspace_events_.clear();
753+
// Retain capacity for typical transactions, but request releasing unusually large buffers.
754+
constexpr std::size_t kMaxRetainedKeyspaceEvents = 1024;
755+
if (pending_keyspace_events_.capacity() > kMaxRetainedKeyspaceEvents) {
756+
pending_keyspace_events_.shrink_to_fit();
757+
}
716758
DisableFlag(Connection::kMultiExec);
717759
}
718760

src/server/redis_connection.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <vector>
3131

3232
#include "commands/commander.h"
33+
#include "common/keyspace_events.h"
3334
#include "event_util.h"
3435
#include "redis_request.h"
3536
#include "server/redis_reply.h"
@@ -208,6 +209,8 @@ class Connection : public EvbufCallbackBase<Connection> {
208209
void ResetMultiExec();
209210
std::deque<redis::CommandTokens> *GetMultiExecCommands() { return &multi_cmds_; }
210211

212+
void FlushKeyspaceEvents();
213+
211214
std::function<void(int)> close_cb = nullptr;
212215

213216
std::set<std::string> watched_keys;
@@ -218,6 +221,9 @@ class Connection : public EvbufCallbackBase<Connection> {
218221
ReplyMode GetReplyMode() const { return reply_mode_; }
219222

220223
private:
224+
// Queues events while EXEC is running; publishes them otherwise.
225+
void queueOrPublishKeyspaceEvents(std::vector<KeyspaceEvent> &&events);
226+
221227
uint64_t id_ = 0;
222228
std::atomic<int> flags_ = 0;
223229
std::string ns_;
@@ -248,6 +254,8 @@ class Connection : public EvbufCallbackBase<Connection> {
248254
bool multi_error_ = false;
249255
std::atomic<bool> is_running_ = false;
250256
std::deque<redis::CommandTokens> multi_cmds_;
257+
258+
std::vector<KeyspaceEvent> pending_keyspace_events_;
251259
bool in_script_ = false;
252260

253261
bool importing_ = false;

src/server/server.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
#include "commands/command_parser.h"
4343
#include "commands/commander.h"
44+
#include "common/keyspace_events.h"
4445
#include "common/string_util.h"
4546
#include "config/config.h"
4647
#include "fmt/format.h"
@@ -478,6 +479,17 @@ int Server::PublishMessage(const std::string &channel, const std::string &msg) {
478479
return cnt;
479480
}
480481

482+
void Server::NotifyKeyspaceEvent(const KeyspaceEvent &event) {
483+
const std::string scope = FormatKeyspaceNotificationScope(event.ns, GetConfig()->redis_databases);
484+
// Publish keyspace before keyevent for each key.
485+
if (event.channel_flags & kNotifyKeyspace) {
486+
PublishMessage("__keyspace@" + scope + "__:" + event.key, event.event);
487+
}
488+
if (event.channel_flags & kNotifyKeyevent) {
489+
PublishMessage("__keyevent@" + scope + "__:" + event.event, event.key);
490+
}
491+
}
492+
481493
void Server::SubscribeChannel(const std::string &channel, redis::Connection *conn) {
482494
std::lock_guard<std::mutex> guard(pubsub_channels_mu_);
483495

src/server/server.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ class Server {
218218
int GetFetchFileThreadNum() const { return fetch_file_threads_num_; }
219219

220220
int PublishMessage(const std::string &channel, const std::string &msg);
221+
222+
// Publishes a keyspace event through the channels selected when it was collected.
223+
void NotifyKeyspaceEvent(const KeyspaceEvent &event);
221224
void SubscribeChannel(const std::string &channel, redis::Connection *conn);
222225
void UnsubscribeChannel(const std::string &channel, redis::Connection *conn);
223226
void GetChannelsByPattern(const std::string &pattern, std::vector<std::string> *channels);

0 commit comments

Comments
 (0)