Files
mesh-host/internal/link/witnessing.go
T

111 lines
4.4 KiB
Go

package link
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/nats-io/nats.go"
)
// What a witness asks the bus (novox/hq to-be 45 §8): the node-engine judging the node tools it placed on this
// machine, by what the bus says about them rather than by what they say about themselves.
//
// **One question, read-only, on the host's own link**: this machine's tool runtime, asked the NATS services protocol's
// PING by its name and this machine's, so only this machine's runtime can answer it.
// ErrCannotAsk is a question the bus did not let this host put, or did not answer: no grant for it,
// no such bucket, the bus slow. **It says nothing about the build being judged**, so a witness
// counts none of it against the build's bound.
var ErrCannotAsk = errors.New("this host cannot ask the bus")
// ErrNoAnswer is a question the bus delivered and nobody answered in time: the build being judged is
// not there to answer. Counted against its bound.
var ErrNoAnswer = errors.New("nothing answered")
// Asker is what a witness asks through. The link open now implements it; nil while there is none.
type Asker interface {
// Ping asks the service `service`'s instance `id` whether it is there. Nil is an answer;
// ErrNoAnswer is none in time; ErrCannotAsk is no asking.
Ping(ctx context.Context, service, id string) error
}
// PingSubject is the services protocol's PING of one instance of one service.
func PingSubject(service, id string) string { return "$SRV.PING." + service + "." + id }
// Asker is the link open now, when there is one and it can ask.
func (q *Session) Asker() Asker {
q.init()
q.mu.Lock()
defer q.mu.Unlock()
if a, ok := q.bus.(Asker); ok {
return a
}
return nil
}
func (l *natsLink) Ping(ctx context.Context, service, id string) error {
_, err := l.stated(ctx, service, id)
return err
}
// ToolRunnerService is the services-protocol name the tool runner answers discovery under, and CapabilitiesKey the metadata
// key of its PING it states the capabilities of the calls it answers under, comma-separated (mesh-tools announce).
const (
ToolRunnerService = "node-tools"
CapabilitiesKey = "capabilities"
)
// Stater is a link that can ask what a service's instance states of itself in its PING: its metadata.
type Stater interface {
Stated(ctx context.Context, service, id string) (map[string]string, error)
}
// Stated is what the tool runner of this machine states in its PING, asked of its own instance as a witness asks it.
func (l *natsLink) Stated(ctx context.Context, service, id string) (map[string]string, error) {
return l.stated(ctx, service, id)
}
func (l *natsLink) stated(ctx context.Context, service, id string) (map[string]string, error) {
subject := PingSubject(service, id)
before := l.conn.LastError()
msg, err := l.conn.RequestWithContext(ctx, subject, nil)
switch {
case errors.Is(err, nats.ErrNoResponders):
// The bus delivered the question and nothing subscribes to it: the runtime is not on the bus.
return nil, fmt.Errorf("%w: no %s on this machine is on the bus", ErrNoAnswer, service)
case err != nil:
if refused := l.refusal(subject, before); refused != "" {
return nil, fmt.Errorf("%w: asking %s: %v%s", ErrCannotAsk, subject, err, refused)
}
return nil, fmt.Errorf("%w: %s did not answer: %v", ErrNoAnswer, subject, err)
}
var ping struct {
Name string `json:"name"`
ID string `json:"id"`
Metadata map[string]string `json:"metadata"`
}
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Name != service || ping.ID != id {
return nil, fmt.Errorf("%w: what answered %s is not %s %s", ErrNoAnswer, subject, service, id)
}
return ping.Metadata, nil
}
// refusal is the bus's word on this connection when it refused a publish to subject while this question
// was asked, said as the end of an error; empty when it did not. before is the connection's last error
// as it stood before asking: the client keeps its last error for the life of the connection, so a
// refusal from an earlier question — under a grant since widened — is not said again as this one's.
func (l *natsLink) refusal(subject string, before error) string {
last := l.conn.LastError()
if last == nil || last == before {
return ""
}
words := strings.ToLower(last.Error())
if strings.Contains(words, "permissions violation") && strings.Contains(last.Error(), subject) {
return " — the bus refused it: this host's grant does not name " + subject
}
return ""
}