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
+7 -25
View File
@@ -6,9 +6,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
// Emitting a module event from Go.
@@ -29,32 +26,17 @@ const (
// EmitEvent publishes one module event, in the envelope the sdk's consumers expect.
//
// Persistent, because an event that a broker restart loses is not an announcement. The publish is
// not confirmed here: the caller has already done the work the event describes, and a build that
// succeeded must not be reported as failed because saying so failed.
func EmitEvent(ctx context.Context, channel *amqp.Channel, eventType, source, node string, body any) error {
// The envelope is the transport's to write (bus.go) and this is only what goes in it, which is
// what lets one conformance fixture hold both implementations to the same headers.
func EmitEvent(ctx context.Context, bus Bus, eventType, source, node string, body any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("cannot serialise a %s event: %w", eventType, err)
}
id, err := eventID()
if err != nil {
return err
}
return channel.PublishWithContext(ctx, EventsExchange, eventType, false, false, amqp.Publishing{
ContentType: "application/json",
DeliveryMode: amqp.Persistent,
MessageId: id,
Timestamp: time.Now().UTC(),
Body: payload,
Headers: amqp.Table{
"x-event-id": id,
"x-source": source,
"x-node": node,
"x-time": time.Now().UTC().Format(time.RFC3339),
"content-type": "application/json",
},
})
// The publish is not confirmed by the caller: it has already done the work the event
// describes, and a build that succeeded must not be reported as failed because saying so
// failed. Each transport decides what "published" means for it.
return bus.PublishEvent(ctx, eventType, source, node, payload)
}
// eventID is what a consumer deduplicates on: delivery is at-least-once, so a handler must be able