One bus: the AMQP transport is gone from the controller
The mesh runs on the seat's bus alone (novox/hq ADR 0131, design 28 task 5.5). The old transport's consume loop, build request, tool ask, management API and account scoping are deleted, and the bus switch with them; the controller connects to the broker seat and to nothing else. The store-window tests keep their assertions on a bus-less fake, and the tests that only made sense for the old transport's in-memory holding go with it.
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
// Asking a machine to build a module, and hearing what came out.
|
||||
@@ -22,21 +17,6 @@ import (
|
||||
// holds no opinion about what they contain, and a host that also built things would be a host
|
||||
// with a container runtime requirement and a git dependency (novox/hq ADR 0005).
|
||||
|
||||
// BuildQueue is where build requests wait. One queue, so several build machines can share the
|
||||
// work and each request is done exactly once — which is what a queue is for and what a
|
||||
// per-machine routing key would not give.
|
||||
const BuildQueue = "builds"
|
||||
|
||||
// KeyBuilt is what a builder publishes when it has finished, successfully or not.
|
||||
const KeyBuilt = "built"
|
||||
|
||||
// ReplyQueue is where the answer to one request goes.
|
||||
//
|
||||
// **Named here rather than left to the broker**, so it can be scoped and reasoned about. A broker
|
||||
// generates its own name for an unnamed queue, and a builder permitted to write to whatever that
|
||||
// convention happens to produce works on one broker and silently cannot answer on another.
|
||||
func ReplyQueue(id string) string { return BuildQueue + ".reply." + id }
|
||||
|
||||
// BuildRequest is one module to build.
|
||||
type BuildRequest struct {
|
||||
// ID correlates the answer with the asking. Not the module name: two builds of one module can
|
||||
@@ -118,77 +98,3 @@ type MadeArtifact struct {
|
||||
Kind string `json:"kind"`
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
// RequestBuild asks for a module to be built and waits for the answer.
|
||||
//
|
||||
// Waiting rather than returning immediately, because the thing a person wants after asking for a
|
||||
// build is to know whether it worked. A build that is dispatched and forgotten needs somewhere to
|
||||
// look afterwards, and there is nowhere yet.
|
||||
func RequestBuild(ctx context.Context, channel *amqp.Channel, request BuildRequest,
|
||||
timeout time.Duration) (BuildResult, error) {
|
||||
|
||||
// Its own queue for the answer, declared before the ask. Consuming from the shared exchange
|
||||
// would mean competing with the control plane's own consumer for a message meant for this
|
||||
// caller — which is the fault this package's own doc comment records having had.
|
||||
replies, err := channel.QueueDeclare(ReplyQueue(request.ID), false, true, true, false, nil)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
// Bound to the exchange, and the answer comes back through it.
|
||||
//
|
||||
// **A builder never publishes to the default exchange**, because permission there is per
|
||||
// exchange and not per queue — a builder allowed to use it could publish into any node's
|
||||
// queue, which is the privilege a build machine most obviously should not have. Found by
|
||||
// running it: the builder built, could not answer, and the connection closed saying only
|
||||
// "not allowed to publish to exchange ''".
|
||||
//
|
||||
// The cost is that every asker sees every result, which is why the correlation is checked
|
||||
// below rather than assumed.
|
||||
if err := channel.QueueBind(replies.Name, KeyBuilt, Exchange, false, nil); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
answers, err := channel.ConsumeWithContext(ctx, replies.Name, "", true, true, false, false, nil)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
if err := channel.PublishWithContext(ctx, "", BuildQueue, false, false, amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
DeliveryMode: amqp.Persistent,
|
||||
CorrelationId: request.ID,
|
||||
ReplyTo: replies.Name,
|
||||
Body: body,
|
||||
}); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
|
||||
waiting, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
for {
|
||||
select {
|
||||
case <-waiting.Done():
|
||||
return BuildResult{}, fmt.Errorf(
|
||||
"no builder answered within %s. Something must be consuming %q, and nothing is "+
|
||||
"— or it is building something that takes longer than this",
|
||||
timeout, BuildQueue)
|
||||
case delivery, ok := <-answers:
|
||||
if !ok {
|
||||
return BuildResult{}, fmt.Errorf("the connection closed while waiting for a build")
|
||||
}
|
||||
var result BuildResult
|
||||
if err := json.Unmarshal(delivery.Body, &result); err != nil {
|
||||
return BuildResult{}, fmt.Errorf("a builder answered with something unreadable: %w", err)
|
||||
}
|
||||
if result.ID != request.ID {
|
||||
// Somebody else's answer on this queue. Ignored rather than returned, because
|
||||
// returning it would attribute one build's outcome to another's.
|
||||
continue
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user