-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add message timestamp seeking support to broker and update dashboard inspection UI #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
59f41e0
d334567
68b9fc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,206 @@ | ||||||||||||||||||||||
| package com.drmq.broker; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import com.google.gson.Gson; | ||||||||||||||||||||||
| import com.google.gson.JsonArray; | ||||||||||||||||||||||
| import com.google.gson.JsonObject; | ||||||||||||||||||||||
| import com.sun.net.httpserver.HttpExchange; | ||||||||||||||||||||||
| import com.sun.net.httpserver.HttpHandler; | ||||||||||||||||||||||
| import com.sun.net.httpserver.HttpServer; | ||||||||||||||||||||||
| import org.slf4j.Logger; | ||||||||||||||||||||||
| import org.slf4j.LoggerFactory; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||
| import java.io.OutputStream; | ||||||||||||||||||||||
| import java.net.InetSocketAddress; | ||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Lightweight HTTP server for administrative REST APIs. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| 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); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+20
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/brokerRepository: 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/javaRepository: samuel025/DRMQ Length of output: 12774 Lock down the admin API 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void start() { | ||||||||||||||||||||||
| server.start(); | ||||||||||||||||||||||
| logger.info("Admin HTTP Server started on port {}", server.getAddress().getPort()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void stop() { | ||||||||||||||||||||||
| server.stop(1); | ||||||||||||||||||||||
| logger.info("Admin HTTP Server stopped."); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void handleTopics(HttpExchange exchange) throws IOException { | ||||||||||||||||||||||
| addCorsHeaders(exchange); | ||||||||||||||||||||||
| if ("OPTIONS".equals(exchange.getRequestMethod())) { | ||||||||||||||||||||||
| exchange.sendResponseHeaders(204, -1); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| JsonArray topicsArray = new JsonArray(); | ||||||||||||||||||||||
| List<String> topics = messageStore.getTopics(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for (String topic : topics) { | ||||||||||||||||||||||
| JsonObject obj = new JsonObject(); | ||||||||||||||||||||||
| obj.addProperty("name", topic); | ||||||||||||||||||||||
| obj.addProperty("messageCount", messageStore.getMessageCount(topic)); | ||||||||||||||||||||||
| // The global offset is global, not per topic. | ||||||||||||||||||||||
| // We'll just return the message count for now. | ||||||||||||||||||||||
| topicsArray.add(obj); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| sendJsonResponse(exchange, 200, gson.toJson(topicsArray)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void handleConsumers(HttpExchange exchange) throws IOException { | ||||||||||||||||||||||
| addCorsHeaders(exchange); | ||||||||||||||||||||||
| if ("OPTIONS".equals(exchange.getRequestMethod())) { | ||||||||||||||||||||||
| exchange.sendResponseHeaders(204, -1); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| JsonArray groupsArray = new JsonArray(); | ||||||||||||||||||||||
| java.util.Map<String, Long> allOffsets = offsetManager.getAllOffsets(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Group by consumer group name | ||||||||||||||||||||||
| java.util.Map<String, JsonArray> groupsMap = new java.util.HashMap<>(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for (java.util.Map.Entry<String, Long> entry : allOffsets.entrySet()) { | ||||||||||||||||||||||
| String[] parts = entry.getKey().split("/"); | ||||||||||||||||||||||
| if (parts.length != 2) continue; | ||||||||||||||||||||||
|
Comment on lines
+88
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🐛 Proposed fix: limit split to 2 parts- String[] parts = entry.getKey().split("/");
+ String[] parts = entry.getKey().split("/", 2);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| String groupName = parts[0]; | ||||||||||||||||||||||
| String topicName = parts[1]; | ||||||||||||||||||||||
| long committedOffset = entry.getValue(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Calculate lag using true topic head offset rather than message count | ||||||||||||||||||||||
| // Since DRMQ uses global offsets, messageCount does not correlate to the offset values. | ||||||||||||||||||||||
| long headOffset = messageStore.getHeadOffset(topicName); | ||||||||||||||||||||||
| long lag = 0; | ||||||||||||||||||||||
| if (headOffset >= 0) { | ||||||||||||||||||||||
| long effectiveCommitted = Math.max(0, committedOffset); // -1 means none committed | ||||||||||||||||||||||
| lag = Math.max(0, (headOffset + 1) - effectiveCommitted); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| JsonObject topicObj = new JsonObject(); | ||||||||||||||||||||||
| topicObj.addProperty("topic", topicName); | ||||||||||||||||||||||
| topicObj.addProperty("headOffset", headOffset); | ||||||||||||||||||||||
| topicObj.addProperty("committedOffset", committedOffset); | ||||||||||||||||||||||
| topicObj.addProperty("lag", lag); | ||||||||||||||||||||||
| topicObj.addProperty("activeMembers", groupCoordinator.getConsumerCount(groupName, topicName)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| groupsMap.computeIfAbsent(groupName, k -> new JsonArray()).add(topicObj); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for (java.util.Map.Entry<String, JsonArray> entry : groupsMap.entrySet()) { | ||||||||||||||||||||||
| JsonObject groupObj = new JsonObject(); | ||||||||||||||||||||||
| groupObj.addProperty("groupId", entry.getKey()); | ||||||||||||||||||||||
| groupObj.add("topics", entry.getValue()); | ||||||||||||||||||||||
| groupsArray.add(groupObj); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| sendJsonResponse(exchange, 200, gson.toJson(groupsArray)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void handleMessages(HttpExchange exchange) throws IOException { | ||||||||||||||||||||||
| addCorsHeaders(exchange); | ||||||||||||||||||||||
| if ("OPTIONS".equals(exchange.getRequestMethod())) { | ||||||||||||||||||||||
| exchange.sendResponseHeaders(204, -1); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| String query = exchange.getRequestURI().getQuery(); | ||||||||||||||||||||||
| if (query == null) { | ||||||||||||||||||||||
| sendJsonResponse(exchange, 400, "{\"error\":\"Missing query parameters\"}"); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| String topic = null; | ||||||||||||||||||||||
| long offset = 0; | ||||||||||||||||||||||
| int limit = 10; | ||||||||||||||||||||||
| Long timestamp = null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for (String param : query.split("&")) { | ||||||||||||||||||||||
| String[] pair = param.split("="); | ||||||||||||||||||||||
| if (pair.length == 2) { | ||||||||||||||||||||||
| if ("topic".equals(pair[0])) topic = pair[1]; | ||||||||||||||||||||||
| else if ("offset".equals(pair[0])) offset = Long.parseLong(pair[1]); | ||||||||||||||||||||||
| else if ("limit".equals(pair[0])) limit = Integer.parseInt(pair[1]); | ||||||||||||||||||||||
| else if ("timestamp".equals(pair[0])) timestamp = Long.parseLong(pair[1]); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (topic == null) { | ||||||||||||||||||||||
| sendJsonResponse(exchange, 400, "{\"error\":\"Missing 'topic' parameter\"}"); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Limit bounds to avoid OOM | ||||||||||||||||||||||
| limit = Math.min(100, Math.max(1, limit)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (timestamp != null && timestamp > 0) { | ||||||||||||||||||||||
| offset = messageStore.findOffsetByTimestamp(topic, timestamp); | ||||||||||||||||||||||
| if (offset == -1) { | ||||||||||||||||||||||
| sendJsonResponse(exchange, 200, "[]"); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| List<com.drmq.protocol.DRMQProtocol.StoredMessage> messages = messageStore.getMessages(topic, offset, limit); | ||||||||||||||||||||||
| JsonArray msgsArray = new JsonArray(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for (com.drmq.protocol.DRMQProtocol.StoredMessage msg : messages) { | ||||||||||||||||||||||
| JsonObject obj = new JsonObject(); | ||||||||||||||||||||||
| obj.addProperty("offset", msg.getOffset()); | ||||||||||||||||||||||
| obj.addProperty("timestamp", msg.getTimestamp()); | ||||||||||||||||||||||
| obj.addProperty("storedAt", msg.getStoredAt()); | ||||||||||||||||||||||
| if (msg.hasKey()) { | ||||||||||||||||||||||
| obj.addProperty("key", msg.getKey()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| obj.addProperty("payload", msg.getPayload().toStringUtf8()); | ||||||||||||||||||||||
| msgsArray.add(obj); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| sendJsonResponse(exchange, 200, gson.toJson(msgsArray)); | ||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||
| logger.error("Error handling messages request", e); | ||||||||||||||||||||||
| sendJsonResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+186
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
🐛 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void addCorsHeaders(HttpExchange exchange) { | ||||||||||||||||||||||
| exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); | ||||||||||||||||||||||
| exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); | ||||||||||||||||||||||
| exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type, Authorization"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void sendJsonResponse(HttpExchange exchange, int statusCode, String response) throws IOException { | ||||||||||||||||||||||
| byte[] bytes = response.getBytes("UTF-8"); | ||||||||||||||||||||||
| exchange.getResponseHeaders().add("Content-Type", "application/json"); | ||||||||||||||||||||||
| exchange.sendResponseHeaders(statusCode, bytes.length); | ||||||||||||||||||||||
| try (OutputStream os = exchange.getResponseBody()) { | ||||||||||||||||||||||
| os.write(bytes); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,6 +48,9 @@ public class MessageStore implements Closeable { | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Topic -> Total number of messages | ||||||||||||||||||||||||||||||||||||||||||
| private final ConcurrentHashMap<String, AtomicLong> topicMessageCounts = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Topic -> Highest offset appended | ||||||||||||||||||||||||||||||||||||||||||
| private final ConcurrentHashMap<String, AtomicLong> topicHeadOffsets = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // In-memory cache for recent messages (Topic -> BoundedMessageCache) | ||||||||||||||||||||||||||||||||||||||||||
| private final ConcurrentHashMap<String, BoundedMessageCache> messageCache = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,6 +127,11 @@ private void recoverInternal() throws IOException { | |||||||||||||||||||||||||||||||||||||||||
| addToCache(topic, message); | ||||||||||||||||||||||||||||||||||||||||||
| topicMessageCounts.computeIfAbsent(topic, k -> new AtomicLong()).incrementAndGet(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1)); | ||||||||||||||||||||||||||||||||||||||||||
| if (offset > head.get()) { | ||||||||||||||||||||||||||||||||||||||||||
| head.set(offset); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (offset > maxOffset) { | ||||||||||||||||||||||||||||||||||||||||||
| maxOffset = offset; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -156,6 +164,7 @@ public void reload() throws IOException { | |||||||||||||||||||||||||||||||||||||||||
| logger.info("Reloading MessageStore state from disk..."); | ||||||||||||||||||||||||||||||||||||||||||
| topicIndex.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| topicMessageCounts.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| topicHeadOffsets.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| messageCache.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| topicWriteLocks.clear(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -222,6 +231,11 @@ public long append(String topic, byte[] payload, String key, long clientTimestam | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| addToCache(topic, message); | ||||||||||||||||||||||||||||||||||||||||||
| topicMessageCounts.computeIfAbsent(topic, k -> new AtomicLong()).incrementAndGet(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1)); | ||||||||||||||||||||||||||||||||||||||||||
| if (offset > head.get()) { | ||||||||||||||||||||||||||||||||||||||||||
| head.set(offset); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+234
to
+238
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Non-atomic "set-if-greater" on
🔒 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Persisted and indexed message: topic={}, offset={}, position={}, segment={}", | ||||||||||||||||||||||||||||||||||||||||||
| topic, offset, position, segment.getFilePath().getFileName()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -292,11 +306,15 @@ public long appendBatch(String topic, List<ProduceBatchRequest.BatchEntry> entri | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| AtomicLong counter = topicMessageCounts.computeIfAbsent(topic, k -> new AtomicLong()); | ||||||||||||||||||||||||||||||||||||||||||
| AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1)); | ||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < messages.size(); i++) { | ||||||||||||||||||||||||||||||||||||||||||
| StoredMessage msg = messages.get(i); | ||||||||||||||||||||||||||||||||||||||||||
| indexMessage(topic, msg.getOffset(), positions.get(i)); | ||||||||||||||||||||||||||||||||||||||||||
| addToCache(topic, msg); | ||||||||||||||||||||||||||||||||||||||||||
| counter.incrementAndGet(); | ||||||||||||||||||||||||||||||||||||||||||
| if (msg.getOffset() > head.get()) { | ||||||||||||||||||||||||||||||||||||||||||
| head.set(msg.getOffset()); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+309
to
+317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Same non-atomic "set-if-greater" race as in Same TOCTOU hazard: 🔒 Proposed fix using atomic max update- AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1));
- for (int i = 0; i < messages.size(); i++) {
- StoredMessage msg = messages.get(i);
- indexMessage(topic, msg.getOffset(), positions.get(i));
- addToCache(topic, msg);
- counter.incrementAndGet();
- if (msg.getOffset() > head.get()) {
- head.set(msg.getOffset());
- }
- }
+ AtomicLong head = topicHeadOffsets.computeIfAbsent(topic, k -> new AtomicLong(-1));
+ for (int i = 0; i < messages.size(); i++) {
+ StoredMessage msg = messages.get(i);
+ indexMessage(topic, msg.getOffset(), positions.get(i));
+ addToCache(topic, msg);
+ counter.incrementAndGet();
+ }
+ head.accumulateAndGet(currentOffset - 1, Math::max);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Batch persisted: topic={}, baseOffset={}, count={}", topic, baseOffset, batchSize); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -523,6 +541,11 @@ public int getMessageCount(String topic) { | |||||||||||||||||||||||||||||||||||||||||
| return count == null ? 0 : count.intValue(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| public long getHeadOffset(String topic) { | ||||||||||||||||||||||||||||||||||||||||||
| AtomicLong head = topicHeadOffsets.get(topic); | ||||||||||||||||||||||||||||||||||||||||||
| return head == null ? -1 : head.get(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| public List<String> getTopics() { | ||||||||||||||||||||||||||||||||||||||||||
| return new ArrayList<>(topicMessageCounts.keySet()); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -542,6 +565,7 @@ public long getCachedMessageCount() { | |||||||||||||||||||||||||||||||||||||||||
| public void clear() { | ||||||||||||||||||||||||||||||||||||||||||
| topicIndex.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| topicMessageCounts.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| topicHeadOffsets.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| messageCache.clear(); | ||||||||||||||||||||||||||||||||||||||||||
| globalOffset.set(0); | ||||||||||||||||||||||||||||||||||||||||||
| logger.info("Message store memory state cleared"); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -682,9 +706,16 @@ public void removeRange(long fromOffset, long toOffset) { | |||||||||||||||||||||||||||||||||||||||||
| public List<StoredMessage> getMessagesFrom(long fromOffset, int maxCount) { | ||||||||||||||||||||||||||||||||||||||||||
| lock.readLock().lock(); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| if (!cache.containsKey(fromOffset)) { | ||||||||||||||||||||||||||||||||||||||||||
| return Collections.emptyList(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| List<StoredMessage> result = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||
| boolean found = false; | ||||||||||||||||||||||||||||||||||||||||||
| for (Map.Entry<Long, StoredMessage> entry : cache.entrySet()) { | ||||||||||||||||||||||||||||||||||||||||||
| if (entry.getKey() >= fromOffset) { | ||||||||||||||||||||||||||||||||||||||||||
| if (entry.getKey() == fromOffset) { | ||||||||||||||||||||||||||||||||||||||||||
| found = true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if (found) { | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+709
to
+718
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Cache lookup now requires an exact offset match, defeating the fast path for normal consumption. Previously (per the AI summary) ⚡ Proposed fix restoring "collect from cached entries >= fromOffset" public List<StoredMessage> getMessagesFrom(long fromOffset, int maxCount) {
lock.readLock().lock();
try {
- if (!cache.containsKey(fromOffset)) {
- return Collections.emptyList();
- }
List<StoredMessage> result = new ArrayList<>();
- boolean found = false;
for (Map.Entry<Long, StoredMessage> entry : cache.entrySet()) {
- if (entry.getKey() == fromOffset) {
- found = true;
- }
- if (found) {
+ if (entry.getKey() >= fromOffset) {
result.add(entry.getValue());
if (result.size() >= maxCount) {
break;
}
}
}
return result;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| result.add(entry.getValue()); | ||||||||||||||||||||||||||||||||||||||||||
| if (result.size() >= maxCount) { | ||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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.javaRepository: 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/messagescall can block/api/topicsand/api/consumers. Use a small dedicated pool here to keep the dashboard responsive.🤖 Prompt for AI Agents