The provisioner's watches of gitea.bound, mesh-vault.bound and postgres.bound failed every minute for an hour on
the control-node ("the current values ... did not arrive in 10s") and 1257 changes were refused as "names no
watch of this bundle". Both sides were at fault. The SDK answered a change only once its handler returned, so a
slow handler (Rejudge waits on the lock its composition holds) counted against the runtime's 10 s bound on the
current values; and when the runtime gave up, the SDK forgot the id at once. The runtime (mesh-tools node-tools
internal/bus/state.go StateWatch) stops its watcher on that bound but still drains and delivers the values already
buffered, to an id nobody holds.
Quick fix: a longer runtime bound. Thorough fix here: each state and provision watch has a follower that takes a
change in order and answers at once, its handler on its own goroutine; Watch still returns only once the current
values were handled, and a handler's error is said on standard error. A watch the runtime refuses is said given up
and kept by id, so a change still handed to it is refused saying so. KeptState.Follow asks again with backoff until
taken, the loop four modules wrote for themselves. A log watch keeps its answer after the handler: that is its
back-pressure.
136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
package stdio
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// A watch's handler runs beside the runtime, never in its way (issue 774, the sweep of 2026-10-30).
|
|
//
|
|
// The runtime hands a watch's current values over one at a time and gives up on the watch when none was handed over
|
|
// within its bound (mesh-tools node-tools internal/bus/state.go StateWatch, 10 s). The bundle answered each change only
|
|
// once its handler returned, so a slow handler (mesh-provisioner's Rejudge waits on the lock its composition holds)
|
|
// counted against the bus's bound: the watch was given up, the SDK forgot its id, and the values the runtime still
|
|
// delivered to it were refused as "names no watch of this bundle", 1257 times in an hour on the control-node.
|
|
//
|
|
// So each watch has a follower: a change is taken into it in the order the runtime hands it over and answered at once,
|
|
// and the handler runs on the follower's own goroutine, in that order. Watch still returns only once every current
|
|
// value was handled. A handler's error is said on the bundle's standard error, naming the watch and the change, since
|
|
// the runtime no longer hears it. A log watch keeps its answer after the handler: its runtime sends the next entry only
|
|
// after the answer, which is the back-pressure a replay of a long log needs.
|
|
|
|
// follower runs one watch's handler on its own goroutine, in order.
|
|
type follower[T any] struct {
|
|
what string
|
|
handle func(T) error
|
|
describe func(T) string
|
|
mu sync.Mutex
|
|
items []followed[T]
|
|
wake chan struct{}
|
|
}
|
|
|
|
// followed is one change, or with no change a mark that closes done once everything before it was handled.
|
|
type followed[T any] struct {
|
|
change T
|
|
mark bool
|
|
done chan struct{}
|
|
}
|
|
|
|
func newFollower[T any](what string, handle func(T) error, describe func(T) string) *follower[T] {
|
|
f := &follower[T]{what: what, handle: handle, describe: describe, wake: make(chan struct{}, 1)}
|
|
go f.run()
|
|
return f
|
|
}
|
|
|
|
// take queues a change; it never waits on the handler.
|
|
func (f *follower[T]) take(change T) {
|
|
f.mu.Lock()
|
|
f.items = append(f.items, followed[T]{change: change})
|
|
f.mu.Unlock()
|
|
f.poke()
|
|
}
|
|
|
|
// handled is closed once every change taken before it was handled.
|
|
func (f *follower[T]) handled() <-chan struct{} {
|
|
done := make(chan struct{})
|
|
f.mu.Lock()
|
|
f.items = append(f.items, followed[T]{mark: true, done: done})
|
|
f.mu.Unlock()
|
|
f.poke()
|
|
return done
|
|
}
|
|
|
|
func (f *follower[T]) poke() {
|
|
select {
|
|
case f.wake <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (f *follower[T]) run() {
|
|
for range f.wake {
|
|
for {
|
|
f.mu.Lock()
|
|
if len(f.items) == 0 {
|
|
f.mu.Unlock()
|
|
break
|
|
}
|
|
next := f.items[0]
|
|
f.items = f.items[1:]
|
|
f.mu.Unlock()
|
|
if next.mark {
|
|
close(next.done)
|
|
continue
|
|
}
|
|
if err := f.handle(next.change); err != nil {
|
|
sayf("%s: the handler did not take %s: %v; the next change hands it again", f.what, f.describe(next.change), err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sayf says something the runtime cannot say for the bundle, on its standard error, which its journal keeps.
|
|
func sayf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[mesh-sdk] "+format+"\n", args...)
|
|
}
|
|
|
|
// gaveUpMost bounds what a bundle remembers of the watches it gave up.
|
|
const gaveUpMost = 256
|
|
|
|
// gaveUp keeps why a watch was given up, so a change the runtime still hands to it is refused saying so, never as a
|
|
// watch nobody asked for. Called with c.mu held.
|
|
func (c *channel) gaveUp(id, why string) {
|
|
if c.givenUp == nil || len(c.givenUp) >= gaveUpMost {
|
|
c.givenUp = map[string]string{}
|
|
}
|
|
c.givenUp[id] = why
|
|
}
|
|
|
|
// noWatch is the refusal of a change for watch id, which this bundle never asked or gave up.
|
|
func (c *channel) noWatch(kind, id string) string {
|
|
c.mu.Lock()
|
|
why, given := c.givenUp[id]
|
|
c.mu.Unlock()
|
|
if given {
|
|
sayf("%s: the runtime still hands over changes for the watch %s, which this bundle gave up: %s", kind, id, why)
|
|
return fmt.Sprintf("%s: this bundle gave the watch %s up (%s); stop handing it over", kind, id, why)
|
|
}
|
|
return kind + " names no watch of this bundle"
|
|
}
|
|
|
|
// Follow watches key of the state as Watch does and asks again until the watch is taken, saying each give-up and
|
|
// when it asks again (2 s, doubled to a minute): the loop every module wrote for itself, in one place. It returns once
|
|
// the watch is taken; call it beside the code that needs it.
|
|
func (s KeptState) Follow(key string, handle func(StateChange) error) {
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
err := s.Watch(key, handle)
|
|
if err == nil {
|
|
return
|
|
}
|
|
sayf("the state %s is not watched yet (%v); asking again in %s", s.name, err, wait)
|
|
time.Sleep(wait)
|
|
}
|
|
}
|