The controller's outbound link behind a seam, with both transports

Step 3.4, first half. Every one of these took an *amqp.Channel, so the
transport reached every caller and swapping it meant touching all of them.
The seam turned out to be small — the controller sends exactly two kinds of
message that expect no answer — which is the same measurement that said
this bus could be replaced at all.

Bus is stated in the mesh's words, not a transport's: PublishEvent and
PublishDeclaration. Two implementations, both shipping, because steps 1 to
4 leave every node on AMQP and the NATS one is selected at the rollout.
Both ship is also what makes them comparable: one conformance fixture holds
both to the same envelope, and the NATS one is checked against a real
server reading back from the stream rather than from the code that wrote it.

Still on *amqp.Channel: RequestBuild and Ask, which carry reply-queue
machinery, and the whole consume side — the control loop, enrolment, serve.
This commit is contained in:
2026-09-26 23:47:15 +02:00
parent 1801f1178e
commit 2fad32767e
7 changed files with 231 additions and 43 deletions
+2 -12
View File
@@ -5,8 +5,6 @@ import (
"encoding/json"
"fmt"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
// Signer is whatever holds the control plane's signing key.
@@ -21,7 +19,7 @@ type Signer interface {
// something else, and the node would refuse a declaration that was genuinely the mesh's.
//
// Published to the node's own queue, which its account alone may read.
func Declare(ctx context.Context, channel *amqp.Channel, signer Signer, node string,
func Declare(ctx context.Context, bus Bus, signer Signer, node string,
declaration []byte, timeout time.Duration) error {
if !json.Valid(declaration) {
@@ -41,13 +39,5 @@ func Declare(ctx context.Context, channel *amqp.Channel, signer Signer, node str
publish, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Published to the queue directly rather than through the exchange: a declaration is for one
// node, and routing it by name through a shared exchange would mean a binding per node that
// nothing removes when a node is retired.
return channel.PublishWithContext(publish, "", QueueFor(node), false, false,
amqp.Publishing{
ContentType: "application/json",
DeliveryMode: amqp.Persistent,
Body: body,
})
return bus.PublishDeclaration(publish, node, body)
}