Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/presence.go
T

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
}