Skip to content

feat: add message timestamp seeking support to broker and update dashboard inspection UI#34

Open
samuel025 wants to merge 3 commits into
mainfrom
dashboard-updates
Open

feat: add message timestamp seeking support to broker and update dashboard inspection UI#34
samuel025 wants to merge 3 commits into
mainfrom
dashboard-updates

Conversation

@samuel025

@samuel025 samuel025 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added Topics and Consumers pages to the dashboard.
    • Added topic browsing and message inspection with offset or timestamp seeking.
    • Added consumer-group lag, offsets, and active member visibility.
    • Added an admin API for topics, consumers, and stored messages.
    • Added adjustable throughput history windows and more detailed telemetry histories.
  • Improvements

    • Improved throughput and latency display precision and formatting.
    • Extended telemetry history retention and enhanced connection stability.
    • Improved timestamp-based message seeking validation.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Admin observability

Layer / File(s) Summary
Storage and offset access contracts
drmq-broker/src/main/java/com/drmq/broker/MessageStore.java, drmq-broker/src/main/java/com/drmq/broker/OffsetManager.java
MessageStore tracks topic head offsets and changes cache retrieval behavior; OffsetManager exposes committed-offset snapshots.
Admin API and broker lifecycle
drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java, drmq-broker/src/main/java/com/drmq/broker/BrokerServer.java, drmq-broker/src/main/java/com/drmq/broker/ConsumerGroupCoordinator.java
The broker serves topic, consumer lag, and message endpoints, starts and stops the admin server, and removes unchanged DLQ/NACK comments.
Topic and consumer dashboard views
drmq-dashboard/src/App.tsx, drmq-dashboard/src/pages/Topics.tsx, drmq-dashboard/src/pages/Consumers.tsx
New routes display topics, inspect stored messages with offset or timestamp seeking, and show consumer-group lag and active members.
Timestamp seek validation
drmq-python-client/test_time_seek.py
A standalone script publishes timestamped messages, seeks by time, polls results, and validates the target message.

Telemetry history and dashboard controls

Layer / File(s) Summary
Telemetry history and payload
drmq-broker/src/main/java/com/drmq/broker/TelemetryWebSocketServer.java
Telemetry histories use concurrent collections, retain 300 samples, expose category-specific throughput arrays, and use four-decimal rounding.
Dashboard telemetry controls
drmq-dashboard/src/pages/Dashboard.tsx
Throughput history supports a selectable 30–300 second window with revised MB/s formatting and chart labels.

Raft state visibility

Layer / File(s) Summary
Volatile Raft state
drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java
Core Raft term, vote, role, commit, apply, and leader fields are declared volatile.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant AdminHttpServer
  participant MessageStore
  participant OffsetManager
  Dashboard->>AdminHttpServer: Request topics, consumers, or messages
  AdminHttpServer->>MessageStore: Read topics, head offsets, or messages
  AdminHttpServer->>OffsetManager: Read committed offsets
  AdminHttpServer-->>Dashboard: Return JSON response
Loading

Possibly related PRs

  • samuel025/DRMQ#5: Introduces the consumer-offset and message-store APIs used by the admin endpoints.
  • samuel025/DRMQ#24: Overlaps with the telemetry WebSocket payload and dashboard telemetry changes.
  • samuel025/DRMQ#9: Also modifies BrokerServer startup and shutdown lifecycle wiring.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: timestamp-based message seeking plus dashboard inspection updates.
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 dashboard-updates

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.

@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: 12

🧹 Nitpick comments (1)
drmq-dashboard/src/pages/Topics.tsx (1)

72-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Admin API base URL (http://localhost:9392) is hardcoded and duplicated across pages. All three sites bake in the same non-configurable host/port, which only ever targets a single broker and will drift if the admin port scheme changes (BrokerServer derives it as config.getPort() + 300).

  • drmq-dashboard/src/pages/Topics.tsx#L72-L86: extract the base URL (fetchTopics) into a shared constant/env var (e.g. VITE_ADMIN_API_URL).
  • drmq-dashboard/src/pages/Topics.tsx#L25-L60: use the same shared base URL constant in fetchMessages's URL construction.
  • drmq-dashboard/src/pages/Consumers.tsx#L23-L36: use the same shared base URL constant in fetchConsumers.
🤖 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 `@drmq-dashboard/src/pages/Topics.tsx` around lines 72 - 86, Replace the
hardcoded admin API host in fetchTopics with a shared configurable base URL,
preferably sourced from VITE_ADMIN_API_URL with the existing local value as its
default. Reuse that same constant when constructing fetchMessages URLs in
drmq-dashboard/src/pages/Topics.tsx lines 25-60 and fetchConsumers URLs in
drmq-dashboard/src/pages/Consumers.tsx lines 23-36; all three sites require this
change.
🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java`:
- Around line 88-90: Update the key parsing in the allOffsets iteration to split
the entry key at only the first "/" so topics may contain additional slashes.
Keep the existing two-part validation and consumer-lag handling, while
preserving the known group-name ambiguity in the "group/topic" key format.
- Around line 186-189: Update the exception response in AdminHttpServer’s
message-request handler to construct the error object through the existing JSON
serializer (such as Gson) instead of concatenating e.getMessage() into a JSON
string. Ensure messages containing quotes or other special characters remain
valid JSON so Topics.tsx can read data.error.
- Around line 20-41: Update the AdminHttpServer constructor and request-handling
flow to bind HttpServer to loopback instead of all interfaces by default, and
require authentication before serving /api/topics, /api/consumers, or
/api/messages. Replace the wildcard CORS behavior with an authenticated,
restricted origin policy, using the existing handlers such as handleTopics,
handleConsumers, and handleMessages without exposing data to unauthenticated
clients.
- Line 40: Replace the null executor configured in AdminHttpServer with a small
dedicated executor for the admin HTTP server, using the server’s setExecutor
call. Ensure the pool is suitable for handling concurrent dashboard endpoints so
slow /api/messages requests do not block /api/topics or /api/consumers.

In `@drmq-broker/src/main/java/com/drmq/broker/MessageStore.java`:
- Around line 234-238: Make the update to topicHeadOffsets in the append path
atomic: replace the separate head.get()/head.set() comparison with an AtomicLong
update operation that retains the maximum of the current value and offset. Keep
the existing computeIfAbsent initialization and topic-scoped behavior unchanged.
- Around line 709-718: Update the cache lookup in getMessagesFrom to avoid
requiring an exact fromOffset key: iterate cached entries and collect those
whose keys are greater than or equal to fromOffset. Remove or adjust the
containsKey(fromOffset) early return so normal consumption can use the bounded
cache fast path, while preserving the existing result construction and ordering
behavior.
- Around line 309-317: Update the head-offset update inside the batch loop in
MessageStore so the comparison and update use an atomic max operation on the
per-topic AtomicLong, matching the concurrency-safe behavior required by
append(). Preserve the existing indexing, caching, and counter updates.

In `@drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java`:
- Around line 48-57: Add a lock-protected immutable Raft status snapshot
containing state, currentTerm, leaderId, commitIndex, and lastApplied, with a
single accessor that captures all values atomically. Update
buildTelemetryPayload() to use this snapshot instead of separately calling the
individual getters, while preserving the existing telemetry fields and values.

In `@drmq-dashboard/src/pages/Dashboard.tsx`:
- Around line 333-340: Add an accessible name to the range input controlling
historyWindowSeconds, using either a visible label associated with the input or
aria-label="Throughput history window". Preserve the existing slider behavior
and styling.
- Around line 331-346: Update MockTelemetryProvider and its mock-history
initialization to retain and seed 300 samples, matching the maximum
historyWindowSeconds selectable by the Dashboard range control. Ensure
non-WebSocket charts display the full selected 60–300 second window while
preserving the existing 30-second sampling behavior.

In `@drmq-dashboard/src/pages/Topics.tsx`:
- Around line 25-70: Update the inspector-opening flow around fetchMessages and
the inspectorTopic useEffect so the initial fetch explicitly uses offset 0
rather than the stale inspectOffset captured before setInspectOffset completes.
Reset inspectTimestamp when opening a new topic, and ensure the first request
does not apply the previous timestamp filter; preserve existing offset/timestamp
behavior for subsequent fetches.

In `@drmq-python-client/test_time_seek.py`:
- Line 9: Update the test topic construction in test_time_seek.py to append a
UUID-based suffix instead of relying on int(time.time()) alone. Preserve the
existing topic prefix while ensuring each test invocation receives a
collision-proof unique topic.

---

Nitpick comments:
In `@drmq-dashboard/src/pages/Topics.tsx`:
- Around line 72-86: Replace the hardcoded admin API host in fetchTopics with a
shared configurable base URL, preferably sourced from VITE_ADMIN_API_URL with
the existing local value as its default. Reuse that same constant when
constructing fetchMessages URLs in drmq-dashboard/src/pages/Topics.tsx lines
25-60 and fetchConsumers URLs in drmq-dashboard/src/pages/Consumers.tsx lines
23-36; all three sites require this change.
🪄 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: d7fe8a45-1a10-4af3-841e-967e15f2ccf4

📥 Commits

Reviewing files that changed from the base of the PR and between c63d307 and 68b9fc3.

📒 Files selected for processing (12)
  • drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java
  • drmq-broker/src/main/java/com/drmq/broker/BrokerServer.java
  • drmq-broker/src/main/java/com/drmq/broker/ConsumerGroupCoordinator.java
  • drmq-broker/src/main/java/com/drmq/broker/MessageStore.java
  • drmq-broker/src/main/java/com/drmq/broker/OffsetManager.java
  • drmq-broker/src/main/java/com/drmq/broker/TelemetryWebSocketServer.java
  • drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java
  • drmq-dashboard/src/App.tsx
  • drmq-dashboard/src/pages/Consumers.tsx
  • drmq-dashboard/src/pages/Dashboard.tsx
  • drmq-dashboard/src/pages/Topics.tsx
  • drmq-python-client/test_time_seek.py
💤 Files with no reviewable changes (1)
  • drmq-broker/src/main/java/com/drmq/broker/ConsumerGroupCoordinator.java

Comment on lines +20 to +41
public class AdminHttpServer {
private static final Logger logger = LoggerFactory.getLogger(AdminHttpServer.class);
private final HttpServer server;
private final MessageStore messageStore;
private final OffsetManager offsetManager;
private final ConsumerGroupCoordinator groupCoordinator;
private final Gson gson = new Gson();

public AdminHttpServer(int port, MessageStore messageStore, OffsetManager offsetManager, ConsumerGroupCoordinator groupCoordinator) throws IOException {
this.messageStore = messageStore;
this.offsetManager = offsetManager;
this.groupCoordinator = groupCoordinator;

this.server = HttpServer.create(new InetSocketAddress(port), 0);

this.server.createContext("/api/topics", this::handleTopics);
this.server.createContext("/api/consumers", this::handleConsumers);
this.server.createContext("/api/messages", this::handleMessages);

// CORS and standard executor
this.server.setExecutor(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=java -C2 'Authorization|BasicAuth|ApiKey|apiKey|Authenticat' drmq-broker/src/main/java/com/drmq/broker

Repository: samuel025/DRMQ

Length of output: 784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- AdminHttpServer.java outline ---'
ast-grep outline drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java --view expanded || true

echo '--- Relevant AdminHttpServer.java slices ---'
sed -n '1,260p' drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java

echo '--- Search for admin server construction / port binding / auth config ---'
rg -n --type=java -C2 'new AdminHttpServer|admin.*port|adminPort|HttpServer.create|setExecutor\(|Access-Control-Allow-Origin|Authorization|Basic|auth' drmq-broker/src/main/java

Repository: samuel025/DRMQ

Length of output: 12774


Lock down the admin API
HttpServer.create(new InetSocketAddress(port), 0) binds to all interfaces, and the handlers also return Access-Control-Allow-Origin: * with no auth check. That makes /api/messages and /api/consumers readable by any reachable client, including raw payloads and consumer offsets. Bind to loopback by default and add authentication before exposing this outside local development.

🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java` around lines
20 - 41, Update the AdminHttpServer constructor and request-handling flow to
bind HttpServer to loopback instead of all interfaces by default, and require
authentication before serving /api/topics, /api/consumers, or /api/messages.
Replace the wildcard CORS behavior with an authenticated, restricted origin
policy, using the existing handlers such as handleTopics, handleConsumers, and
handleMessages without exposing data to unauthenticated clients.

this.server.createContext("/api/messages", this::handleMessages);

// CORS and standard executor
this.server.setExecutor(null);

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:

sed -n '1,220p' drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java

Repository: samuel025/DRMQ

Length of output: 8486


🌐 Web query:

JDK HttpServer.setExecutor(null) default executor single threaded behavior

💡 Result:

When you call HttpServer.setExecutor(null) (or fail to set an executor before starting the server), the JDK HttpServer uses a default executor implementation [1][2][3]. This default executor is synchronous and single-threaded [4][5]. Internally, the server uses a static class called DefaultExecutor, which executes tasks by calling task.run directly within the thread that manages the request dispatching [4][5]. Consequently, all HTTP requests are handled serially in the same thread, meaning they are processed in a blocking manner [5]. As a result, the server cannot process multiple requests in parallel when using the default executor [5]. To enable parallel request processing, you must provide a custom Executor, such as one returned by java.util.concurrent.Executors (e.g., Executors.newCachedThreadPool or Executors.newFixedThreadPool) [5].

Citations:


Use a dedicated executor for the admin server. setExecutor(null) leaves requests on the JDK default executor, so one slow /api/messages call can block /api/topics and /api/consumers. Use a small dedicated pool here to keep the dashboard responsive.

🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java` at line 40,
Replace the null executor configured in AdminHttpServer with a small dedicated
executor for the admin HTTP server, using the server’s setExecutor call. Ensure
the pool is suitable for handling concurrent dashboard endpoints so slow
/api/messages requests do not block /api/topics or /api/consumers.

Comment on lines +88 to +90
for (java.util.Map.Entry<String, Long> entry : allOffsets.entrySet()) {
String[] parts = entry.getKey().split("/");
if (parts.length != 2) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

split("/") silently drops any consumer/topic entry whose topic name contains "/".

OffsetManager.key() joins as group + "/" + topic. If topic itself contains a "/", split("/") yields more than 2 parts and the if (parts.length != 2) continue; guard silently skips that group/topic pair from the consumer-lag view instead of parsing it correctly.

🐛 Proposed fix: limit split to 2 parts
-            String[] parts = entry.getKey().split("/");
+            String[] parts = entry.getKey().split("/", 2);
Note this only fixes slash-containing topic names; a slash-containing consumer group name would still be mis-parsed, which is a pre-existing ambiguity in the `"group/topic"` key scheme.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (java.util.Map.Entry<String, Long> entry : allOffsets.entrySet()) {
String[] parts = entry.getKey().split("/");
if (parts.length != 2) continue;
for (java.util.Map.Entry<String, Long> entry : allOffsets.entrySet()) {
String[] parts = entry.getKey().split("/", 2);
if (parts.length != 2) 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 `@drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java` around lines
88 - 90, Update the key parsing in the allOffsets iteration to split the entry
key at only the first "/" so topics may contain additional slashes. Keep the
existing two-part validation and consumer-lag handling, while preserving the
known group-name ambiguity in the "group/topic" key format.

Comment on lines +186 to +189
} catch (Exception e) {
logger.error("Error handling messages request", e);
sendJsonResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unescaped exception message can produce malformed JSON error responses.

e.getMessage() is concatenated directly into a JSON string literal. NumberFormatException messages (a likely cause here, from Long.parseLong/Integer.parseInt on bad query params) contain embedded double quotes (e.g. For input string: "abc"), which breaks the resulting JSON and defeats Topics.tsx's data.error handling.

🐛 Proposed fix using Gson to build the error object
         } catch (Exception e) {
             logger.error("Error handling messages request", e);
-            sendJsonResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}");
+            JsonObject err = new JsonObject();
+            err.addProperty("error", e.getMessage());
+            sendJsonResponse(exchange, 500, gson.toJson(err));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (Exception e) {
logger.error("Error handling messages request", e);
sendJsonResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}");
}
} catch (Exception e) {
logger.error("Error handling messages request", e);
JsonObject err = new JsonObject();
err.addProperty("error", e.getMessage());
sendJsonResponse(exchange, 500, gson.toJson(err));
}
🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/AdminHttpServer.java` around lines
186 - 189, Update the exception response in AdminHttpServer’s message-request
handler to construct the error object through the existing JSON serializer (such
as Gson) instead of concatenating e.getMessage() into a JSON string. Ensure
messages containing quotes or other special characters remain valid JSON so
Topics.tsx can read data.error.

Comment on lines +234 to +238

AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1));
if (offset > head.get()) {
head.set(offset);
}

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

Non-atomic "set-if-greater" on topicHeadOffsets races under concurrent appends to the same topic.

head.get()/head.set() here runs outside the synchronized(topicLock) block, only under the shared globalLock.readLock(). Two threads appending to the same topic concurrently can interleave the check-then-set and leave head at a stale, lower value than the true max offset — this directly feeds getHeadOffset(), which AdminHttpServer.handleConsumers uses to compute consumer lag.

🔒 Proposed fix using atomic max update
-            AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1));
-            if (offset > head.get()) {
-                head.set(offset);
-            }
+            topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1))
+                    .accumulateAndGet(offset, Math::max);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1));
if (offset > head.get()) {
head.set(offset);
}
topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1))
.accumulateAndGet(offset, Math::max);
🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/MessageStore.java` around lines 234
- 238, Make the update to topicHeadOffsets in the append path atomic: replace
the separate head.get()/head.set() comparison with an AtomicLong update
operation that retains the maximum of the current value and offset. Keep the
existing computeIfAbsent initialization and topic-scoped behavior unchanged.

Comment on lines +48 to +57
private volatile long currentTerm;
private volatile String votedFor;
private final RaftLog raftLog;

// Volatile state
private RaftState state;
private long commitIndex;
private long lastApplied;
private long lastAppliedTerm;
private String leaderId;
private volatile RaftState state;
private volatile long commitIndex;
private volatile long lastApplied;
private volatile long lastAppliedTerm;
private volatile String leaderId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant files and their structure around the referenced lines.
git ls-files | rg 'drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java|TelemetryWebSocketServer.java$'
echo '--- RaftNode outline ---'
ast-grep outline drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java --view expanded || true
echo '--- TelemetryWebSocketServer outline ---'
ast-grep outline drmq-broker/src/main/java/com/drmq/broker/raft/TelemetryWebSocketServer.java --view expanded || true

echo '--- RaftNode excerpt ---'
sed -n '1,260p' drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java

echo '--- TelemetryWebSocketServer excerpt ---'
sed -n '200,320p' drmq-broker/src/main/java/com/drmq/broker/raft/TelemetryWebSocketServer.java

Repository: samuel025/DRMQ

Length of output: 14114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the likely getters and mutation sites if the excerpts are large.
rg -n "getCurrentTerm|getVotedFor|getState|getCommitIndex|getLastApplied|getLastAppliedTerm|getLeaderId|stepDown|apply|leaderId|commitIndex|lastApplied" \
  drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java \
  drmq-broker/src/main/java/com/drmq/broker/raft/TelemetryWebSocketServer.java

Repository: samuel025/DRMQ

Length of output: 12087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the relevant fields are read together anywhere else.
rg -n "currentTerm|votedFor|state|commitIndex|lastApplied|lastAppliedTerm|leaderId" drmq-broker/src/main/java/com/drmq/broker/raft -g '!**/target/**'

Repository: samuel025/DRMQ

Length of output: 23585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the minimal relevant sections.
for f in \
  drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java \
  drmq-broker/src/main/java/com/drmq/broker/raft/TelemetryWebSocketServer.java
do
  echo "===== $f ====="
  wc -l "$f"
done

echo '===== RaftNode relevant section ====='
sed -n '1,180p' drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java

echo '===== TelemetryWebSocketServer relevant section ====='
sed -n '220,300p' drmq-broker/src/main/java/com/drmq/broker/raft/TelemetryWebSocketServer.java

Repository: samuel025/DRMQ

Length of output: 488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f=drmq-broker/src/main/java/com/drmq/broker/TelemetryWebSocketServer.java

echo '--- outline ---'
ast-grep outline "$f" --view expanded || true

echo '--- file size ---'
wc -l "$f"

echo '--- relevant regions ---'
# Read likely telemetry/status methods without dumping the whole file if large.
rg -n "getState\\(|getCurrentTerm\\(|getLeaderId\\(|getCommitIndex\\(|getLastApplied\\(|status|telemetry|publish|websocket|snapshot" "$f"
echo '--- excerpt 1 ---'
sed -n '1,260p' "$f"
echo '--- excerpt 2 ---'
sed -n '260,420p' "$f"

Repository: samuel025/DRMQ

Length of output: 21763


Expose a coherent Raft status snapshot.

buildTelemetryPayload() reads getState(), getCurrentTerm(), getLeaderId(), getCommitIndex(), and getLastApplied() separately, so telemetry can publish a mixed Raft state during role changes or snapshot apply. Add a lock-protected immutable snapshot and use it here.

🤖 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 `@drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java` around lines 48
- 57, Add a lock-protected immutable Raft status snapshot containing state,
currentTerm, leaderId, commitIndex, and lastApplied, with a single accessor that
captures all values atomically. Update buildTelemetryPayload() to use this
snapshot instead of separately calling the individual getters, while preserving
the existing telemetry fields and values.

Comment on lines +331 to +346
<div className="mt-4 pt-4 border-t border-white/5 flex items-center justify-between gap-4">
<span className="mono text-[10px] text-zinc-500 w-16">30s</span>
<input
type="range"
min="30"
max="300"
step="30"
value={historyWindowSeconds}
onChange={(e) => setHistoryWindowSeconds(Number(e.target.value))}
className="flex-1 h-1.5 bg-white/10 rounded-full appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-cyan-400 [&::-webkit-slider-thumb]:rounded-full cursor-pointer"
/>
<span className="mono text-[10px] text-zinc-500 w-16 text-right">5min</span>
</div>
<div className="text-center mt-1">
<span className="mono text-[9px] text-zinc-600 tracking-wider">VIEWING LAST {historyWindowSeconds >= 60 ? `${historyWindowSeconds / 60} MIN` : `${historyWindowSeconds} SEC`}</span>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep mock history length consistent with the selectable window.

When VITE_USE_WEBSOCKET is not true, the dashboard uses MockTelemetryProvider, which retains only 30 samples. Selecting 60–300 seconds therefore leaves the chart at 30 seconds while claiming a longer window. Seed and retain 300 mock samples, or cap the control to available history.

🤖 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 `@drmq-dashboard/src/pages/Dashboard.tsx` around lines 331 - 346, Update
MockTelemetryProvider and its mock-history initialization to retain and seed 300
samples, matching the maximum historyWindowSeconds selectable by the Dashboard
range control. Ensure non-WebSocket charts display the full selected 60–300
second window while preserving the existing 30-second sampling behavior.

Comment on lines +333 to +340
<input
type="range"
min="30"
max="300"
step="30"
value={historyWindowSeconds}
onChange={(e) => setHistoryWindowSeconds(Number(e.target.value))}
className="flex-1 h-1.5 bg-white/10 rounded-full appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-cyan-400 [&::-webkit-slider-thumb]:rounded-full cursor-pointer"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give the range control an accessible name.

Add a <label> or aria-label="Throughput history window" so assistive technology can identify this slider.

🤖 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 `@drmq-dashboard/src/pages/Dashboard.tsx` around lines 333 - 340, Add an
accessible name to the range input controlling historyWindowSeconds, using
either a visible label associated with the input or aria-label="Throughput
history window". Preserve the existing slider behavior and styling.

Comment on lines +25 to +70
const fetchMessages = async () => {
if (!inspectorTopic) return;
setInspectLoading(true);
setInspectError(null);
try {
const offsetParam = inspectOffset === '' ? 0 : inspectOffset;
const limitParam = inspectLimit === '' ? 1 : inspectLimit;

let url = `http://localhost:9392/api/messages?topic=${inspectorTopic}&limit=${limitParam}`;
if (inspectTimestamp) {
const ts = new Date(inspectTimestamp).getTime();
if (!isNaN(ts)) {
url += `&timestamp=${ts}`;
} else {
url += `&offset=${offsetParam}`;
}
} else {
url += `&offset=${offsetParam}`;
}

const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
if (data.error) throw new Error(data.error);
setMessages(data);

// Snap the input offset to the actual returned offset to show compaction jumps
if (data && data.length > 0) {
setInspectOffset(data[0].offset);
}
} catch (err: any) {
setInspectError(err.message || 'Failed to fetch messages');
} finally {
setInspectLoading(false);
}
};

// Automatically fetch messages when inspector opens
useEffect(() => {
if (inspectorTopic) {
setInspectOffset(0); // Reset to 0 when opening new topic
fetchMessages();
} else {
setMessages([]);
}
}, [inspectorTopic]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Opening the inspector for a new topic can fetch with a stale offset and a leftover timestamp filter from the previous topic.

setInspectOffset(0) only schedules a state update; the immediately-following fetchMessages() call in the same effect still closes over the pre-update inspectOffset (React batches the update), so the reset to 0 doesn't actually take effect for this first fetch. Additionally, inspectTimestamp is never reset here, so a timestamp seek left over from a previously inspected topic is silently reapplied to the newly opened topic. Since if (inspectTimestamp) takes priority over offset in fetchMessages, this can produce wrong/empty results for the new topic.

🐛 Proposed fix: pass an explicit override offset and reset the timestamp filter
-  const fetchMessages = async () => {
+  const fetchMessages = async (overrideOffset?: number) => {
     if (!inspectorTopic) return;
     setInspectLoading(true);
     setInspectError(null);
     try {
-      const offsetParam = inspectOffset === '' ? 0 : inspectOffset;
+      const offsetParam = overrideOffset ?? (inspectOffset === '' ? 0 : inspectOffset);
       const limitParam = inspectLimit === '' ? 1 : inspectLimit;
   useEffect(() => {
     if (inspectorTopic) {
       setInspectOffset(0); // Reset to 0 when opening new topic
-      fetchMessages();
+      setInspectTimestamp('');
+      fetchMessages(0);
     } else {
       setMessages([]);
     }
   }, [inspectorTopic]);
🤖 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 `@drmq-dashboard/src/pages/Topics.tsx` around lines 25 - 70, Update the
inspector-opening flow around fetchMessages and the inspectorTopic useEffect so
the initial fetch explicitly uses offset 0 rather than the stale inspectOffset
captured before setInspectOffset completes. Reset inspectTimestamp when opening
a new topic, and ensure the first request does not apply the previous timestamp
filter; preserve existing offset/timestamp behavior for subsequent fetches.


def run_test():
servers = "localhost:9092,localhost:9093,localhost:9094"
topic = "time-seek-topic-" + str(int(time.time()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the test topic collision-proof.

Second-resolution names collide for parallel or rapid runs, allowing another invocation’s messages to cause a false pass or failure. Use a UUID suffix.

Proposed fix
+import uuid
 import time
 ...
-    topic = "time-seek-topic-" + str(int(time.time()))
+    topic = f"time-seek-topic-{uuid.uuid4().hex}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
topic = "time-seek-topic-" + str(int(time.time()))
import uuid
import time
...
topic = f"time-seek-topic-{uuid.uuid4().hex}"
🤖 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 `@drmq-python-client/test_time_seek.py` at line 9, Update the test topic
construction in test_time_seek.py to append a UUID-based suffix instead of
relying on int(time.time()) alone. Preserve the existing topic prefix while
ensuring each test invocation receives a collision-proof unique topic.

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.

1 participant