ADR-0093: Bulk operations are persisted runs over the single-queue commands
- Status: accepted
- Date: 2026-09-21
- Deciders: Artemis Studio maintainers
Context
Operators need to pause, resume, purge or delete many queues at once (Roadmap B). Each of those already exists for one queue. It runs through BrokerCommands, which provides:
- per-node fan-out;
- preflight;
- the bulk safety cap (ADR-0022);
- an audit row committed before the broker call (ADR-0078);
- the per-node rate limiter.
A bulk operation that reimplemented any of that would be a second, weaker copy of the safety model.
A 200-queue delete takes minutes. An HTTP request cannot hold it honestly: no progress, no stop, and a lost result on reload. kernel.jobs schedules recurring work only, and there is no user-submitted job facility. The audit trail has no way to say "these forty rows are one operator action".
Decision
- A bulk run is one operation over a frozen set of queues, resolved and hashed at preview time. Execute must echo the hash, and the preview expires after 10 minutes. Nothing added after the preview joins the run.
- The preview estimates from the aggregated queue list: one batched read per node, never a dry run per queue (non-negotiable #1). An unanswered node is "unknown", never zero. The authoritative preflight still runs per queue at execution.
- Execution is sequential, and each queue goes through the existing single-queue command, unchanged. Safety, audit and rate limiting are inherited, not re-implemented.
- Runs are persisted (
bulk_run,bulk_run_item, owned byfeature/bulk).- They execute on a virtual thread that carries the initiating operator's identity, so grants are re-checked per queue.
- A partial unique index allows one running run per cluster.
- The default failure policy is stop at the first failed or partial queue, and continuing is opt-in. Stop is cooperative: the queue in flight finishes.
- A restart marks a running run
INTERRUPTEDand its in-flight queueUNKNOWN. It is never resumed. - Two caps.
- Queue count,
safety.bulk-queue-cap, default 200, with no override. It bounds how long a run can be. - Estimated messages, the existing
safety.bulk-cap, overridable. It bounds how much data one confirmation can destroy.
- Queue count,
- Audit grouping is a nullable
audit_event.parent_id, bound through aScopedValue(AuditScope.PARENT) thatAuditService.beginreads. No signature of an existing service or ofBrokerCommandschanges. - Bulk lives in its own module, depending on
queues,messagesandresources. The queues screen shows bulk actions through a kernel slot (queues.selection), so queues never depends on bulk (ADR-0069, ADR-0070).
Consequences
- Every bulk safety property is the single-queue property, proven by the single-queue tests. A fix to a single-queue command fixes bulk too.
- Sequential execution is slower than parallel. That is deliberate: Studio must never be why a broker falls over.
- The stop flag and the runner are in-process, so Studio assumes one instance per deployment, as every supported deployment is today. The unique index keeps a second instance from starting a concurrent run on a cluster. It would still need a DB-polled stop flag, and that is the change to make if Studio ever runs replicated.
kernel.jobsgains no async-job abstraction. When a second feature needs persisted, observable, stoppable work, extract it fromfeature/bulkthen.- Bulk move/retry and bulk over MCP are not covered. Each needs its own decision.