Keep what discovery heard when a round hears nothing, and fetch only changed announcements (issue 554)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
A round in which no PING reached the laptop's console within the 750 ms window was taken as the mesh's answer that nothing is there, and replaced the index: every seat and module call was refused for five seconds at a time. Discovery now waits up to its patience for a first PING, a round that heard nothing is not kept, a runtime not heard this round is resolved from what it announced last (said with its age), and a round that misses runtimes is logged once. A PING carries a digest of what the instance serves, so an unchanged announcement is not pulled again every round (some 880 kB to the laptop).
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
package announce
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -35,6 +37,12 @@ var Window = 750 * time.Millisecond
|
||||
// serves is waited for until it arrives or this passes, and a service that never sends it is named.
|
||||
var Patience = 5 * time.Second
|
||||
|
||||
// Digest is the PING answer's metadata key for a digest of what the instance serves now: a console
|
||||
// that holds the answer of that digest does not ask for it again (novox/hq issue 554). Every round
|
||||
// pulled every runtime's whole announcement — some 880 kB, every five seconds while a session works —
|
||||
// to the laptop, and the hundred-byte PING answers queued behind it.
|
||||
const Digest = "digest"
|
||||
|
||||
// Shortened is the service metadata key a runtime sets when its announcement was cut to fit the bus.
|
||||
const Shortened = "shortened"
|
||||
|
||||
@@ -157,7 +165,14 @@ func Serve(conn *bus.Conn, s Service, current func() []Endpoint) (func(), error)
|
||||
var v any
|
||||
switch parts[1] {
|
||||
case "PING":
|
||||
v = micro.Ping{ServiceIdentity: s.identity(), Type: micro.PingResponseType}
|
||||
id := s.identity()
|
||||
md := map[string]string{}
|
||||
for k, v := range id.Metadata {
|
||||
md[k] = v
|
||||
}
|
||||
md[Digest] = DigestOf(Info(s, current()))
|
||||
id.Metadata = md
|
||||
v = micro.Ping{ServiceIdentity: id, Type: micro.PingResponseType}
|
||||
case "INFO":
|
||||
v = Info(s, current())
|
||||
case "STATS":
|
||||
@@ -205,6 +220,16 @@ func Serve(conn *bus.Conn, s Service, current func() []Endpoint) (func(), error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DigestOf is a digest of an info_response: equal for equal announcements, whatever the moment.
|
||||
func DigestOf(i micro.Info) string {
|
||||
body, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:12])
|
||||
}
|
||||
|
||||
// shorten is an info_response cut to fit the bus: descriptions to their first line, then none, the
|
||||
// service's metadata saying which (so discovery can tell a person what it was given).
|
||||
func shorten(logf func(string, ...any), info micro.Info, body []byte, limit int64) []byte {
|
||||
@@ -244,6 +269,11 @@ type Heard struct {
|
||||
Took time.Duration
|
||||
Bytes int
|
||||
Shortened string // why its descriptions are not whole, or ""
|
||||
// Digest is the digest its PING gave for this answer, or "" when it gave none.
|
||||
Digest string
|
||||
// Reused is true when the instance's PING said it serves what the console already held, so its
|
||||
// announcement was not asked for again; Took is then how long its PING took.
|
||||
Reused bool
|
||||
}
|
||||
|
||||
// Machine is the machine an instance runs on: its metadata's, or the one its endpoints name.
|
||||
@@ -273,12 +303,24 @@ type Discovery struct {
|
||||
Waited time.Duration
|
||||
Heard []Heard
|
||||
Silent []Instance
|
||||
// Pinged is how many PING answers arrived.
|
||||
Pinged int
|
||||
}
|
||||
|
||||
// Gather asks every service on the bus who it is and what it serves, and waits at least the window
|
||||
// and at most the patience: until every instance that answered PING has answered INFO. An answer
|
||||
// Empty is a round in which nothing arrived at all, not even a PING: no answer about the mesh, but a
|
||||
// round that failed (novox/hq issue 554).
|
||||
func (d Discovery) Empty() bool { return d.Pinged == 0 && len(d.Heard) == 0 }
|
||||
|
||||
// Known is the announcement a console already holds for an instance at this digest, if any.
|
||||
type Known func(name, id, digest string) (Heard, bool)
|
||||
|
||||
// Gather asks every service on the bus who it is (`$SRV.PING`), and each instance that answers what it
|
||||
// serves (`$SRV.INFO.<name>.<id>`) unless its PING's digest is one `known` holds; it waits at least the
|
||||
// window and at most the patience: until at least one instance answered PING and every instance that
|
||||
// did has said what it serves. A round in which no PING arrives within the window is not taken as the
|
||||
// mesh's answer that nothing is there (novox/hq issue 554): it waits on, up to the patience. An answer
|
||||
// that is not the protocol's is skipped.
|
||||
func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
func Gather(conn *bus.Conn, known Known) (Discovery, error) {
|
||||
start := time.Now()
|
||||
d := Discovery{At: start.UTC()}
|
||||
pings, stopPings, err := conn.Collect("$SRV.PING", nil)
|
||||
@@ -286,7 +328,7 @@ func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
return d, err
|
||||
}
|
||||
defer stopPings()
|
||||
infos, stopInfos, err := conn.Collect("$SRV.INFO", nil)
|
||||
infos, askInfo, stopInfos, err := conn.Collector("$SRV.INFO")
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
@@ -296,8 +338,14 @@ func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
pinged := map[key]int{}
|
||||
answered := map[key]int{}
|
||||
where := map[key]string{}
|
||||
digest := map[key]string{}
|
||||
asked := map[key]bool{}
|
||||
reused := map[key]int{} // index in d.Heard of an answer held already
|
||||
var order []key
|
||||
complete := func() bool {
|
||||
if len(pinged) == 0 {
|
||||
return false // nobody said they are there: not an answer, a round that has not arrived
|
||||
}
|
||||
for k, n := range pinged {
|
||||
if answered[k] < n {
|
||||
return false
|
||||
@@ -305,6 +353,15 @@ func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
}
|
||||
return true
|
||||
}
|
||||
ask := func(k key) {
|
||||
if asked[k] {
|
||||
return
|
||||
}
|
||||
asked[k] = true
|
||||
if err := askInfo("$SRV.INFO."+k.name+"."+k.id, nil); err != nil {
|
||||
conn.Logf("[mesh-tools] discovery could not ask %s/%s what it serves: %v", k.name, k.id, err)
|
||||
}
|
||||
}
|
||||
least := time.NewTimer(Window)
|
||||
defer least.Stop()
|
||||
most := time.NewTimer(Patience)
|
||||
@@ -317,21 +374,49 @@ func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
if json.Unmarshal(a.Data, &p) != nil || p.Type != micro.PingResponseType {
|
||||
continue
|
||||
}
|
||||
d.Pinged++
|
||||
k := key{p.Name, p.ID}
|
||||
if _, seen := pinged[k]; !seen {
|
||||
_, seen := pinged[k]
|
||||
if !seen {
|
||||
order = append(order, k)
|
||||
}
|
||||
pinged[k]++
|
||||
if where[k] == "" {
|
||||
where[k] = p.Metadata["node"]
|
||||
}
|
||||
if seen {
|
||||
// Two instances under one name and id: what each serves is asked of both, never assumed.
|
||||
if i, ok := reused[k]; ok {
|
||||
d.Heard = append(d.Heard[:i], d.Heard[i+1:]...)
|
||||
for kk, j := range reused {
|
||||
if j > i {
|
||||
reused[kk] = j - 1
|
||||
}
|
||||
}
|
||||
delete(reused, k)
|
||||
answered[k]--
|
||||
}
|
||||
ask(k)
|
||||
continue
|
||||
}
|
||||
digest[k] = p.Metadata[Digest]
|
||||
if h, ok := heldAt(known, k.name, k.id, digest[k]); ok {
|
||||
h.Took, h.Reused, h.Digest = a.At.Sub(start), true, digest[k]
|
||||
reused[k] = len(d.Heard)
|
||||
d.Heard = append(d.Heard, h)
|
||||
answered[k]++
|
||||
break
|
||||
}
|
||||
ask(k)
|
||||
case a := <-infos:
|
||||
var i micro.Info
|
||||
if json.Unmarshal(a.Data, &i) != nil || i.Type != micro.InfoResponseType {
|
||||
continue
|
||||
}
|
||||
answered[key{i.Name, i.ID}]++
|
||||
d.Heard = append(d.Heard, Heard{Info: i, Took: a.At.Sub(start), Bytes: len(a.Data), Shortened: i.Metadata[Shortened]})
|
||||
k := key{i.Name, i.ID}
|
||||
answered[k]++
|
||||
d.Heard = append(d.Heard, Heard{Info: i, Took: a.At.Sub(start), Bytes: len(a.Data), Shortened: i.Metadata[Shortened],
|
||||
Digest: digest[k]})
|
||||
case <-least.C:
|
||||
windowOver = true
|
||||
case <-most.C:
|
||||
@@ -350,6 +435,14 @@ func Gather(conn *bus.Conn) (Discovery, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// heldAt is the answer `known` holds for an instance at this digest; never for a PING without one.
|
||||
func heldAt(known Known, name, id, digest string) (Heard, bool) {
|
||||
if known == nil || digest == "" {
|
||||
return Heard{}, false
|
||||
}
|
||||
return known(name, id, digest)
|
||||
}
|
||||
|
||||
// Endpoints reads an info_response's endpoints back into what they announce.
|
||||
func Endpoints(i micro.Info) []Endpoint {
|
||||
var out []Endpoint
|
||||
|
||||
Reference in New Issue
Block a user