//go:build integration package bus_test import ( "errors" "fmt" "strings" "sync" "testing" "time" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // **A watch whose current values take longer than the bound to hand over still holds** (novox/hq issue // 1726). The gitea module watches the checker's owed: one key per pull request head, each a round trip // to the bundle. With a few hundred keys the hand-over outlasted StateTimeout at every attempt, the // watch was refused, and the newest keys, handed over last, never reached mesh/check: a group's passing // verdict was never written on its members' heads. The bound is on progress: a watch fails only when // no current value is handed over within it. func TestAWatchHoldsWhileItsCurrentValuesKeepArriving(t *testing.T) { was := bus.StateTimeout bus.StateTimeout = 400 * time.Millisecond t.Cleanup(func() { bus.StateTimeout = was }) conn, kv := owedWatcher(t) // A group of three pull requests judged last, after many older heads: their keys come last. const older = 30 var keys []string for i := 0; i < older; i++ { keys = append(keys, fmt.Sprintf("novox/mesh-catalog.%d.%040x.status=mesh/check", 400+i, i)) } for _, member := range []string{"novox/mesh-sdk.76.34f210e5", "novox/mesh-controller.453.c06f75d0", "novox/mesh-delivery.33.28ee22ef"} { keys = append(keys, member+strings.Repeat("0", 32)+".status=mesh/check") } for _, k := range keys { if _, err := kv.Put(k, []byte(`{"v":1,"act":"status","context":"mesh/check","state":"success"}`)); err != nil { t.Fatal(err) } } var mu sync.Mutex var current, live []string // Each value takes 40 ms to be taken: 33 values take about 1.3 s, three times the bound. stop, err := conn.StateWatch("gitea", "mesh-delivery-checker.owed", "", func(ch bus.StateChange) error { if ch.Current { time.Sleep(40 * time.Millisecond) } mu.Lock() defer mu.Unlock() if ch.Current { current = append(current, ch.Key) } else { live = append(live, ch.Key) } return nil }) if err != nil { t.Fatalf("a watch whose current values kept arriving was refused: %v", err) } t.Cleanup(stop) mu.Lock() got := append([]string(nil), current...) mu.Unlock() if len(got) != len(keys) { t.Fatalf("the watch returned after %d of %d current values", len(got), len(keys)) } for i, k := range keys { if got[i] != k { t.Fatalf("current value %d is %s, want %s", i, got[i], k) } } // And it holds: a verdict written afterwards reaches it as a change. next := "novox/mesh-sdk.77." + strings.Repeat("a", 40) + ".status=mesh/check" if _, err := kv.Put(next, []byte(`{"v":1,"act":"status","context":"mesh/check","state":"pending"}`)); err != nil { t.Fatal(err) } mt.Until(t, func() error { mu.Lock() defer mu.Unlock() if len(live) != 1 || live[0] != next { return fmt.Errorf("live changes so far: %v", live) } return nil }) } // A watch whose bundle takes nothing for longer than the bound is still refused, so a bundle that hangs // is said, not waited on for ever. func TestAWatchIsRefusedWhenNoCurrentValueIsTakenWithinTheBound(t *testing.T) { was := bus.StateTimeout bus.StateTimeout = 300 * time.Millisecond t.Cleanup(func() { bus.StateTimeout = was }) conn, kv := owedWatcher(t) for i := 0; i < 3; i++ { if _, err := kv.Put(fmt.Sprintf("novox/hq.%d.%040x.status=mesh/check", i+1, i), []byte(`{"v":1}`)); err != nil { t.Fatal(err) } } release := make(chan struct{}) t.Cleanup(func() { close(release) }) began := time.Now() _, err := conn.StateWatch("gitea", "mesh-delivery-checker.owed", "", func(bus.StateChange) error { <-release return nil }) if err == nil || !strings.Contains(err.Error(), "did not arrive in") { t.Fatalf("a watch whose first value was never taken: want refused, got %v", err) } if took := time.Since(began); took > 3*time.Second { t.Fatalf("the refusal took %s, far past the bound", took) } } // owedWatcher is the gitea module's view of the checker's owed: the bucket, and a runtime connection // that serves gitea with the owed issued for reading. func owedWatcher(t *testing.T) (*bus.Conn, interface { Put(string, []byte) (uint64, error) }) { t.Helper() mesh := mt.New(t) kv := mesh.Bucket(t, "mesh-delivery-checker_owed") gitea := mt.MembershipOf("gitea", "anchor", false, nil) gitea.State = []bus.StateIssued{{Name: "mesh-delivery-checker.owed", Bucket: "mesh-delivery-checker_owed"}} mesh.Issue(t, gitea) conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"}) if err != nil { t.Fatal(err) } conn.Logf = func(string, ...any) {} t.Cleanup(conn.Close) conn.Follow("gitea") mt.Until(t, func() error { if conn.Membership("gitea") == nil { return errors.New("gitea not issued yet") } return nil }) return conn, kv }