Answering and announcing are different acts. The reply goes to whoever asked and is correlated to their request; the announcement says to the whole mesh that a module now exists at a commit, which is what the catalogue places in the module graph (novox/hq ADR 0072). A build nobody asked for still has to be announced, or the graph knows less than the registry does. What it was built on top of is read out of the build's own inputs rather than declared, because a declared list drifts from what the code actually uses (ADR 0009). These are artifact references, which is what a build input names; resolving them to module-versions is the catalogue's work, since it is what knows which module-version published which artifact. Events ride the topic exchange, not the direct one nodes speak over, so the builder's account is granted both: it must be able to answer and to announce. The envelope is the sdk's, reproduced exactly — a second shape would be a second thing for consumers to handle, and they are written against the first. Announcing is not allowed to fail a build. The work was done and was answered; a build reported as failed because saying so failed is a lie about it. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
73 lines
2.7 KiB
Go
73 lines
2.7 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
)
|
|
|
|
// Emitting a module event from Go.
|
|
//
|
|
// **Every event rides one topic exchange** (novox/hq ADR 0042), which is not the direct exchange
|
|
// nodes and the control plane speak over. A module that announces something publishes here, and
|
|
// consumers bind their own durable queue to a pattern over it.
|
|
//
|
|
// This exists because the builder is a module written in Go while every other emitter is
|
|
// TypeScript on the sdk. The envelope is the sdk's, reproduced exactly: the body is the payload
|
|
// alone and everything about the event travels as headers. A second shape would be a second thing
|
|
// for consumers to handle, and they are written against the first.
|
|
const (
|
|
// EventsExchange is where every event rides. Named here rather than imported from the broker
|
|
// package for the same reason BuildQueueName is duplicated there — one direction of dependency.
|
|
EventsExchange = "mesh.events"
|
|
)
|
|
|
|
// 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 {
|
|
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",
|
|
},
|
|
})
|
|
}
|
|
|
|
// eventID is what a consumer deduplicates on: delivery is at-least-once, so a handler must be able
|
|
// to tell a redelivery from a second event, and only the emitter can say which it is.
|
|
func eventID() (string, error) {
|
|
raw := make([]byte, 16)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return "", fmt.Errorf("cannot make an event id: %w", err)
|
|
}
|
|
return hex.EncodeToString(raw), nil
|
|
}
|
|
|
|
// KeyModuleBuilt is what the builder announces when it has built something. The catalogue places
|
|
// it in the module graph; nothing else need care.
|
|
const KeyModuleBuilt = "module.builder.built"
|