This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new ba28701069f7 CAMEL-25346: camel-console - a dev console capacity set 
from configuration breaks the trace, receive, event and sql-trace consoles 
(#27380)
ba28701069f7 is described below

commit ba28701069f75b71cdd885440af8e3cc284fe8ff
Author: Federico Mariani <[email protected]>
AuthorDate: Mon Oct 5 20:37:59 2026 +0200

    CAMEL-25346: camel-console - a dev console capacity set from configuration 
breaks the trace, receive, event and sql-trace consoles (#27380)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/impl/console/ConsoleHelper.java   |  69 +++++++++
 .../apache/camel/impl/console/EventConsole.java    |  54 +++----
 .../camel/impl/console/ReceiveDevConsole.java      |  32 ++--
 .../camel/impl/console/SqlTraceDevConsole.java     |  20 ++-
 .../apache/camel/impl/console/TraceDevConsole.java |  34 ++---
 .../camel/impl/console/ConsoleHelperTest.java      |  37 +++++
 .../camel/impl/console/DevConsoleCapacityTest.java | 170 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 +
 8 files changed, 355 insertions(+), 68 deletions(-)

diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ConsoleHelper.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ConsoleHelper.java
index bb7f1beaad10..0720e5ff00f7 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ConsoleHelper.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ConsoleHelper.java
@@ -19,8 +19,12 @@ package org.apache.camel.impl.console;
 import java.io.LineNumberReader;
 import java.io.Reader;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.Resource;
@@ -147,4 +151,69 @@ public final class ConsoleHelper {
         return location;
     }
 
+    /**
+     * Validates the capacity of a console buffer.
+     *
+     * @throws IllegalArgumentException if the capacity is not between the 
given minimum and 1000
+     */
+    static void checkCapacity(int capacity, int min) {
+        if (capacity > 1000 || capacity < min) {
+            throw new IllegalArgumentException("Capacity must be between " + 
min + " and 1000");
+        }
+    }
+
+    /**
+     * Adds the element to the bounded queue, removing the oldest elements to 
make room.
+     */
+    static <T> void offerLast(Queue<T> queue, T element) {
+        while (!queue.offer(element)) {
+            queue.poll();
+        }
+    }
+
+    /**
+     * Creates a bounded queue of the given capacity holding the newest 
elements of the given queue.
+     */
+    static <T> Queue<T> resize(Queue<T> queue, int capacity) {
+        Queue<T> answer = new LinkedBlockingQueue<>(Math.max(1, capacity));
+        for (T element : queue) {
+            offerLast(answer, element);
+        }
+        return answer;
+    }
+
+    /**
+     * Creates a ring buffer of the given capacity holding the newest entries 
of the given ring buffer, so that slot 0
+     * is the next one to write.
+     *
+     * @param ring the ring buffer
+     * @param next the next slot to write in the given ring buffer
+     */
+    static <T> T[] resize(T[] ring, int next, int capacity) {
+        int length = Math.max(1, capacity);
+        List<T> entries = new ArrayList<>(ring.length);
+        for (int i = 0; i < ring.length; i++) {
+            T entry = ring[(next + i) % ring.length];
+            if (entry != null) {
+                entries.add(entry);
+            }
+        }
+        T[] answer = Arrays.copyOf(ring, length);
+        Arrays.fill(answer, null);
+        // the newest entries, oldest first, at the end: slot 0 holds the 
oldest (or nothing) and is written next
+        int kept = Math.min(entries.size(), length);
+        for (int i = 0; i < kept; i++) {
+            answer[length - kept + i] = entries.get(entries.size() - kept + i);
+        }
+        return answer;
+    }
+
+    /**
+     * Returns the slot to write in a ring buffer of the given length, and 
moves the position to the next slot.
+     */
+    static int nextSlot(AtomicInteger pos, int length) {
+        // the position may come from a larger ring buffer that has just been 
resized
+        return pos.getAndUpdate(operand -> (operand + 1) % length) % length;
+    }
+
 }
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
index af8a56eec145..a41590aac968 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
@@ -53,11 +53,11 @@ public class EventConsole extends AbstractDevConsole {
               description = "Maximum capacity of last number of events to 
capture (capacity must be between 25 and 1000)")
     private int capacity = 25;
 
-    private CamelEvent[] events;
+    private volatile CamelEvent[] events;
     private final AtomicInteger posEvents = new AtomicInteger();
-    private CamelEvent.RouteEvent[] routeEvents;
+    private volatile CamelEvent.RouteEvent[] routeEvents;
     private final AtomicInteger posRoutes = new AtomicInteger();
-    private CamelEvent.ExchangeEvent[] exchangeEvents;
+    private volatile CamelEvent.ExchangeEvent[] exchangeEvents;
     private final AtomicInteger posExchanges = new AtomicInteger();
     private final ConsoleEventNotifier listener = new ConsoleEventNotifier();
 
@@ -70,14 +70,20 @@ public class EventConsole extends AbstractDevConsole {
     }
 
     public void setCapacity(int capacity) {
+        if (events != null) {
+            // Camel Main configures the console after the registry has 
started it: resize its buffers (an entry
+            // added meanwhile may be lost, which only matters while 
configuring)
+            ConsoleHelper.checkCapacity(capacity, 25);
+            this.events = ConsoleHelper.resize(events, posEvents.getAndSet(0), 
capacity);
+            this.routeEvents = ConsoleHelper.resize(routeEvents, 
posRoutes.getAndSet(0), capacity);
+            this.exchangeEvents = ConsoleHelper.resize(exchangeEvents, 
posExchanges.getAndSet(0), capacity);
+        }
         this.capacity = capacity;
     }
 
     @Override
     protected void doInit() throws Exception {
-        if (capacity > 1000 || capacity < 25) {
-            throw new IllegalArgumentException("Capacity must be between 25 
and 1000");
-        }
+        ConsoleHelper.checkCapacity(capacity, 25);
         this.events = new CamelEvent[capacity];
         this.routeEvents = new CamelEvent.RouteEvent[capacity];
         this.exchangeEvents = new CamelEvent.ExchangeEvent[capacity];
@@ -96,37 +102,32 @@ public class EventConsole extends AbstractDevConsole {
     protected String doCallText(Map<String, Object> options) {
         StringBuilder sb = new StringBuilder();
 
-        int pos = posEvents.get();
-        sb.append(appendTextEvents(events, "Camel", pos, capacity));
+        sb.append(appendTextEvents(events, "Camel", posEvents.get()));
         sb.append("\n");
-        pos = posRoutes.get();
-        sb.append(appendTextEvents(routeEvents, "Route", pos, capacity));
+        sb.append(appendTextEvents(routeEvents, "Route", posRoutes.get()));
         sb.append("\n");
-        pos = posExchanges.get();
-        sb.append(appendTextEvents(exchangeEvents, "Exchange", pos, capacity));
+        sb.append(appendTextEvents(exchangeEvents, "Exchange", 
posExchanges.get()));
         sb.append("\n");
 
         return sb.toString();
     }
 
     protected Map<String, Object> doCallJson(Map<String, Object> options) {
-        int pos = posEvents.get();
-        List<EventEntry> arr = appendJSonEvents(events, pos, capacity);
+        List<EventEntry> arr = appendJSonEvents(events, posEvents.get());
         List<EventEntry> eventsOut = !arr.isEmpty() ? arr : null;
 
-        pos = posRoutes.get();
-        arr = appendJSonEvents(routeEvents, pos, capacity);
+        arr = appendJSonEvents(routeEvents, posRoutes.get());
         List<EventEntry> routeEventsOut = !arr.isEmpty() ? arr : null;
 
-        pos = posExchanges.get();
-        arr = appendJSonEvents(exchangeEvents, pos, capacity);
+        arr = appendJSonEvents(exchangeEvents, posExchanges.get());
         List<EventEntry> exchangeEventsOut = !arr.isEmpty() ? arr : null;
 
         Response response = new Response(eventsOut, routeEventsOut, 
exchangeEventsOut);
         return JsonRecordSupport.toJsonObject(response);
     }
 
-    private static String appendTextEvents(CamelEvent[] events, String kind, 
int cursor, int capacity) {
+    private static String appendTextEvents(CamelEvent[] events, String kind, 
int cursor) {
+        int capacity = events.length;
         StringBuilder sb = new StringBuilder();
         int pos = 0;
         int added = 0;
@@ -153,7 +154,8 @@ public class EventConsole extends AbstractDevConsole {
         return sb.toString();
     }
 
-    private static List<EventEntry> appendJSonEvents(CamelEvent[] events, int 
cursor, int capacity) {
+    private static List<EventEntry> appendJSonEvents(CamelEvent[] events, int 
cursor) {
+        int capacity = events.length;
         List<EventEntry> arr = new ArrayList<>();
         int pos = 0;
         // cursor is at last event, so move to back
@@ -191,15 +193,15 @@ public class EventConsole extends AbstractDevConsole {
         public void notify(CamelEvent event) throws Exception {
             if (event instanceof CamelEvent.ExchangeEvent) {
                 CamelEvent.ExchangeEvent ce = (CamelEvent.ExchangeEvent) event;
-                int pos = posExchanges.getAndUpdate(operand -> ++operand % 
capacity);
-                exchangeEvents[pos] = ce;
+                CamelEvent.ExchangeEvent[] ring = exchangeEvents;
+                ring[ConsoleHelper.nextSlot(posExchanges, ring.length)] = ce;
             } else if (event instanceof CamelEvent.RouteEvent) {
                 CamelEvent.RouteEvent re = (CamelEvent.RouteEvent) event;
-                int pos = posRoutes.getAndUpdate(operand -> ++operand % 
capacity);
-                routeEvents[pos] = re;
+                CamelEvent.RouteEvent[] ring = routeEvents;
+                ring[ConsoleHelper.nextSlot(posRoutes, ring.length)] = re;
             } else {
-                int pos = posEvents.getAndUpdate(operand -> ++operand % 
capacity);
-                events[pos] = event;
+                CamelEvent[] ring = events;
+                ring[ConsoleHelper.nextSlot(posEvents, ring.length)] = event;
             }
         }
 
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ReceiveDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ReceiveDevConsole.java
index 70654e151b76..0c38c24a20ed 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ReceiveDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ReceiveDevConsole.java
@@ -96,7 +96,7 @@ public class ReceiveDevConsole extends AbstractDevConsole {
     private final List<Consumer> consumers = new ArrayList<>();
     private final AtomicBoolean enabled = new AtomicBoolean();
     private final AtomicLong uuid = new AtomicLong();
-    private Queue<JsonObject> queue;
+    private volatile Queue<JsonObject> queue;
     private long firstTimestamp;
     private long lastTimestamp;
 
@@ -109,6 +109,13 @@ public class ReceiveDevConsole extends AbstractDevConsole {
     }
 
     public void setCapacity(int capacity) {
+        Queue<JsonObject> q = queue;
+        if (q != null) {
+            // Camel Main configures the console after the registry has 
started it: resize its buffers (an entry
+            // added meanwhile may be lost, which only matters while 
configuring)
+            ConsoleHelper.checkCapacity(capacity, 50);
+            this.queue = ConsoleHelper.resize(q, capacity);
+        }
         this.capacity = capacity;
     }
 
@@ -130,9 +137,7 @@ public class ReceiveDevConsole extends AbstractDevConsole {
 
     @Override
     protected void doInit() throws Exception {
-        if (capacity > 1000 || capacity < 50) {
-            throw new IllegalArgumentException("Capacity must be between 50 
and 1000");
-        }
+        ConsoleHelper.checkCapacity(capacity, 50);
         this.queue = new LinkedBlockingQueue<>(capacity);
     }
 
@@ -153,10 +158,11 @@ public class ReceiveDevConsole extends AbstractDevConsole 
{
 
         String dump = optionString(options, DUMP);
         if ("true".equals(dump)) {
+            Queue<JsonObject> q = queue;
             JsonArray arr = new JsonArray();
-            arr.addAll(queue);
+            arr.addAll(q);
             if (removeOnDump) {
-                queue.clear();
+                q.clear();
             }
             JsonObject jo = (JsonObject) arr.get(0);
             firstTimestamp = jo.getLongOrDefault("timestamp", 0);
@@ -205,9 +211,10 @@ public class ReceiveDevConsole extends AbstractDevConsole {
     protected Map<String, Object> doCallJson(Map<String, Object> options) {
         String dump = optionString(options, DUMP);
         if ("true".equals(dump)) {
-            List<Map<String, Object>> messages = new ArrayList<>(queue);
+            Queue<JsonObject> q = queue;
+            List<Map<String, Object>> messages = new ArrayList<>(q);
             if (removeOnDump) {
-                queue.clear();
+                q.clear();
             }
             JsonObject first = (JsonObject) messages.get(0);
             firstTimestamp = first.getLongOrDefault("timestamp", 0);
@@ -282,14 +289,7 @@ public class ReceiveDevConsole extends AbstractDevConsole {
         lastTimestamp = exchange.getMessage().getMessageTimestamp();
         json.put("timestamp", lastTimestamp);
 
-        // ensure there is space on the queue by polling until at least single 
slot is free
-        int drain = queue.size() - capacity + 1;
-        if (drain > 0) {
-            for (int i = 0; i < drain; i++) {
-                queue.poll();
-            }
-        }
-        queue.add(json);
+        ConsoleHelper.offerLast(queue, json);
     }
 
     protected static Endpoint findMatchingEndpoint(CamelContext camelContext, 
String endpoint) {
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
index cc6b9afcb86e..076cd3728a80 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
@@ -49,7 +49,7 @@ public class SqlTraceDevConsole extends AbstractDevConsole {
               description = "Maximum capacity of traced SQL statements 
(capacity must be between 25 and 1000)")
     private int capacity = 200;
 
-    private StatementEntry[] events;
+    private volatile StatementEntry[] events;
     private final AtomicInteger pos = new AtomicInteger();
     private final ConsoleEventNotifier listener = new ConsoleEventNotifier();
 
@@ -94,14 +94,18 @@ public class SqlTraceDevConsole extends AbstractDevConsole {
     }
 
     public void setCapacity(int capacity) {
+        if (events != null) {
+            // Camel Main configures the console after the registry has 
started it: resize its buffers (an entry
+            // added meanwhile may be lost, which only matters while 
configuring)
+            ConsoleHelper.checkCapacity(capacity, 25);
+            this.events = ConsoleHelper.resize(events, pos.getAndSet(0), 
capacity);
+        }
         this.capacity = capacity;
     }
 
     @Override
     protected void doInit() throws Exception {
-        if (capacity > 1000 || capacity < 25) {
-            throw new IllegalArgumentException("Capacity must be between 25 
and 1000");
-        }
+        ConsoleHelper.checkCapacity(capacity, 25);
         this.events = new StatementEntry[capacity];
     }
 
@@ -191,11 +195,13 @@ public class SqlTraceDevConsole extends 
AbstractDevConsole {
 
     private List<StatementEntry> collectEvents() {
         List<StatementEntry> list = new ArrayList<>();
+        StatementEntry[] ring = events;
+        int capacity = ring.length;
         int cursor = pos.get();
         // cursor points to the NEXT write slot, so walk backward from cursor-1
         for (int i = 0; i < capacity; i++) {
             cursor = (cursor - 1 + capacity) % capacity;
-            StatementEntry event = events[cursor];
+            StatementEntry event = ring[cursor];
             if (event != null) {
                 list.add(event);
             }
@@ -345,8 +351,8 @@ public class SqlTraceDevConsole extends AbstractDevConsole {
                             event.getTimestamp(), exchange.getExchangeId(), 
exchange.getFromRouteId(), nodeId, location,
                             uri, finalQuery, category, ese.getTimeTaken(), 
exchange.isFailed(), rowCount, updateCount);
 
-                    int p = pos.getAndUpdate(operand -> ++operand % capacity);
-                    events[p] = entry;
+                    StatementEntry[] ring = events;
+                    ring[ConsoleHelper.nextSlot(pos, ring.length)] = entry;
                 }
             }
         }
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java
index a8fdf375fe34..bd36449926f9 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java
@@ -67,7 +67,7 @@ public class TraceDevConsole extends AbstractDevConsole {
               javaType = "java.lang.String", enums = "true,false")
     public static final String DUMP = "dump";
 
-    private Queue<BacklogTracerEventMessage> queue;
+    private volatile Queue<BacklogTracerEventMessage> queue;
 
     public TraceDevConsole() {
         super("camel", "trace", "Camel Tracing", "Trace routed messages");
@@ -78,14 +78,19 @@ public class TraceDevConsole extends AbstractDevConsole {
     }
 
     public void setCapacity(int capacity) {
+        Queue<BacklogTracerEventMessage> q = queue;
+        if (q != null) {
+            // Camel Main configures the console after the registry has 
started it: resize its buffers (an entry
+            // added meanwhile may be lost, which only matters while 
configuring)
+            ConsoleHelper.checkCapacity(capacity, 50);
+            this.queue = ConsoleHelper.resize(q, capacity);
+        }
         this.capacity = capacity;
     }
 
     @Override
     protected void doInit() throws Exception {
-        if (capacity > 1000 || capacity < 50) {
-            throw new IllegalArgumentException("Capacity must be between 50 
and 1000");
-        }
+        ConsoleHelper.checkCapacity(capacity, 50);
         this.queue = new LinkedBlockingQueue<>(capacity);
     }
 
@@ -97,10 +102,11 @@ public class TraceDevConsole extends AbstractDevConsole {
         BacklogTracer tracer = 
getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class);
         if (tracer != null) {
             if ("true".equalsIgnoreCase(dump)) {
+                Queue<BacklogTracerEventMessage> q = queue;
                 for (BacklogTracerEventMessage t : 
tracer.dumpAllTracedMessages()) {
-                    addMessage(t);
+                    ConsoleHelper.offerLast(q, t);
                 }
-                for (BacklogTracerEventMessage t : queue) {
+                for (BacklogTracerEventMessage t : q) {
                     String json = t.toJSon(0);
                     sb.append(json).append("\n");
                 }
@@ -136,17 +142,6 @@ public class TraceDevConsole extends AbstractDevConsole {
         return sb.toString();
     }
 
-    private void addMessage(BacklogTracerEventMessage message) {
-        // ensure there is space on the queue by polling until at least single 
slot is free
-        int drain = queue.size() - capacity + 1;
-        if (drain > 0) {
-            for (int i = 0; i < drain; i++) {
-                queue.poll();
-            }
-        }
-        queue.add(message);
-    }
-
     protected Map<String, Object> doCallJson(Map<String, Object> options) {
         String enabled = optionString(options, ENABLED);
         String dump = optionString(options, DUMP);
@@ -157,11 +152,12 @@ public class TraceDevConsole extends AbstractDevConsole {
         BacklogTracer tracer = 
getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class);
         if (tracer != null) {
             if ("true".equalsIgnoreCase(dump)) {
+                Queue<BacklogTracerEventMessage> q = queue;
                 for (BacklogTracerEventMessage t : 
tracer.dumpAllTracedMessages()) {
-                    addMessage(t);
+                    ConsoleHelper.offerLast(q, t);
                 }
                 List<Map<String, Object>> traces = new ArrayList<>();
-                for (BacklogTracerEventMessage t : queue) {
+                for (BacklogTracerEventMessage t : q) {
                     traces.add((JsonObject) t.asJSon());
                 }
                 response = new Response(
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/ConsoleHelperTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/ConsoleHelperTest.java
index 84893bdad92d..da0e676def8c 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/ConsoleHelperTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/ConsoleHelperTest.java
@@ -18,6 +18,10 @@ package org.apache.camel.impl.console;
 
 import java.io.Reader;
 import java.io.StringReader;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -117,4 +121,37 @@ public class ConsoleHelperTest {
         JsonArray result = ConsoleHelper.loadSourceAsJson(nullReader, 1);
         Assertions.assertNull(result);
     }
+
+    @Test
+    public void testResizeRingKeepsNewestEntries() {
+        // 5 slots, written 7 times: next slot is 2, oldest to newest is 3..7
+        String[] ring = { "6", "7", "3", "4", "5" };
+
+        String[] larger = ConsoleHelper.resize(ring, 2, 8);
+        Assertions.assertArrayEquals(new String[] { null, null, null, "3", 
"4", "5", "6", "7" }, larger);
+
+        String[] smaller = ConsoleHelper.resize(ring, 2, 3);
+        Assertions.assertArrayEquals(new String[] { "5", "6", "7" }, smaller);
+    }
+
+    @Test
+    public void testNextSlotAfterResize() {
+        AtomicInteger pos = new AtomicInteger(7);
+        // a position from a larger ring buffer stays within the smaller one
+        Assertions.assertEquals(1, ConsoleHelper.nextSlot(pos, 3));
+        Assertions.assertEquals(2, ConsoleHelper.nextSlot(pos, 3));
+        Assertions.assertEquals(0, ConsoleHelper.nextSlot(pos, 3));
+    }
+
+    @Test
+    public void testResizeQueueKeepsNewestElements() {
+        Queue<Integer> queue = new LinkedBlockingQueue<>(5);
+        for (int i = 1; i <= 7; i++) {
+            ConsoleHelper.offerLast(queue, i);
+        }
+        Assertions.assertEquals(List.of(3, 4, 5, 6, 7), List.copyOf(queue));
+
+        Assertions.assertEquals(List.of(5, 6, 7), 
List.copyOf(ConsoleHelper.resize(queue, 3)));
+        Assertions.assertEquals(List.of(3, 4, 5, 6, 7), 
List.copyOf(ConsoleHelper.resize(queue, 10)));
+    }
 }
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/DevConsoleCapacityTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/DevConsoleCapacityTest.java
new file mode 100644
index 000000000000..96b17fe094f8
--- /dev/null
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/DevConsoleCapacityTest.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.impl.console;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.IntConsumer;
+import java.util.function.IntSupplier;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.console.DevConsoleRegistry;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Camel Main sets {@code camel.devConsole.<id>.capacity} on a console the 
registry has already started, so the console
+ * must resize the buffers it allocated with the default capacity.
+ */
+public class DevConsoleCapacityTest extends ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.setBacklogTracing(true);
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:start").routeId("myRoute").log("${body}").to("mock:result");
+            }
+        };
+    }
+
+    private <T extends DevConsole> T resolve(String id) {
+        // the registry starts the console, as when Camel Main configures it
+        return (T) 
context.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class).resolveById(id);
+    }
+
+    private void sendMessages(String uri, int count) {
+        for (int i = 0; i < count; i++) {
+            template.sendBody(uri, "Hello " + i);
+        }
+    }
+
+    @Test
+    public void testTraceCapacitySetAfterStart() {
+        TraceDevConsole console = resolve("trace");
+        console.setCapacity(1000);
+
+        // each dump takes up to 100 events from the tracer (its backlog 
size): the second one used to fail with
+        // "Queue full" as the queue still had the default 100 slots
+        int traces = 0;
+        for (int round = 0; round < 3; round++) {
+            sendMessages("direct:start", 50);
+            JsonObject out = (JsonObject) 
console.call(DevConsole.MediaType.JSON, Map.of("dump", "true"));
+            traces = out.getCollection("traces").size();
+        }
+        assertEquals(300, traces);
+    }
+
+    @Test
+    public void testTraceCapacityLoweredAfterStart() {
+        TraceDevConsole console = resolve("trace");
+        sendMessages("direct:start", 50);
+        console.call(DevConsole.MediaType.JSON, Map.of("dump", "true"));
+
+        console.setCapacity(50);
+        sendMessages("direct:start", 50);
+        JsonObject out = (JsonObject) console.call(DevConsole.MediaType.JSON, 
Map.of("dump", "true"));
+        assertEquals(50, out.getCollection("traces").size());
+    }
+
+    @Test
+    public void testReceiveCapacitySetAfterStart() {
+        ReceiveDevConsole console = resolve("receive");
+        console.setCapacity(1000);
+        console.call(DevConsole.MediaType.JSON, Map.of("enabled", "true", 
"endpoint", "direct:incoming"));
+
+        sendMessages("direct:incoming", 150);
+
+        JsonObject out = (JsonObject) console.call(DevConsole.MediaType.JSON, 
Map.of("dump", "true"));
+        List<JsonObject> messages = out.getCollection("messages");
+        assertEquals(150, messages.size());
+        assertEquals(150, messages.get(149).getLong("uid"));
+    }
+
+    @Test
+    public void testEventCapacitySetAfterStart() {
+        EventConsole console = resolve("event");
+        console.setCapacity(1000);
+
+        // used to fail with ArrayIndexOutOfBoundsException once past the 
default 25 slots
+        sendMessages("direct:start", 50);
+
+        JsonObject out = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        assertTrue(out.getCollection("exchangeEvents").size() > 25);
+        assertTrue(((String) 
console.call(DevConsole.MediaType.TEXT)).contains("Exchange Events"));
+    }
+
+    @Test
+    public void testEventCapacityLoweredAfterStart() {
+        EventConsole console = resolve("event");
+        console.setCapacity(100);
+        sendMessages("direct:start", 30);
+
+        console.setCapacity(25);
+        sendMessages("direct:start", 30);
+
+        JsonObject out = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        assertEquals(25, out.getCollection("exchangeEvents").size());
+    }
+
+    @Test
+    public void testSqlTraceCapacitySetAfterStart() {
+        SqlTraceDevConsole console = resolve("sql-trace");
+        console.setCapacity(1000);
+
+        // used to fail with ArrayIndexOutOfBoundsException as the dump walked 
1000 slots of the default 200
+        JsonObject out = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        assertTrue(out.isEmpty());
+        assertEquals("", console.call(DevConsole.MediaType.TEXT));
+    }
+
+    @Test
+    public void testInvalidCapacitySetAfterStart() {
+        // the range checked when the console starts is also checked when the 
capacity is set afterwards
+        TraceDevConsole trace = resolve("trace");
+        assertInvalidCapacity(trace::getCapacity, trace::setCapacity, 50);
+        ReceiveDevConsole receive = resolve("receive");
+        assertInvalidCapacity(receive::getCapacity, receive::setCapacity, 50);
+        EventConsole event = resolve("event");
+        assertInvalidCapacity(event::getCapacity, event::setCapacity, 25);
+        SqlTraceDevConsole sqlTrace = resolve("sql-trace");
+        assertInvalidCapacity(sqlTrace::getCapacity, sqlTrace::setCapacity, 
25);
+    }
+
+    private static void assertInvalidCapacity(IntSupplier getter, IntConsumer 
setter, int min) {
+        int before = getter.getAsInt();
+        for (int capacity : new int[] { min - 1, 1001 }) {
+            IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, () -> setter.accept(capacity));
+            assertEquals("Capacity must be between " + min + " and 1000", 
e.getMessage());
+            assertEquals(before, getter.getAsInt());
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 8afd5488a35a..0eaa9c32cd09 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -645,6 +645,13 @@ The `inflight` and `blocked` developer consoles now carry 
a `nodeSource` field p
 exchange sits at is in the source (such as `orders.camel.yaml:18`). It is 
`null` when message history or source
 location is disabled. Existing fields are unchanged.
 
+The `capacity` option of the `trace`, `receive`, `event` and `sql-trace` 
developer consoles, set from configuration
+(such as `camel.devConsole.trace.capacity`), is now applied. Prior to Camel 
4.23 Camel Main set it after the console
+had started, so a capacity larger than the default broke the console (`Queue 
full` or
+`ArrayIndexOutOfBoundsException`), and the documented range was not checked. A 
capacity outside that range (50-1000
+for `trace` and `receive`, 25-1000 for `event` and `sql-trace`) now fails at 
startup with an `IllegalArgumentException`,
+also when it is lower than the minimum, which was accepted before.
+
 === Error handler - redelivery options of onException
 
 An `onException` that sets some of the redelivery options (such as 
`maximumRedeliveries`) now inherits the options it

Reply via email to