Skip to content

fix(server): INFO # Keyspace lists all dbs across all shards; GRAPH.LIST unions all shards#280

Merged
pilotspacex-byte merged 1 commit into
mainfrom
feat/listing-parity
Jul 11, 2026
Merged

fix(server): INFO # Keyspace lists all dbs across all shards; GRAPH.LIST unions all shards#280
pilotspacex-byte merged 1 commit into
mainfrom
feat/listing-parity

Conversation

@pilotspacex-byte

@pilotspacex-byte pilotspacex-byte commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Listing parity (task #26): two commands silently reported a single shard's local view as if it were the whole server.

INFO # Keyspace — all dbs × all shards

Previously printed one hardcoded db0: line holding the SELECTED db's LOCAL-shard key count — SELECT 2; SET k v; INFO reported the db-2 count as db0, other dbs were invisible, and at --shards N the other shards' keys were uncounted. Now:

  • New ShardMessage::KeyspaceStats typed-reply scatter (same pattern as GetKeysInSlot/AofFold): each shard returns per-db (keys, expires) from O(#dbs) counter reads — no key iteration.
  • coordinate_keyspace_info sums element-wise across shards; degrades to partial results on a dead channel.
  • info_with_keyspace lists every NON-EMPTY logical db as db{i}:keys=K,expires=E,avg_ttl=0 (Redis semantics).
  • Wired on all three dispatch paths: monoio, tokio sharded, single-handler.

GRAPH.LIST — all shards

Previously listed only the connection shard's graphs (~1/N — a graph lives on the shard owning its name). Now scatters a GraphCommand to every shard and returns the sorted, deduplicated union via merge_graph_list_responses (skips error/non-array frames so one sick shard degrades instead of failing the command). Both monoio and tokio sharded paths.

Verification

  • lib 4109 green; fmt + clippy clean (default and runtime-tokio,jemalloc)
  • Smoke-tested at --shards 1 and --shards 4: 8 graphs created round-robin all listed; SELECT 2/3/5 writes all reported with cross-shard sums
  • Unit tests: info_with_keyspace_lists_every_nonempty_db, merge_graph_list_unions_sorts_dedups_and_skips_errors

Summary by CodeRabbit

  • Bug Fixes
    • Improved INFO keyspace reporting to include every non-empty logical database and aggregate keys and expirations across shards.
    • Updated GRAPH.LIST --shards N to return a sorted, deduplicated list combining graphs from all shards.
    • Ensured empty databases and invalid remote graph responses are excluded from results.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

INFO now aggregates non-empty logical database keyspace counters across shards, while multi-shard GRAPH.LIST scatters requests and returns a sorted, deduplicated union of graph names.

Changes

Listing parity

Layer / File(s) Summary
Keyspace statistics coordination
src/shard/dispatch.rs, src/shard/spsc_handler.rs, src/shard/coordinator.rs
Adds KeyspaceStats shard messages and sums per-database key and expiration counters across shards.
INFO keyspace response generation
src/command/connection.rs, src/server/conn/handler_single.rs, src/server/conn/handler_monoio/..., src/server/conn/handler_sharded/...
Adds info_with_keyspace, updates INFO handlers to use aggregated counters, and lists only non-empty logical databases.
GRAPH.LIST response merging
src/command/graph/graph_read.rs, src/command/graph/mod.rs
Adds and tests a helper that unions, sorts, and deduplicates graph names while ignoring non-array responses.
Multi-shard GRAPH.LIST dispatch
src/server/conn/handler_monoio/write.rs, src/server/conn/handler_sharded/write.rs, CHANGELOG.md
Scatters GRAPH.LIST to remote shards, combines results with local graphs, and documents both parity corrections.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant INFOHandler
  participant ShardCoordinator
  participant RemoteShard
  Client->>INFOHandler: INFO
  INFOHandler->>ShardCoordinator: aggregate keyspace counters
  ShardCoordinator->>RemoteShard: KeyspaceStats
  RemoteShard-->>ShardCoordinator: per-database counters
  ShardCoordinator-->>INFOHandler: summed keyspace
  INFOHandler-->>Client: INFO with non-empty db entries
Loading
sequenceDiagram
  participant Client
  participant GRAPHLISTHandler
  participant RemoteShard
  participant GraphMerger
  Client->>GRAPHLISTHandler: GRAPH.LIST
  GRAPHLISTHandler->>RemoteShard: GraphCommand on each remote shard
  RemoteShard-->>GRAPHLISTHandler: graph arrays
  GRAPHLISTHandler->>GraphMerger: local and remote responses
  GraphMerger-->>Client: sorted deduplicated graph union
Loading

Possibly related PRs

  • pilotspace/moon#250: Overlaps with the cross-shard GRAPH dispatch and bounded reply handling paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main behavior fixes for INFO # Keyspace and GRAPH.LIST.
Description check ✅ Passed The description covers the main summary, implementation details, and verification, with only some template sections omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/listing-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…IST unions all shards

Listing parity (task #26): two commands silently reported a single
shard's local view as if it were the whole server.

INFO # Keyspace previously printed one hardcoded `db0:` line holding the
SELECTED db's LOCAL-shard key count — `SELECT 2; SET k v; INFO` reported
the db-2 count as db0, other dbs were invisible, and at --shards N the
other shards' keys were uncounted. Now:

- New `ShardMessage::KeyspaceStats` typed-reply scatter (same pattern as
  GetKeysInSlot/AofFold): each shard returns per-db (keys, expires) from
  O(#dbs) counter reads — no key iteration.
- `coordinate_keyspace_info` (shard/coordinator.rs) sums element-wise
  across shards, degrades to partial results on a dead channel.
- `info_with_keyspace` (command/connection.rs) rewrites the # Keyspace
  section listing every NON-EMPTY logical db as
  `db{i}:keys=K,expires=E,avg_ttl=0` (Redis semantics).
- Wired on all three dispatch paths: monoio dispatch, tokio
  handler_sharded dispatch, and handler_single (single shard = truth,
  no scatter needed).

GRAPH.LIST previously listed only the connection shard's graphs (~1/N,
since a graph lives on the shard that owns its name). Now scatters a
GraphCommand to every shard and returns the sorted, deduplicated union
via `merge_graph_list_responses` (skips error/non-array frames so one
sick shard degrades instead of failing the command). Wired on both
monoio and tokio sharded write paths.

Verified: lib tests 4109 pass, fmt + clippy clean (default and
runtime-tokio,jemalloc), smoke-tested at --shards 1 and --shards 4
(8 graphs created round-robin all listed; SELECT 2/3/5 writes all
reported with cross-shard sums).

author: Tin Dang
@TinDang97 TinDang97 force-pushed the feat/listing-parity branch from 63b79fd to 758e95a Compare July 11, 2026 04:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/conn/handler_single.rs (1)

875-904: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Run the keyspace-aware INFO path even without replication state.

When repl_state is None, the code falls through at Line 903 to generic dispatch, so single-shard INFO still reports only the selected database as db0 and hides other non-empty databases. Build the keyspace response unconditionally; append replication details only when repl_state is present.

Proposed fix
 if cmd.eq_ignore_ascii_case(b"INFO") {
-    if let Some(ref rs) = repl_state {
-        // build keyspace response
-        // append replication response
-        continue;
-    }
-    // Fall through to normal dispatch if no repl_state
+    // Build the keyspace-aware response unconditionally.
+    // Append replication details only when available.
+    continue;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_single.rs` around lines 875 - 904, Make the
keyspace-aware INFO handling unconditional instead of gating it on repl_state:
in the INFO branch, always build the per-database keyspace, call
conn_cmd::info_with_keyspace, and append replication details only when
repl_state is Some (using try_read as currently). Remove the fallback to generic
dispatch so INFO reports all non-empty databases even without replication state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/conn/handler_monoio/dispatch.rs`:
- Around line 641-652: Gate the cross-shard call to coordinate_keyspace_info in
the INFO handling path so it runs only when the requested sections include
Keyspace. Reuse the section parsing logic from conn_cmd::info, covering explicit
Keyspace, default, and all-section requests; otherwise skip coordination and
pass an empty or absent keyspace result to info_with_keyspace while preserving
existing responses.

In `@src/server/conn/handler_monoio/write.rs`:
- Around line 857-891: Update the GRAPH.LIST fan-out handling in the sharded
branch to await all remote replies concurrently instead of serially iterating
over receivers. Use FuturesUnordered, join_all, or an equivalent concurrent
pattern around recv_reply_bounded so each shard’s timeout runs in parallel,
while preserving successful partial replies in remotes before merging with the
local response.

In `@src/server/conn/handler_sharded/write.rs`:
- Around line 764-798: GRAPH.LIST waits for remote shard replies sequentially,
allowing one stalled shard to delay all others. Update the reply-collection loop
in the sharded GRAPH.LIST handling block to await all receivers concurrently,
such as by joining or spawning bounded receive futures, then collect each
successful response into remotes before merging with the local result via
merge_graph_list_responses.

In `@src/shard/coordinator.rs`:
- Around line 1814-1822: Update the receiver aggregation loop in the
coordinator’s keyspace stats method to await all shard replies concurrently
rather than calling recv_reply_bounded(rx).await serially. Spawn or join one
bounded receive future per receiver, then aggregate each successful result into
totals so the operation completes within a single XSHARD_REPLY_TIMEOUT.

---

Outside diff comments:
In `@src/server/conn/handler_single.rs`:
- Around line 875-904: Make the keyspace-aware INFO handling unconditional
instead of gating it on repl_state: in the INFO branch, always build the
per-database keyspace, call conn_cmd::info_with_keyspace, and append replication
details only when repl_state is Some (using try_read as currently). Remove the
fallback to generic dispatch so INFO reports all non-empty databases even
without replication state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 281da787-ff91-4582-b018-b2850f84b743

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8511e and 758e95a.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • src/command/connection.rs
  • src/command/graph/graph_read.rs
  • src/command/graph/mod.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_monoio/write.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_sharded/write.rs
  • src/server/conn/handler_single.rs
  • src/shard/coordinator.rs
  • src/shard/dispatch.rs
  • src/shard/spsc_handler.rs

Comment on lines +641 to +652
// # Keyspace parity: per-db (keys, expires) summed across ALL shards —
// previously the section reported the selected db's LOCAL count as db0
// (other dbs invisible, other shards uncounted).
let keyspace = crate::shard::coordinator::coordinate_keyspace_info(
ctx.shard_id,
ctx.num_shards,
&ctx.dispatch_tx,
&ctx.spsc_notifiers,
)
.await;
let response_text = crate::shard::slice::with_shard_db(conn.selected_db, |db| {
let resp_frame = conn_cmd::info_readonly(db, cmd_args);
let resp_frame = conn_cmd::info_with_keyspace(db, cmd_args, &keyspace);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Only scatter when the requested INFO section includes Keyspace.

Line 644 runs the cross-shard gather for INFO server, INFO clients, etc.; info_with_keyspace then returns the unchanged local response when # Keyspace is absent. Gate this call using the same section parsing as conn_cmd::info, including the default/all forms, so unrelated diagnostics do not incur shard timeouts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_monoio/dispatch.rs` around lines 641 - 652, Gate the
cross-shard call to coordinate_keyspace_info in the INFO handling path so it
runs only when the requested sections include Keyspace. Reuse the section
parsing logic from conn_cmd::info, covering explicit Keyspace, default, and
all-section requests; otherwise skip coordination and pass an empty or absent
keyspace result to info_with_keyspace while preserving existing responses.

Comment on lines +857 to +891
if ctx.num_shards > 1 && cmd.eq_ignore_ascii_case(b"GRAPH.LIST") {
let mut receivers = Vec::with_capacity(ctx.num_shards - 1);
for target in 0..ctx.num_shards {
if target == ctx.shard_id {
continue;
}
let (reply_tx, reply_rx) = crate::runtime::channel::oneshot();
let msg = crate::shard::dispatch::ShardMessage::GraphCommand {
command: std::sync::Arc::new(frame.clone()),
reply_tx,
};
let _ = crate::shard::coordinator::spsc_send(
&ctx.dispatch_tx,
ctx.shard_id,
target,
msg,
&ctx.spsc_notifiers,
)
.await;
receivers.push(reply_rx);
}
let mut remotes = Vec::with_capacity(receivers.len());
for rx in receivers {
if let Ok(f) = crate::shard::coordinator::recv_reply_bounded(rx).await {
remotes.push(f);
}
}
let local = crate::shard::slice::with_shard(|s| {
crate::command::graph::dispatch_graph_read(&s.graph_store, cmd, cmd_args, None)
});
responses.push(crate::command::graph::merge_graph_list_responses(
local, &remotes,
));
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
grep -n '^futures' Cargo.toml
rg -n 'join_all|FuturesUnordered|select_all' src --type rust
ast-grep run --pattern 'fn race2($$$) { $$$ }' --lang rust src/runtime

Repository: pilotspace/moon

Length of output: 1359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first
ast-grep outline src/server/conn/handler_monoio/write.rs --view expanded
ast-grep outline src/shard/coordinator.rs --view expanded
ast-grep outline src/runtime/channel.rs --view expanded
ast-grep outline src/server/conn/blocking.rs --view expanded

# Inspect the graph-list branch and the bounded receive helper
sed -n '840,910p' src/server/conn/handler_monoio/write.rs
rg -n 'recv_reply_bounded|XSHARD_REPLY_TIMEOUT|FuturesUnordered|join_all|select_all' src/shard src/runtime src/server/conn --type rust
sed -n '1,220p' src/shard/coordinator.rs
sed -n '230,390p' src/runtime/channel.rs
sed -n '200,290p' src/server/conn/blocking.rs

Repository: pilotspace/moon

Length of output: 30386


Await the GRAPH.LIST replies concurrently.
The current serial for rx in receivers loop can turn K stalled shards into K × XSHARD_REPLY_TIMEOUT total latency. Use FuturesUnordered or join_all so the per-shard timeouts run in parallel and partial results return promptly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_monoio/write.rs` around lines 857 - 891, Update the
GRAPH.LIST fan-out handling in the sharded branch to await all remote replies
concurrently instead of serially iterating over receivers. Use FuturesUnordered,
join_all, or an equivalent concurrent pattern around recv_reply_bounded so each
shard’s timeout runs in parallel, while preserving successful partial replies in
remotes before merging with the local response.

Comment on lines +764 to +798
if ctx.num_shards > 1 && cmd.eq_ignore_ascii_case(b"GRAPH.LIST") {
let mut receivers = Vec::with_capacity(ctx.num_shards - 1);
for target in 0..ctx.num_shards {
if target == ctx.shard_id {
continue;
}
let (reply_tx, reply_rx) = crate::runtime::channel::oneshot();
let msg = crate::shard::dispatch::ShardMessage::GraphCommand {
command: std::sync::Arc::new(frame.clone()),
reply_tx,
};
let _ = crate::shard::coordinator::spsc_send(
&ctx.dispatch_tx,
ctx.shard_id,
target,
msg,
&ctx.spsc_notifiers,
)
.await;
receivers.push(reply_rx);
}
let mut remotes = Vec::with_capacity(receivers.len());
for rx in receivers {
if let Ok(f) = crate::shard::coordinator::recv_reply_bounded(rx).await {
remotes.push(f);
}
}
let local = crate::shard::slice::with_shard(|s| {
crate::command::graph::dispatch_graph_read(&s.graph_store, cmd, cmd_args, None)
});
responses.push(crate::command::graph::merge_graph_list_responses(
local, &remotes,
));
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and symbols first.
ast-grep outline src/server/conn/handler_sharded/write.rs --view expanded | sed -n '1,220p'
printf '\n---\n'
ast-grep outline src/server/conn/handler_monoio/write.rs --view expanded | sed -n '1,240p'
printf '\n---\n'
rg -n "recv_reply_bounded|XSHARD_REPLY_TIMEOUT|GraphCommand|GRAPH.LIST|dispatch_graph_read|merge_graph_list_responses" src/server src/shard src/command -S

Repository: pilotspace/moon

Length of output: 9011


🏁 Script executed:

ast-grep outline src/server/conn/handler_sharded/write.rs --view expanded | sed -n '1,220p'
printf '\n---\n'
ast-grep outline src/server/conn/handler_monoio/write.rs --view expanded | sed -n '1,240p'
printf '\n---\n'
rg -n "recv_reply_bounded|XSHARD_REPLY_TIMEOUT|GraphCommand|GRAPH.LIST|dispatch_graph_read|merge_graph_list_responses" src/server src/shard src/command -S

Repository: pilotspace/moon

Length of output: 9011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '746,802p' src/server/conn/handler_sharded/write.rs
printf '\n---\n'
sed -n '839,893p' src/server/conn/handler_monoio/write.rs
printf '\n---\n'
sed -n '191,230p' src/shard/coordinator.rs

Repository: pilotspace/moon

Length of output: 6677


Collect shard replies concurrently. The for loop waits on recv_reply_bounded(rx).await one shard at a time, so a stalled early shard can delay already-ready replies and stretch GRAPH.LIST latency by roughly N × XSHARD_REPLY_TIMEOUT.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_sharded/write.rs` around lines 764 - 798, GRAPH.LIST
waits for remote shard replies sequentially, allowing one stalled shard to delay
all others. Update the reply-collection loop in the sharded GRAPH.LIST handling
block to await all receivers concurrently, such as by joining or spawning
bounded receive futures, then collect each successful response into remotes
before merging with the local result via merge_graph_list_responses.

Comment thread src/shard/coordinator.rs
Comment on lines +1814 to +1822
for rx in receivers {
if let Ok(stats) = recv_reply_bounded(rx).await {
for (i, (k, e)) in stats.into_iter().enumerate() {
if let Some(t) = totals.get_mut(i) {
t.0 += k;
t.1 += e;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Await shard replies concurrently.

Line 1814 waits receivers serially, so each unavailable shard adds another XSHARD_REPLY_TIMEOUT; INFO Keyspace can block for (shards - 1) × timeout despite already-available replies. Drive all bounded receives concurrently so partial results return within one timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shard/coordinator.rs` around lines 1814 - 1822, Update the receiver
aggregation loop in the coordinator’s keyspace stats method to await all shard
replies concurrently rather than calling recv_reply_bounded(rx).await serially.
Spawn or join one bounded receive future per receiver, then aggregate each
successful result into totals so the operation completes within a single
XSHARD_REPLY_TIMEOUT.

@pilotspacex-byte pilotspacex-byte merged commit ccea4ab into main Jul 11, 2026
15 checks passed
@TinDang97 TinDang97 deleted the feat/listing-parity branch July 11, 2026 05:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants