186 lines
5.3 KiB
Go
186 lines
5.3 KiB
Go
package watch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// partPrefix is what the watchman keeps presence of among the module users: the parts of delivery.
|
|
const partPrefix = "mesh-delivery-"
|
|
|
|
// presenceOf is the presence key of a bus user, and what a wait for its return is called: `node.<node>` is the
|
|
// node-engine of that node, `<node>.<module>` a part's instance for a module of delivery, and any other user has none.
|
|
func presenceOf(user string) (key, waited string, ok bool) {
|
|
first, rest, found := strings.Cut(user, ".")
|
|
if !found || first == "" || rest == "" {
|
|
return "", "", false
|
|
}
|
|
if first == "node" {
|
|
k, err := delivery.PresenceNodeKey(rest)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
return k, rest + " to come back online", true
|
|
}
|
|
if !strings.HasPrefix(rest, partPrefix) {
|
|
return "", "", false
|
|
}
|
|
k, err := delivery.PresencePartKey(rest, first)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
return k, rest + " on " + first + " to come back online", true
|
|
}
|
|
|
|
// waitedOf is the words a presence key's return is waited in.
|
|
func waitedOf(key string) string {
|
|
node, part, instance, err := delivery.ParsePresenceKey(key)
|
|
switch {
|
|
case err != nil:
|
|
return key + " to come back online"
|
|
case part != "":
|
|
return part + " on " + instance + " to come back online"
|
|
default:
|
|
return node + " to come back online"
|
|
}
|
|
}
|
|
|
|
// Connect is a connection of the user opened.
|
|
func (w *Watchman) Connect(user string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if key, _, ok := presenceOf(user); ok {
|
|
w.counts[key]++
|
|
w.settle(key)
|
|
}
|
|
}
|
|
|
|
// Disconnect is a connection of the user closed; the user is offline when none is left.
|
|
func (w *Watchman) Disconnect(user string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if key, _, ok := presenceOf(user); ok {
|
|
if w.counts[key] > 0 {
|
|
w.counts[key]--
|
|
}
|
|
w.settle(key)
|
|
}
|
|
}
|
|
|
|
// settle makes the presence of key what its connections say, writing only when that differs from what it says now.
|
|
func (w *Watchman) settle(key string) {
|
|
online := w.counts[key] > 0
|
|
cur, known := w.presence[key]
|
|
if known && cur.online == online {
|
|
if !online {
|
|
w.ensureOffline(key)
|
|
}
|
|
return
|
|
}
|
|
if err := w.out.PutPresence(key, online); err != nil {
|
|
w.say("cannot write presence %s: %v", key, err)
|
|
return
|
|
}
|
|
w.presence[key] = presence{online: online, since: w.now()}
|
|
if online {
|
|
w.moved(delivery.PresenceBucket, key)
|
|
return
|
|
}
|
|
w.ensureOffline(key)
|
|
}
|
|
|
|
// ensureOffline mirrors the return of a node or part that is offline.
|
|
func (w *Watchman) ensureOffline(key string) {
|
|
p := w.presence[key]
|
|
w.mirror(delivery.PresenceBucket, key, waitedOf(key), "", p.since, 0)
|
|
}
|
|
|
|
// SeedPresence tells the watchman a presence key as it stands in its bucket at start, with the time it was written.
|
|
func (w *Watchman) SeedPresence(key string, online bool, at time.Time) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.presence[key] = presence{online: online, since: at}
|
|
}
|
|
|
|
// Connections replaces what the watchman knows of the open connections with the users listed: every user listed is
|
|
// online, every presence it knew online and that is not listed is offline now.
|
|
func (w *Watchman) Connections(users []string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
counts := map[string]int{}
|
|
for _, u := range users {
|
|
if key, _, ok := presenceOf(u); ok {
|
|
counts[key]++
|
|
}
|
|
}
|
|
w.counts = counts
|
|
keys := map[string]bool{}
|
|
for k := range counts {
|
|
keys[k] = true
|
|
}
|
|
for k := range w.presence {
|
|
keys[k] = true
|
|
}
|
|
for k := range keys {
|
|
w.settle(k)
|
|
}
|
|
}
|
|
|
|
// ParseEvent is the user of a connect or disconnect event of the bus (the server's ConnectEventMsg and
|
|
// DisconnectEventMsg: client.user). An event that names no user gives an empty one.
|
|
func ParseEvent(data []byte) (user string, err error) {
|
|
var ev struct {
|
|
Client struct {
|
|
User string `json:"user"`
|
|
} `json:"client"`
|
|
}
|
|
if err := json.Unmarshal(data, &ev); err != nil {
|
|
return "", fmt.Errorf("the bus event is not JSON: %w", err)
|
|
}
|
|
return ev.Client.User, nil
|
|
}
|
|
|
|
// ConnzRequest is the body of the one request of the open connections: without auth the server leaves the user names
|
|
// out (nats-server monitor.go ConnzOptions.Username).
|
|
func ConnzRequest() []byte { return []byte(`{"auth":true}`) }
|
|
|
|
// Rebuild asks for the open connections once, with ask, and takes presence from the answer. The caller makes it at
|
|
// start and at each reconnect.
|
|
func (w *Watchman) Rebuild(ask func(body []byte) ([]byte, error)) error {
|
|
reply, err := ask(ConnzRequest())
|
|
if err != nil {
|
|
return fmt.Errorf("the open connections cannot be asked: %w", err)
|
|
}
|
|
var answer struct {
|
|
Data struct {
|
|
Connections []struct {
|
|
User string `json:"authorized_user"`
|
|
} `json:"connections"`
|
|
Total int `json:"total"`
|
|
} `json:"data"`
|
|
Error *struct {
|
|
Description string `json:"description"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(reply, &answer); err != nil {
|
|
return fmt.Errorf("the answer about the open connections is not JSON: %w", err)
|
|
}
|
|
if answer.Error != nil {
|
|
return errors.New("the bus refused the open connections: " + answer.Error.Description)
|
|
}
|
|
users := make([]string, 0, len(answer.Data.Connections))
|
|
for _, c := range answer.Data.Connections {
|
|
users = append(users, c.User)
|
|
}
|
|
if answer.Data.Total > len(users) {
|
|
w.say("the bus lists %d of %d connections", len(users), answer.Data.Total)
|
|
}
|
|
w.Connections(users)
|
|
return nil
|
|
}
|