Skip to content

add bus prune to remove the subscriptions and bindings a bus no longer needs - #364

Open
adenhertog wants to merge 3 commits into
masterfrom
issue-283-prune-transport-topology
Open

adenhertog wants to merge 3 commits into
masterfrom
issue-283-prune-transport-topology

Conversation

@adenhertog

@adenhertog adenhertog commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #283

Summary

Adds bus prune and bus.prune({ apply }), which find the subscriptions and bindings of a service's own queue to topics or exchanges it no longer handles and, with --apply, remove them. SQS and RabbitMQ implement it.

Background

#333 delivered bus provision. #283 was narrowed to pruning: after a rollout, removing what the configuration no longer needs, with a report-only default and SQS and RabbitMQ support.

Problem

Provisioning only adds. When a service stops handling a message, its queue stays subscribed (SNS) or bound (RabbitMQ), so it keeps receiving the message and the bus discards it. Nothing cleans that up.

Approach

  • bus-core: bus.prune({ apply?, only?, allowEmpty?, allowCustomHandlersWithoutTopic? }) calls the new optional Transport.prune(TransportPruneOptions) (provision's options with apply instead of dryRun, plus only) and returns PruningPlan[] ({ adapter, resources, removed, failed, notFound }, reusing ProvisionedResource). only is an approved list: transports remove only what's both in it and still found, and report the rest in notFound. With apply, transports try every removal and return failures in failed; bus.prune() then throws PruneIncomplete, whose plans say exactly what was removed and what failed. It throws PruneRefused for a send-only bus or a scheduler, a bus that handles nothing unless allowEmpty, and a bus with a custom handler without a topicIdentifier unless allowCustomHandlersWithoutTopic (for handlers whose messages are sent straight to the queue, such as S3 notifications).
  • bus-sqs: reads every page of ListSubscriptions (the caller's own subscriptions in the client's region), keeps sqs subscriptions whose endpoint is the service queue, and reports those to topics outside planResources().subscriptions. It reads each candidate with GetSubscriptionAttributes and skips, with a warning, any with raw message delivery, a filter policy, a redrive policy or a subscription role, which the bus never sets. With apply it unsubscribes them, settling every call. only matches on subscriptionArn. Pending subscriptions are skipped. The queue policy, topics and queues are never touched.
  • bus-rabbitmq: new optional managementUrl (credentials and vhost default from connectionString; RabbitMqManagementUrlMissing with fix-it help when it's missing). It lists GET /api/queues/{vhost}/{queue}/bindings and only keeps bindings shaped like the bus' own: from a fanout exchange, routing key '', no arguments, never the default exchange or the queue's own exchange. It unbinds over AMQP, one channel per binding, so a refused unbind (which closes the channel) doesn't fail the rest. only matches on exchange and queue.
  • bus-cli: bus prune <module> [--export] [--apply] [--allow-empty] [--allow-custom-handlers] [--check] [--json], using the same loader as bus provision. It always does a report-only pass first and prints it before removing anything (to stderr with --json), then passes exactly what it printed as only, so it never removes anything it didn't print. The JSON is PruneReport (formatVersion: 1, with removed, failed and notFound per adapter). On a partial failure it prints the report, then the error, and exits 1. Exit codes are 0, 1 (load, refusal or failure), 2 (usage, including --check --apply) and 3 (--check found something).
  • Docs: a Pruning section in the provisioning guide covers ordering (provision, roll out, prune), rollback (re-run bus provision of the old version), the approved-list and partial-failure behaviour, the custom-handler opt-in, and removing an old queue with the broker's tools. Each adapter page has a "Pruning" section with its deploy permissions (SQS now includes sns:GetSubscriptionAttributes, and notes that cross-account and cross-region subscriptions are never found). The custom transport page, the bus-cli and bus-rabbitmq READMEs and the CLAUDE.md files are updated too.
  • Tests: bus-core delegation, only, PruneIncomplete and refusals (including the opt-in); the CLI with the fake importer (including the race where more is found between printing and removing, and a partial failure in text and JSON) and through bus.mjs; both transports with mocked clients (approved lists, hand-made SNS subscriptions, a failed unsubscribe or refused unbind not stopping the rest). Integration tests on LocalStack and RabbitMQ provision with handlers A and B, then prune with only A. They check the dry run lists B, apply with an approved list removes only that, apply removes B, another queue's subscription or binding to B survives, external custom-handler topics are kept or removed correctly, raw and filtered SNS subscriptions and bindings not made by the bus survive, and the refusals hold.

The CLI's prune fixture lives in test/provision/ (sharing its type: module package.json), so it needs no lockfile importer.

Follow-ups for other transports: #361 (Postgres transport), #362 (bus-redis) and #363 (Azure Service Bus, blocked by #346).

  • This is original work under the clean-room policy, not ported, translated or copied from another messaging framework
  • Added a changeset (pnpm changeset) for user-facing changes to published packages, or none is needed
  • Docs: updated docs/ for user-facing changes, or none needed

🤖 Generated with Claude Code

adenhertog and others added 3 commits October 25, 2026 20:34
…r needs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ort partial failures

- bus.prune() takes an approved list (only), which bus prune --apply sets to what it printed
- transports try every removal and return removed, failed and notFound; bus.prune() throws PruneIncomplete on failures
- sqs skips subscriptions with raw delivery, a filter or redrive policy or a role
- allowCustomHandlersWithoutTopic / --allow-custom-handlers opts in to pruning custom handlers without a topic
- move the cli prune fixture into test/provision so it needs no lockfile importer

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI: apply and prune transport topology (queues, topics, subscriptions)

1 participant