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 controller and the // node tools it placed on this machine, by what the bus says about them rather than by what they // say about themselves. // // **Two questions, both read-only, both on the host's own link.** The controller's lease key, read // by JetStream's direct get — one subject, no stream information, nothing written; and 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 { // ReadKey is a key-value bucket's current value for a key. Found false is nobody holding it — // absent, deleted or expired; an error wrapping ErrCannotAsk is no reading at all. ReadKey(ctx context.Context, bucket, key string) (value []byte, found bool, err error) // 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 } // DirectGetSubject is the one subject a host asks a bucket's key on: JetStream's direct get of the // bucket's stream, for the key's own subject. What a host's grant names exactly — no wildcard. func DirectGetSubject(bucket, key string) string { return "$JS.API.DIRECT.GET.KV_" + bucket + ".$KV." + bucket + "." + key } // 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) ReadKey(ctx context.Context, bucket, key string) ([]byte, bool, error) { subject := DirectGetSubject(bucket, key) before := l.conn.LastError() msg, err := l.conn.RequestWithContext(ctx, subject, nil) switch { case errors.Is(err, nats.ErrNoResponders): return nil, false, fmt.Errorf("%w: the bus has no bucket %s that answers a direct get", ErrCannotAsk, bucket) case err != nil: return nil, false, fmt.Errorf("%w: reading %s from %s: %v%s", ErrCannotAsk, key, bucket, err, l.refusal(subject, before)) } if msg.Header != nil { switch status := msg.Header.Get("Status"); status { case "": case "404": return nil, false, nil default: return nil, false, fmt.Errorf("%w: reading %s from %s: the bus answered %s %s", ErrCannotAsk, key, bucket, status, msg.Header.Get("Description")) } switch msg.Header.Get("KV-Operation") { case "DEL", "PURGE": return nil, false, nil } } if len(msg.Data) == 0 { return nil, false, nil } return msg.Data, true, nil } func (l *natsLink) Ping(ctx context.Context, service, id 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 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 fmt.Errorf("%w: asking %s: %v%s", ErrCannotAsk, subject, err, refused) } return fmt.Errorf("%w: %s did not answer: %v", ErrNoAnswer, subject, err) } var ping struct { Name string `json:"name"` ID string `json:"id"` } if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Name != service || ping.ID != id { return fmt.Errorf("%w: what answered %s is not %s %s", ErrNoAnswer, subject, service, id) } return 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 "" }