2026.09.24 — 2026-09-14
Breaking
Match captured rows by address, isolate instances, and bound taps in bytes (69430080)
Full capture now reports, bounds and cleans up what it actually captured.
- Footprint, loss, size-bound enforcement, deletion and retention match captured rows by the address they were routed to. Previously they matched queue names, so a multicast address, or any queue named differently from its address, showed a footprint of zero, counted every message as lost, ignored its size bound and retention, and left payload behind on delete.
- Loss counts each routed message once per address rather than once per bound queue, clears DEGRADED after a clean pass, states the window the loss happened in, and says "loss cannot be estimated" for a filtered capture instead of reporting a number.
- A tap whose divert was removed outside Studio is reinstalled even while its drain is still running. Drains on nodes that stopped serving are stopped.
- Changing a live subscription's filter or bounds re-creates its taps so the change reaches the broker; the audit row records old and new values. Deleting or disabling a capture stops recording before rows are removed.
- A node with no Core URL is refused before any broker object is created. A refusal that will not change on its own (an invalid filter, a missing role) is recorded once as FAILED instead of being retried and audited every pass.
- Capture settings are scoped to the Studio instance that owns them, so one instance removing its taps no longer strips another instance's capture queues of their DROP policy and access restriction.
- Capture queues are bounded in bytes (artemis-studio.capture.max-ring-bytes, default 64 MiB) as well as in messages, and are configured for unlimited redelivery.
- Studio's own capture addresses are never captured; a pattern that can only match them is refused.
- Out-of-range subscription bounds are refused with the field and range instead of being silently clamped.
Batch by-id operations, share list reads, and bound backlog walks (cb357d5e)
Message move, retry, delete and expire by ids are now sent to the broker in batches of fifty, one request and one rate-limit permit each, instead of one request per id. When an operation stops part-way the partial result lists every id that was not done — refused by the broker, or never sent — as notDone.
The connections, sessions, consumers, addresses and producers views now share one broker read per node and kind for two seconds, so several open tabs and the stream-driven refreshes cost one full-list call per node rather than one each. A failed read is not reused.
A sampled index subscription still indexes the messages already on its queues when it starts, but reads at most five pages per queue per poll and walks a deep backlog over several polls. The subscription reports backlogInProgress until that walk is done.
Core consumers and browsers now prefetch at most 64 KiB instead of the 1 MiB default, and capture drains use their own session pool, so many capture taps on a node can no longer make an operator's browse or send wait.
The site configuration guide gains a Monitoring section with the new broker request metrics and the recommended alerts.
Rate-limit every broker request and bound Core browses (3bc58747)
Studio now charges the per-node management-call ceiling for every request it sends to a broker, not once per operation. Previously a message operation by ids took one permit and then sent a request per id, a capture install sent eight to ten requests on one permit, and cluster registration, rediscovery and capability probes took no permit at all. The ceiling is now taken inside the broker client and the Core message transport, so no caller can skip it; a batch request pays one permit per fifty operations.
A Core message browse now reads only the requested page. It used to stream the entire queue to Studio just to count it, and the request-reply sampler did that for every traced address every few seconds. The total comes from the broker's own count; when that count cannot be read the API returns count: null with countUnavailable explaining why, instead of a guessed number. The request-reply sampler now reads at most its sample size.
New metrics: studio.broker.requests{node}, studio.broker.permit.wait{node} and studio.broker.permit.timeouts{node}. The brokers health contributor reports the last rate-limit wait per node as before.
Commit the audit row before the broker call (c2419345)
Every mutating action now commits its audit row before Studio calls a broker, and records the outcome in a separate commit afterwards. Previously the row lived in the same transaction as the whole command, so a crash part-way through a cluster-wide command left brokers changed and no audit record at all, and the command held a database connection for every node it contacted.
A caller that rolls back before recording an outcome now leaves a failed row with that reason, never a row that looks in flight. Cluster-wide commands and message operations no longer hold a database transaction across broker calls.
Message move, retry, delete and expire by ids now report a partial outcome when they stop part-way: how many messages were acted on, which ids were not done, and why. The HTTP response is the new PartialView (partial: true) and the audit row records the failure with the count affected before it.
Added
Arm capture from its dry run, guard the divert form, and state partial and unavailable results (73a8ea60)
- Capture everything is armed from its dry run: the screen shows the addresses, live nodes, ring bound in messages and bytes, the broker objects and the broker.xml, then asks for the pattern to be typed. A refusal is shown instead of the arming control. Capture bounds sit behind a disclosure and are checked on blur against the limits the server enforces.
- Sampled subscriptions say they are just sampling: a message consumed between two polls is never recorded.
- Request-reply tracing names traced addresses that no capture subscription covers and points to Settings → Message index; the hint disappears once they are captured.
- The divert form validates on blur, puts server field errors on their fields and focuses the first one, freezes what was previewed until Edit, withholds Create when every node refuses, and cannot be closed while a request is in flight. An exclusive divert offers the acknowledgement capture shadowing needs.
- A divert Studio created shows its ownership as a button that opens its broker.xml.
- Per-node outcomes are announced through a live region.
- A by-id message operation that stopped part-way says how many were done and lists the ids left undone. A message browse whose total could not be read says so rather than showing zero.
Dry-run a capture subscription and recover drains that lost their connection (7d84bca0)
POST /api/v1/clusters/{id}/sql/index?dryRun=true now previews a capture subscription without saving it or touching a broker: the addresses the pattern resolves to, the live nodes the tap would be installed on, the capture queue's bounds in messages and bytes, the broker objects it creates, the equivalent broker.xml, and why it would be refused as things stand — a pattern that matches nothing, or a broker role that is not configured.
A capture drain whose broker connection fails, for example because the broker restarted, is now dropped at once, so the next reconcile pass re-creates the capture queue and records the gap. Previously the drain stayed registered, reading nothing, while the subscription reported the node as capturing.
Changed
Batch by-id operations, share list reads, and bound backlog walks (cb357d5e)
Message move, retry, delete and expire by ids are now sent to the broker in batches of fifty, one request and one rate-limit permit each, instead of one request per id. When an operation stops part-way the partial result lists every id that was not done — refused by the broker, or never sent — as notDone.
The connections, sessions, consumers, addresses and producers views now share one broker read per node and kind for two seconds, so several open tabs and the stream-driven refreshes cost one full-list call per node rather than one each. A failed read is not reused.
A sampled index subscription still indexes the messages already on its queues when it starts, but reads at most five pages per queue per poll and walks a deep backlog over several polls. The subscription reports backlogInProgress until that walk is done.
Core consumers and browsers now prefetch at most 64 KiB instead of the 1 MiB default, and capture drains use their own session pool, so many capture taps on a node can no longer make an operator's browse or send wait.
The site configuration guide gains a Monitoring section with the new broker request metrics and the recommended alerts.
Rate-limit every broker request and bound Core browses (3bc58747)
Studio now charges the per-node management-call ceiling for every request it sends to a broker, not once per operation. Previously a message operation by ids took one permit and then sent a request per id, a capture install sent eight to ten requests on one permit, and cluster registration, rediscovery and capability probes took no permit at all. The ceiling is now taken inside the broker client and the Core message transport, so no caller can skip it; a batch request pays one permit per fifty operations.
A Core message browse now reads only the requested page. It used to stream the entire queue to Studio just to count it, and the request-reply sampler did that for every traced address every few seconds. The total comes from the broker's own count; when that count cannot be read the API returns count: null with countUnavailable explaining why, instead of a guessed number. The request-reply sampler now reads at most its sample size.
New metrics: studio.broker.requests{node}, studio.broker.permit.wait{node} and studio.broker.permit.timeouts{node}. The brokers health contributor reports the last rate-limit wait per node as before.
Fixed
Measure capture loss at one instant so a healthy capture reports none (db6219ad)
Capture could still report messages as missed when every one was stored: about 100 over 60,000 on the dev stack, and thousands across a database outage. Stored rows were counted when the loss pass ran, while routed and queued counts came from the last queue snapshot, so anything stored in between counted against routing the snapshot had not seen, and the difference moved with how stale the snapshot was.
Stored rows are now counted up to the snapshot's own time. What remains unmeasurable is a drain's in-flight batch, so a rise is reported only once it exceeds that window twice over, measured from the lowest level since the last report: a swing that falls back is never reported, and real loss keeps rising until it is.
Slow capture to its rate limit instead of dropping, and keep idle Core connections alive (718c0140)
Capture could lose data after its database recovered. While Postgres was down the capture queue held the backlog as designed, but once it came back the backlog was redelivered faster than the subscription's ingest rate limit, and every message over the limit was acknowledged without being stored. On the dev stack a 2-minute Postgres outage lost about 17,800 of 30,000 messages this way.
The rate limit now slows capture instead: a drain waits for the next permit and the message stays in the bounded capture queue, so a limit can only lead to loss through the queue's own, counted, bound. Rate-limited is no longer reported as a cause of loss.
Studio's Core connections also dropped whenever capture was idle or waiting: the connection TTL (20s) was shorter than the client's ping period (30s by default), so the broker closed them and every drain was re-created on the next pass. Core connections now ping every read timeout (10s by default) with a TTL of three pings.
Stop reporting messages still in the capture ring as lost (e70857c2)
A capture that stored every message could still show as degraded, with "about N messages were not captured". Each pass compared what the broker routed against what was stored, so messages routed but still waiting in the capture queue were counted as lost, and when the drain caught up on the next pass the difference was never taken back. On the dev stack at about 200 messages a second this reported roughly 2,400 missed messages over 30 minutes with none actually lost.
Loss is now what was routed and is neither stored nor still in the capture queue. A broker restart or a retention delete, which makes a counter go backwards, resets the baseline for that pass instead of being reported as loss.
Verify divert deployment and refuse diverts that would break routing (e2a43c19)
Creating a divert now reports what the broker actually deployed. Artemis answers success and only logs a warning when it declines a divert, so each node's divert list is read back: an identical divert already there is reported as already present, a different one under the same name fails naming the differing fields, and a divert the broker accepted but did not deploy fails instead of reading as applied.
Before anything is created, in the preview and again for real, each node is checked. A divert is refused when:
- its forwarding address does not exist and is not auto-created (producers to the source would fail), with the broker.xml that creates it;
- it would close a cycle of diverts, naming the cycle;
- it is exclusive on an address being captured, unless the request sets acknowledgeCaptureShadowing (capture would observe nothing).
Divert requests are validated field by field (name characters and length, routing type, source different from forwarding) over REST and MCP alike, and names in the artemis-studio.capture. namespace are refused for create and delete on both. The broker.xml shown for a divert is now well-formed whatever its filter contains. The routing view finds Studio-created diverts with one bounded query instead of loading the whole divert audit history.
Match captured rows by address, isolate instances, and bound taps in bytes (69430080)
Full capture now reports, bounds and cleans up what it actually captured.
- Footprint, loss, size-bound enforcement, deletion and retention match captured rows by the address they were routed to. Previously they matched queue names, so a multicast address, or any queue named differently from its address, showed a footprint of zero, counted every message as lost, ignored its size bound and retention, and left payload behind on delete.
- Loss counts each routed message once per address rather than once per bound queue, clears DEGRADED after a clean pass, states the window the loss happened in, and says "loss cannot be estimated" for a filtered capture instead of reporting a number.
- A tap whose divert was removed outside Studio is reinstalled even while its drain is still running. Drains on nodes that stopped serving are stopped.
- Changing a live subscription's filter or bounds re-creates its taps so the change reaches the broker; the audit row records old and new values. Deleting or disabling a capture stops recording before rows are removed.
- A node with no Core URL is refused before any broker object is created. A refusal that will not change on its own (an invalid filter, a missing role) is recorded once as FAILED instead of being retried and audited every pass.
- Capture settings are scoped to the Studio instance that owns them, so one instance removing its taps no longer strips another instance's capture queues of their DROP policy and access restriction.
- Capture queues are bounded in bytes (artemis-studio.capture.max-ring-bytes, default 64 MiB) as well as in messages, and are configured for unlimited redelivery.
- Studio's own capture addresses are never captured; a pattern that can only match them is refused.
- Out-of-range subscription bounds are refused with the field and range instead of being silently clamped.
Stop jobs and write buffered events before releasing brokers (1ff5ffee)
Shutdown now stops background jobs before anything they use is released, waits briefly for runs in progress, writes broker events still buffered in memory, and lets in-flight SQL console tail polls finish, before broker calls are refused. Previously scheduled jobs could keep starting during teardown and buffered events were dropped with the process.
A broker-event batch whose database write fails is now kept, in order, for the next flush instead of being discarded; only overflow beyond the buffer size is dropped and counted, as before.
No action is needed after upgrading.
Commit the audit row before the broker call (c2419345)
Every mutating action now commits its audit row before Studio calls a broker, and records the outcome in a separate commit afterwards. Previously the row lived in the same transaction as the whole command, so a crash part-way through a cluster-wide command left brokers changed and no audit record at all, and the command held a database connection for every node it contacted.
A caller that rolls back before recording an outcome now leaves a failed row with that reason, never a row that looks in flight. Cluster-wide commands and message operations no longer hold a database transaction across broker calls.
Message move, retry, delete and expire by ids now report a partial outcome when they stop part-way: how many messages were acted on, which ids were not done, and why. The HTTP response is the new PartialView (partial: true) and the audit row records the failure with the count affected before it.
Acknowledge captured messages only after they are stored (c95a28bd)
Full capture no longer loses messages when Studio's database is slow or unavailable. Previously a failed write was logged and the batch was acknowledged anyway, so every message captured during a Postgres outage was removed from the capture queue and never stored. A shared buffer also let one capture queue acknowledge while another queue's rows were still being written.
Each capture drain now stores its own batch and acknowledges only after the store commits. When a store fails, capture pauses with increasing delay (1s to 5m) and retries; the bounded capture queue holds the backlog meanwhile and drops the oldest beyond its bound, which is counted as loss and shown with the cause. Capture queues are configured for unlimited redelivery so retries never discard messages on their own. Messages rejected by the ingest rate cap, and messages that cannot be read after three attempts, are counted and named as the cause instead of disappearing silently. Stopping a drain stores what it holds before acknowledging it.
Production traffic is unaffected in every case. No action is needed; existing capture taps pick up the redelivery setting on their next install.
Lower default MaxRAMPercentage to 50 (ca22dd40)
The image and compose defaults now give the JVM heap half of the container memory limit instead of three quarters. The rest is needed for metaspace, code cache, GC structures, direct buffers and thread stacks; at 75% a 1 GiB container had about 256 MiB for all of them.
If you set JAVA_OPTS yourself with -XX:MaxRAMPercentage=75, review it: keep it only if your container limit is well above 2 GiB.
Reuse one HTTP client per TLS bundle instead of one per Jolokia call (1850fb97)
Studio no longer accumulates threads and file descriptors while it polls brokers. Every Jolokia call used to build a new JDK HttpClient, and each one held a selector thread and an epoll descriptor until a garbage collection happened to reclaim it. On an idle heap that could take hours: deployments reached about 13,000 threads and were OOM-killed by their container limit every few hours, and the leaked descriptors contributed to exhausting the host's file table.
Clients are now shared, one per TLS bundle. A broker timeout change replaces them and shuts the old ones down gracefully; a reloaded TLS bundle gets a new client; all clients are shut down with the application. Changing a broker timeout at runtime also no longer re-enables following redirects.
A Core notification subscription that fails to start now releases its connection factory before it is retried, so an unreachable node no longer accumulates resources while it stays down.
No action is needed after upgrading.