Repository: dragonflydb/dragonfly (2026-07-02T18:05:07Z)

Issue:
MONITOR never decrements ConnectionContext::subscriptions
`ConnectionContext::EnableMonitoring` in `src/server/conn_context.h` increments `subscriptions` when MONITOR starts, but the counter is never decremented when MONITOR stops — `ChangeMonitor()` runs the same increment on both paths:

```cpp
void EnableMonitoring(bool enable) {
  subscriptions++;  // required to support the monitoring
  monitor = enable;
}
```

Found while implementing the RESET command (#7760).



Discussion:

Patch:
diff --git a/src/facade/dragonfly_connection.h b/src/facade/dragonfly_connection.h
index 870e6955eb6c..a604f6eb919e 100644
--- a/src/facade/dragonfly_connection.h
+++ b/src/facade/dragonfly_connection.h
@@ -176,7 +176,8 @@ class Connection : public util::Connection {
   virtual void SendPubMessageAsync(PubMessage);
 
   // Add monitor message to dispatch queue.
-  void SendMonitorMessageAsync(std::string);
+  // Virtual because behavior is overridden in test_utils.
+  virtual void SendMonitorMessageAsync(std::string);
 
   // If any dispatch is currently in progress, increment counter and send checkpoint message to
   // decrement it once finished.
diff --git a/src/server/conn_context.cc b/src/server/conn_context.cc
index bfa9232c1921..6966757aaad4 100644
--- a/src/server/conn_context.cc
+++ b/src/server/conn_context.cc
@@ -120,7 +120,11 @@ void ConnectionContext::ChangeMonitor(bool start) {
   // Tell other threads that about the change in the number of connection that we monitor
   shard_set->pool()->AwaitBrief(
       [start](unsigned, auto*) { ServerState::tlocal()->Monitors().NotifyChangeCount(start); });
-  EnableMonitoring(start);
+  if (start) {
+    EnableMonitoring();
+  } else {
+    DisableMonitoring();
+  }
 }
 
 void ConnectionContext::ChangeSubscription(bool to_add, bool to_reply, bool sharded,
diff --git a/src/server/conn_context.h b/src/server/conn_context.h
index 23bd793e071c..a80fb487d998 100644
--- a/src/server/conn_context.h
+++ b/src/server/conn_context.h
@@ -6,6 +6,8 @@
 
 #include <absl/container/flat_hash_set.h>
 
+#include <cassert>
+
 #include "facade/conn_context.h"
 #include "facade/parsed_command.h"
 #include "facade/reply_mode.h"
@@ -382,9 +384,15 @@ class ConnectionContext : public facade::ConnectionContext {
   bool skip_acl_validation = false;
 
  private:
-  void EnableMonitoring(bool enable) {
+  void EnableMonitoring() {
     subscriptions++;  // required to support the monitoring
-    monitor = enable;
+    monitor = true;
+  }
+
+  void DisableMonitoring() {
+    assert(subscriptions > 0u);
+    subscriptions--;
+    monitor = false;
   }
 
   std::vector<unsigned> ChangeSubscriptions(facade::ParsedArgs channels, bool pattern, bool to_add,


Test patch:
diff --git a/src/server/dragonfly_test.cc b/src/server/dragonfly_test.cc
index 5c4f70730e3d..0cb87917d56f 100644
--- a/src/server/dragonfly_test.cc
+++ b/src/server/dragonfly_test.cc
@@ -654,6 +654,17 @@ TEST_F(DflyEngineTest, PUnsubscribe) {
   EXPECT_THAT(resp.GetVec(), ElementsAre("punsubscribe", "b*", IntArg(0)));
 }
 
+TEST_F(DflyEngineTest, MonitorSubscriptionsAccounting) {
+  EXPECT_EQ(Run({"monitor"}), "OK");
+  EXPECT_EQ(1u, NumSubscriptions("IO0"));
+
+  // Deactivating the monitor (here via QUIT) must fully undo its contribution to `subscriptions`,
+  // so the connection is treated as non-subscribed again (eligible for thread migration and
+  // synchronous dispatch).
+  EXPECT_EQ(Run({"quit"}), "OK");
+  EXPECT_EQ(0u, NumSubscriptions("IO0"));
+}
+
 TEST_F(DflyEngineTest, Bug468) {
   RespExpr resp = Run({"multi"});
   ASSERT_EQ(resp, "OK");
diff --git a/src/server/test_utils.cc b/src/server/test_utils.cc
index 641548417867..0e40d69716bb 100644
--- a/src/server/test_utils.cc
+++ b/src/server/test_utils.cc
@@ -96,6 +96,10 @@ void TestConnection::SendInvalidationMessageAsync(InvalidationMessage msg) {
   invalidate_messages.push_back(std::move(msg));
 }
 
+void TestConnection::SendMonitorMessageAsync(std::string msg) {
+  monitor_messages.push_back(std::move(msg));
+}
+
 std::string TestConnection::RemoteEndpointStr() const {
   return "";
 }
@@ -674,6 +678,13 @@ string BaseFamilyTest::GetId() const {
   return absl::StrCat("IO", id);
 }
 
+size_t BaseFamilyTest::NumSubscriptions(string_view conn_id) const {
+  auto it = connections_.find(conn_id);
+  CHECK(it != connections_.end());
+
+  return it->second->conn()->cntx()->subscriptions;
+}
+
 size_t BaseFamilyTest::SubscriberMessagesLen(string_view conn_id) const {
   auto it = connections_.find(conn_id);
   if (it == connections_.end())
diff --git a/src/server/test_utils.h b/src/server/test_utils.h
index 6da21d7d9fe0..fbfbe247a781 100644
--- a/src/server/test_utils.h
+++ b/src/server/test_utils.h
@@ -36,6 +36,8 @@ class TestConnection : public facade::Connection {
 
   void SendInvalidationMessageAsync(InvalidationMessage msg) final;
 
+  void SendMonitorMessageAsync(std::string msg) final;
+
   bool IsPrivileged() const override {
     return is_privileged_;
   }
@@ -47,6 +49,8 @@ class TestConnection : public facade::Connection {
 
   std::vector<InvalidationMessage> invalidate_messages;
 
+  std::vector<std::string> monitor_messages;
+
  private:
   bool is_privileged_ = false;
 };
@@ -161,6 +165,8 @@ class BaseFamilyTest : public ::testing::Test {
   std::string GetId() const;
   size_t SubscriberMessagesLen(std::string_view conn_id) const;
 
+  size_t NumSubscriptions(std::string_view conn_id) const;
+
   size_t InvalidationMessagesLen(std::string_view conn_id) const;
 
   const facade::Connection::PubMessage& GetPublishedMessage(std::string_view conn_id,