Skip to content

2026.09.28 — 2026-09-20

Breaking

  • One configuration screen with the apply on it (8a9b5fb9)

    Configuration was four places answering one question. The declared items were on one tab, their live state on another in a different vocabulary, and the apply was a separate 453-line page behind a small header button. Saving an edit wrote a revision and said nothing about no broker having it; the only way to fix one drifted setting was to preview and confirm every pending step on the cluster.

    It is now one screen. Each declared item is a row carrying what it should be, what each live node runs in words — "in sync on 2/2", "differs on broker-2", "queue orders.audit missing on broker-1" — and the keys that differ, declared to observed. A queue's state is on its address's row, named, because a queue carries its own name and an address that reads "in sync" while one of its queues is gone is the misreading this screen exists to prevent. A status bar states where the declaration has got to ("Revision 4 — applied to 0 of 2 live nodes"), when the nodes were last evaluated and on what cadence, and holds Evaluate now and Review & apply. That sentence is what a save produces, so an edit's consequence is on the screen the editor returns to.

    Review & apply is a drawer over those rows: the same plan, hazards acknowledged one by one, the cluster's name typed to confirm, and the per-node result in the same component as the preview so the two compare row by row. All four outcomes are rendered, a small live region announces the stage and the outcome, and a row's "Apply this" opens the drawer scoped to that item alone — the address with its queues, one setting, one divert — on every targeted node. Untick every node and the drawer says so and plans nothing: the server reads an empty node set as "every node", so an empty selection is never sent.

    The open editor stays in the URL as ?section=&item=, so a half-made edit can be linked and restored.

  • Apply converges divergent queues and addresses (01709961)

    An apply now updates a declared queue or address that already exists but differs from the declaration, instead of reporting a finding it could never close (ADR-0082, ADR-0083). Before this, a cluster whose queues differed from the declaration stayed in drift after every apply, from the UI and from MCP.

  • Apply writes to brokers from the UI (76f37583)

    Apply on the Configuration screen now changes the brokers. Before this fix it only previewed. The UI sent the real run with no dryRun parameter, POST /config/apply treated a missing dryRun as a preview, and the screen showed the plan again without saying that nothing had been written. Drift never converged because nothing was ever applied.

    • POST /api/v1/clusters/{id}/config/apply now defaults dryRun to false, the same as every other lifecycle endpoint.
    • The web client now always sends dryRun explicitly, false included. A real action is never left to a server default.

    A real run is refused when expectedPlanHash is sent and the plan has changed since the preview. The hash is checked only when a caller sends it. The High-hazard acknowledgements and the step cap apply to every real run, as before.

    MCP broker_config_change did not go through this endpoint and is unchanged.

Added

  • The edit form opens on what the queue runs (449ccb25)

    Editing a queue offered five blank inputs under "leave blank to keep the current value". That form could not say what the queue was configured as, and reopened after an update it looked exactly as it had before — so an operator had no way to tell whether the change they had just applied took.

    The form now reads the queue's configuration from the nodes that have it (GET /queues/{name}/configuration, the read Artemis's replace-don't-merge update already does for itself) and opens on it. Applying sends only the fields that differ, the per-node outcome is stated as before, and the form reseeds from the broker afterwards, so the value on screen is the value the broker now reports. Apply stays disabled while nothing differs. Where the nodes disagree, the form says which node it is showing.

  • Review & apply shows the writes, not the whole plan (39b643bb)

    A plan lists every declared item on every node, and most of them already match — 17 steps to review for 6 writes, in a table whose values wrapped a word per line. The steps that write nothing now fold away behind "Show the 11 already as declared", counted, never silently dropped.

    • a create step lists what it would create, instead of every key as "— → value";
    • the step table's columns have widths, so "Change" has room and a two-digit step number stays on one line;
    • the node line says "6 steps would apply, 11 already as declared" rather than carrying a bare 17 beside it;
    • the plan's shape is stated once, in the summary bar, not three times.
  • One configuration screen with the apply on it (8a9b5fb9)

    Configuration was four places answering one question. The declared items were on one tab, their live state on another in a different vocabulary, and the apply was a separate 453-line page behind a small header button. Saving an edit wrote a revision and said nothing about no broker having it; the only way to fix one drifted setting was to preview and confirm every pending step on the cluster.

    It is now one screen. Each declared item is a row carrying what it should be, what each live node runs in words — "in sync on 2/2", "differs on broker-2", "queue orders.audit missing on broker-1" — and the keys that differ, declared to observed. A queue's state is on its address's row, named, because a queue carries its own name and an address that reads "in sync" while one of its queues is gone is the misreading this screen exists to prevent. A status bar states where the declaration has got to ("Revision 4 — applied to 0 of 2 live nodes"), when the nodes were last evaluated and on what cadence, and holds Evaluate now and Review & apply. That sentence is what a save produces, so an edit's consequence is on the screen the editor returns to.

    Review & apply is a drawer over those rows: the same plan, hazards acknowledged one by one, the cluster's name typed to confirm, and the per-node result in the same component as the preview so the two compare row by row. All four outcomes are rendered, a small live region announces the stage and the outcome, and a row's "Apply this" opens the drawer scoped to that item alone — the address with its queues, one setting, one divert — on every targeted node. Untick every node and the drawer says so and plans nothing: the server reads an empty node set as "every node", so an empty selection is never sent.

    The open editor stays in the URL as ?section=&item=, so a half-made edit can be linked and restored.

  • An apply can be scoped to one declared item (39135ca1)

    A row's "Apply this" on the Configuration screen sends the plan step identifiers it wants run. ApplyRequest takes stepIds (SECTION:key:OP, absent or empty means the whole plan) and the planner narrows the computed plan to them: steps that are not named are dropped on every node, hazards belonging to a dropped item go with them — acknowledging a hazard for a step that will not run teaches ticking without reading — and planHash and stepCount are recomputed over what is left.

    That last part is the safety property: ADR-0067 D12's "the plan you confirmed is the plan that ran" now covers a scoped run exactly, because a preview and its real run must name the same steps or the hash refuses. An identifier that matches nothing is ignored, so a client can name the three ops an item could take without knowing which one the plan chose. The audit row records the identifiers it was given.

    Canary order, verification, halting, the step cap and the hazard gate are unchanged. ADR-0087 records the decision, together with the screen redesign it exists for.

  • Apply converges divergent queues and addresses (01709961)

    An apply now updates a declared queue or address that already exists but differs from the declaration, instead of reporting a finding it could never close (ADR-0082, ADR-0083). Before this, a cluster whose queues differed from the declaration stayed in drift after every apply, from the UI and from MCP.

Fixed

  • A day's metric partition attaches even while samples are arriving (0e09b65f)

    The daily maintenance creates each day's metric_sample partition by moving the rows the default partition already holds for that day and then attaching it. The move and the attach were separate statements, so a scrape insert landing between them put a row for the day back into the default partition and Postgres refused the attach:

    ERROR: updated partition constraint for default partition "metric_sample_default" would be violated by some row

    The day was then left unpartitioned and every later run failed the same way, so an instance that hit it once stopped partitioning metric_sample until it was restarted — found on a dev instance after nine hours. The move and the attach are now one transaction holding EXCLUSIVE on the default partition, which a scrape insert waits on for the few milliseconds the move takes.

  • A deleted queue leaves the listing, and the panel follows the queue (ebb2957b)

    Three ways the queue screen showed an operator something that was no longer true:

    • a destroyed queue stayed in the listing until the next sweep, so the delete looked as though it had done nothing, and every action on the row that was left failed against an MBean that no longer existed. A delete now drops the queue's snapshot rows as part of the command;
    • the detail panel held a copy of the row taken when it was opened, so a pause, an edit or a delete never changed what it showed. It now holds the queue's name — in the URL, so the open queue can be linked and restored — and reads the row from the listing;
    • a pause applied on every node but not yet swept left a button still offering to pause. The control now shows the state the broker confirmed, and says the listing catches up on the next sweep.

    An action against a queue that is no longer on the node also reported a raw javax.management.InstanceNotFoundException; it now says the queue is gone and to refresh.

  • The overview map matches the product and folds away (fa9176e1)

    React Flow paints the minimap from its own palette, so on Studio's canvas it arrived as a white rectangle — the brightest thing on the screen, for the least important — and on a wide graph it covered the corner an operator was reading. It is painted from the flow tokens in both schemes, and a "Hide overview" control folds it away; the choice is per viewer and survives a reload.

  • A queue's messages refresh themselves (71706c17)

    Opening a queue browsed it once and then never again, so the page an operator was reading went stale the moment a message arrived or was consumed, and the only way to see the queue as it now is was to press refresh. The browse now refetches on the standard interval, one bounded page read at a time, and pauses with every other poll.

  • The configuration screen stays readable at any width (b3965f10)

    The declaration tables broke below about 1200px: a cell laid out its keys in a grid whose key column took whatever it needed, so a long one — a drift finding's queue ORDERS.inbound.audit max-consumers — left the value a character wide and stretched one row to the height of a screen. A declaration of six addresses ran to 11,700px at 900px wide; it is now 2,100px.

    • each section's table scrolls horizontally below 760px instead of crushing its columns, and the columns have explicit widths so the four sections line up with each other;
    • a key/value pair wraps rather than driving the value out of its cell;
    • a missing item says "missing on <node>" once. It used to add every declared key against an em dash, which said nothing the sentence had not, and a node carrying two findings for one label was named twice.
  • A purge overrides the bulk cap only where the count is known (a80e9afc)

    The purge dialog sent override=true on every confirmation, which disabled the server's bulk safety cap. That was unreachable until the dialog began arming the confirmation when the depth estimate failed; from then on a purge with no stated blast radius could destroy any number of messages with the last guard switched off.

    The override now travels only when the preview reported the estimate over the cap, and that case states the number and the cap before the confirmation can arm, as the queue delete already did. When the depth is unknown the purge runs without the override and the dialog says the cap still applies, so a purge over it is refused by the broker with its count rather than waved through.

  • A delete no node was live for keeps the queue open (19748f2a)

    Deleting a queue on a cluster where no node is live returned a result the dialog read as success: nothing failed and the run is not partial, because nothing settled anywhere. The queue view was dismissed as though the queue were gone, while the same result said "0 live nodes · N not live".

    The dialog now closes the view only when every node reports APPLIED or ALREADY, and that judgement lives once, as appliedEverywhere() next to the summary that renders it, so the next caller cannot re-derive it differently. A result with no live node also says so in words instead of "Applied to all 0 live nodes".

  • A purge whose estimate failed says so instead of waiting forever (a36b224c)

    Opening Purge queue takes a dry-run count first. When that call failed, the dialog kept reading "Estimating current depth…" and its confirmation stayed disabled with no reason given, so the purge could not be run at all and nothing on screen said why. The failure is now stated in the dialog, with the warning that the depth is unknown, and the typed confirmation is armed the same as it is with a number.

  • A delete that failed on every node keeps the queue open (74f4f6e0)

    The delete dialog closed the queue behind it whenever the run was not partial. A run where every node failed is not partial either, so a delete that destroyed nothing dismissed the queue's view as though the queue were gone. The view now stays open unless no node failed, and the dialog's own result keeps saying which node refused and why.

  • A live tail on a captured queue shows what was consumed (ea811449)

    A live tail over a captured queue showed nothing while messages were flowing through it. Three separate things were wrong, and all of them had to be fixed.

    A capture drain only stored and acknowledged what it had read once fifty messages had accumulated. On an address that carries a handful of messages a minute that never happens, so the copies sat in the drain — unstored, so the index answered nothing about them, and unacknowledged, so the capture queue kept growing towards its ring bound while the drain looked healthy. Drains now commit a partial batch on a capture-flush job, every artemis-studio.capture.flush-interval (1s by default). A captured message becomes answerable, and leaves the capture queue, within one interval. Each flush runs on its own virtual thread, so a stop and start of the application context leaves the flush path working rather than silently rejecting from then on.

    A tail over a plan that resolved onto the index still re-read the queues. That is the one thing capture exists to make unnecessary: a message consumed the instant it arrives is gone from the queue, and the copy the divert took is the only place it still exists. Such a tail now reads the index, oldest first, from where it left off — which is what the console's captured banner has been promising (ADR-0062 D2).

    Where it left off follows what the query returned, not what the tail chose to show. A window that only moved for delivered rows stood still as soon as the index held a page's worth of already-seen rows inside it: every poll re-read the same page, anything written behind it fell past the row limit, and the tail went quiet again — at roughly one message a second on a query with LIMIT 20. A page the row limit cut short now continues from its last row.

    You will notice a captured tail delivering messages your applications consume, and a capture queue on a quiet address sitting at zero rather than slowly filling. Nothing to do on upgrade. Set artemis-studio.capture.flush-interval higher if you would rather trade tail latency for fewer writes.

  • The plain query reads the index only where capture is active, and says where it begins (36044523)

    A SQL query with no source qualifier, such as SELECT * FROM "ORDERS.IN", read the message index whenever an enabled CAPTURE subscription matched the queue. Two cases still answered with a short list and said nothing:

    • A CAPTURE subscription whose capture was pending or refused on the node the queue sits on copies nothing there, but still sent the query to the index. The plain query now reads the index only when capture is ACTIVE on every target's node; otherwise it reads the live brokers.
    • Capture does not backfill. A queue holding 3 messages when its CAPTURE subscription was created answered the plain query with 0 rows. An index query with no time window now carries a coverage notice naming the time the index begins and pointing at FROM broker."Q" for what is on the queue now. Windowed queries are unchanged: they get the notice only when the window reaches before capture began or past retention.

    ADR-0086 and the SQL console guide are corrected to match.

  • A consumed queue is browsed over Core again (f14aeba4)

    The message view reads a page over the Core channel and checks it against the broker's count, reading the page again over Jolokia when it comes back short (the paging-queue fix in this release). That check compared the page with the queue's MessageCount, which also counts messages delivered to a consumer and not yet acknowledged, and scheduled messages. A browser never returns either, so every page of a queue with a live consumer or a scheduled message looked short. Each page then cost a Core browse, a count read and a Jolokia browse, and was served over Jolokia, losing the Core channel's exact bodies and property types on exactly the queues being consumed.

    The page is now checked against MessageCount less DeliveringCount and ScheduledCount, read together in the same single request. The total shown for the queue is still the broker's MessageCount. Nothing to do on upgrade.

  • A query on a sampled queue reads the live brokers (6561e12a)

    A SQL query with no source qualifier, such as SELECT * FROM "ORDERS.IN", read the message index whenever any subscription covered the queue, including a SAMPLE subscription. A sampled index holds only what a poll happened to see, and nothing before the first poll: a queue holding 3 messages answered with 0 rows right after its SAMPLE subscription was created.

    The plain query now reads the index only when an enabled CAPTURE subscription covers every queue it names. A queue that is only sampled is read from the live brokers, which means the query now costs broker reads and falls under the cost ceiling like any other broker query. To read what a sampled index holds, name it: SELECT * FROM index."ORDERS.IN". The console's syntax help and the SQL console guide say so. See ADR-0086.

  • A paging queue no longer browses as short or empty pages (05ded93e)

    When Studio reads messages over the Core connection, a queue whose address is paging to disk could come back with pages missing messages, or empty, while the header showed the broker's full count. The JMS browser ends as soon as the next message is not already on the client, which on a paging queue happens part-way through. Measured on a 120-message paging queue: pages of 50, 46 and 20, or a first page of 100 holding 32.

    A page that comes back shorter than the broker's own count says it should be is now read again over the management channel, and the view reports that channel as the one that served it. The message view and SQL console queries against the live broker both read pages this way.

  • Browse and SQL find queues the scrape has not reached (39e91fd2)

    A queue created a moment ago is on the broker before Studio's scrape reaches it. Until then, opening it answered "queue does not exist" and a SQL query naming it said "No queue on this cluster matches", though the broker already held its messages. Both now look the queue up live on each live node, with one management read per node, and read from where it is.

    A SQL FROM that names one queue outright is looked up live; a wildcard pattern still matches only the queues the scrape has seen. The live lookup also reads each copy's depth, so the query's cost estimate and the choice of node to browse include a queue found this way.

  • Opening a queue reads the node that holds its messages (be04141f)

    On a cluster with more than one live node, every live node has its own copy of a queue. Opening a queue with no node chosen read from whichever live node Studio happened to list first, so a queue with thousands of messages on one node could open as an empty page read from another. The queue list showed the messages; the queue itself showed none.

    With no node chosen, the message view now reads the copy that held the most messages at the last scrape. The header still names the node it read from, and the node selector still reads any other copy. Purge and the message actions, which default to the same node, now act on the copy the view shows.

  • Queue delete handles consumers and dependent diverts (aeadd0d4)

    Deleting a queue now checks every node first, in the preview and again for real (ADR-0084, ADR-0085):

    • A queue with consumers attached is refused on that node, with the count, instead of failing with a raw broker error. Tick "Disconnect this queue's consumers" in the delete dialog (REST disconnectConsumers=true, MCP queue_lifecycle disconnectConsumers: true) to close them and delete it. A consumer that attaches after the check gets the same explanation.
    • A divert that forwards into the queue's address is removed with the queue, before it, when the delete leaves that address with no queue and no divert bound. Left in place it re-created the queue on the next message (auto-create on) or broke every producer of its source address (auto-create off). The preview names each one per node; a divert that also sits in the cluster's declared configuration is flagged as coming back on the next apply, and every such preview warns that a divert in broker.xml returns at the next restart.
    • Diverts whose source is the queue's address are kept, and named as kept. They keep the address bound, so a divert into it is kept too and named as kept: a chain SRC -> DST -> OTHER still delivers after DST's queue goes.
    • Capture taps on the address are left to their capture subscription. The preview says which ones the subscription removes and which it keeps because it still captures another queue there.
    • If a divert cannot be removed, that node keeps its queue. If the queue delete fails after its diverts went, the node's error names them. Each removed divert's full configuration is on the node's result and in the audit row, so it can be recreated exactly.
    • A node whose queue, consumer or address state cannot be read is refused as "could not be checked", never assumed to have nothing to delete.

    A queue the scrape has not reached yet (just created, or on a node whose sweep has not come round) is now found on the live nodes instead of answering 404.

    On every lifecycle command, a node's preview warning is now repeated on its real result, so what was confirmed and what happened read the same. A preview with refused nodes now says so in its headline ("Would apply to 0 of 1 nodes, 1 refused") instead of counting them as nodes it would apply to.

  • Turning capture off removes its divert immediately (bc03cf9e)

    Disabling a capture subscription, or switching it from capture to sampling, now removes its divert and capture queue from every serving node as soon as the change commits. Before, the drain stopped at once but the divert stayed on the broker until the next scheduled reconcile (every 30s by default, longer if that pass was skipped or failed), copying every message on the address into a capture queue nothing was emptying. Deletion already swept at once; disabling now does the same.

    The one exception: if a reconcile pass (on this or another Studio instance sharing the database) holds the cluster's lock at the moment the change commits, the immediate sweep is skipped and the divert is removed by the next pass instead, as before.

    A reconcile pass also stops revisiting a cluster once no serving node has a capture tap wanted or left on it. A node whose orphan could not be removed, or that did not answer, keeps its cluster on the next pass, so the removal is retried.

    No action is needed on upgrade. A capture queue that filled up under the old behaviour is removed the next time the reconciler sees its subscription is disabled.

  • Apply writes to brokers from the UI (76f37583)

    Apply on the Configuration screen now changes the brokers. Before this fix it only previewed. The UI sent the real run with no dryRun parameter, POST /config/apply treated a missing dryRun as a preview, and the screen showed the plan again without saying that nothing had been written. Drift never converged because nothing was ever applied.

    • POST /api/v1/clusters/{id}/config/apply now defaults dryRun to false, the same as every other lifecycle endpoint.
    • The web client now always sends dryRun explicitly, false included. A real action is never left to a server default.

    A real run is refused when expectedPlanHash is sent and the plan has changed since the preview. The hash is checked only when a caller sends it. The High-hazard acknowledgements and the step cap apply to every real run, as before.

    MCP broker_config_change did not go through this endpoint and is unchanged.

Apache-2.0. Apache ActiveMQ and Apache ActiveMQ Artemis are trademarks of the Apache Software Foundation. Artemis Studio is an independent project, not produced by, endorsed by, or affiliated with the ASF.