438c1d6df3
Slice B, first half. The F18 ring is bounded, lossy and local, so the debt ledger reported it as a gap about itself. Two events make it durable: ObservationIncidentOpened at first sight, appended immediately so a coordinator that dies mid-incident still leaves the fact that it existed, and ObservationIncidentClosed carrying the aggregate. The rules are what matter. Repeats update the aggregate and append nothing, because run 11's 409 loop was one incident with an intensity of 301 rather than 301 pieces of evidence. Absence from the ring closes nothing, since a bounded history evicts as easily as it recovers. An incident is scoped to its lease and closes on lease end, epoch change, worker restart, or, for observations with no lease to bound them, on last_seen going stale. A ring entry that is evicted and recreated accumulates: 34 then 3 is 37. Also collapses three drifted copies of the list of events that carry no task into one predicate. Adding a type to two of them left it rejected by the third, which is how the first version of this failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
210 lines
7.1 KiB
Go
210 lines
7.1 KiB
Go
package operations
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"orchestra/internal/domain"
|
|
"orchestra/internal/store"
|
|
)
|
|
|
|
func tracker(t *testing.T) (*ObservationTracker, *store.Store) {
|
|
t.Helper()
|
|
s, err := store.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &ObservationTracker{Store: s}, s
|
|
}
|
|
|
|
func ring(message string, count int, last time.Time) []domain.WorkerObservation {
|
|
return []domain.WorkerObservation{{Message: message, Count: count, First: last.Add(-time.Minute), Last: last}}
|
|
}
|
|
|
|
func closedIncident(t *testing.T, s *store.Store) domain.ObservationIncident {
|
|
t.Helper()
|
|
var out domain.ObservationIncident
|
|
found := 0
|
|
for _, e := range s.Events(0) {
|
|
if e.Type != domain.EventObservationIncidentClosed {
|
|
continue
|
|
}
|
|
found++
|
|
if err := json.Unmarshal(e.Payload, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if found != 1 {
|
|
t.Fatalf("closed incidents = %d, want 1", found)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 301 repeats of one refusal are one incident with an intensity of 301, not
|
|
// 301 pieces of evidence. Appending each would spam the log and make one stuck
|
|
// loop look like chronic, recurring debt.
|
|
func TestRepeatsAreOneIncident(t *testing.T) {
|
|
tr, s := tracker(t)
|
|
at := time.Unix(1700000000, 0).UTC()
|
|
report := func(count int, when time.Time) {
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "workpc-claude", Incarnation: "boot-1", TaskID: "task-a", LeaseEpoch: "epoch-1",
|
|
Observations: ring("release task-a commit: 409 superseded", count, when), At: when,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
report(1, at)
|
|
report(40, at.Add(time.Minute))
|
|
report(301, at.Add(2*time.Minute))
|
|
|
|
opened := 0
|
|
for _, e := range s.Events(0) {
|
|
if e.Type == domain.EventObservationIncidentOpened {
|
|
opened++
|
|
}
|
|
if e.Type == domain.EventObservationIncidentClosed {
|
|
t.Fatal("an incident was closed while its lease was still running")
|
|
}
|
|
}
|
|
if opened != 1 {
|
|
t.Fatalf("opened %d incidents for one repeating failure", opened)
|
|
}
|
|
if open := s.OpenObservations(); len(open) != 1 || open[0].TaskID != "task-a" {
|
|
t.Fatalf("open incidents = %+v", open)
|
|
}
|
|
}
|
|
|
|
// The ring is a bounded history, so an entry that disappears may have been
|
|
// evicted rather than resolved. Absence must not close anything, and a
|
|
// recreated entry must accumulate rather than restart its count.
|
|
func TestEvictionNeitherClosesNorRestartsTheCount(t *testing.T) {
|
|
tr, s := tracker(t)
|
|
at := time.Unix(1700000000, 0).UTC()
|
|
send := func(obs []domain.WorkerObservation, when time.Time) {
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "w", Incarnation: "boot-1", TaskID: "task-a", LeaseEpoch: "epoch-1",
|
|
Observations: obs, At: when,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
send(ring("lease task-a not renewed: agent idle", 34, at), at)
|
|
// Evicted: the message is simply gone from this heartbeat.
|
|
send(nil, at.Add(time.Minute))
|
|
if len(s.OpenObservations()) != 1 {
|
|
t.Fatal("an incident was closed because its message left a bounded ring")
|
|
}
|
|
// Recreated, counting from scratch on the worker side.
|
|
send(ring("lease task-a not renewed: agent idle", 3, at.Add(2*time.Minute)), at.Add(2*time.Minute))
|
|
|
|
// The lease ends, which is a real boundary.
|
|
if _, err := tr.Ingest(WorkerReport{WorkerID: "w", Incarnation: "boot-1", At: at.Add(3 * time.Minute)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inc := closedIncident(t, s)
|
|
if inc.RepeatCount != 37 {
|
|
t.Fatalf("repeat_count = %d, want 37 (34 before eviction plus 3 after)", inc.RepeatCount)
|
|
}
|
|
if inc.CloseReason != domain.ObservationCloseEpochChange {
|
|
t.Fatalf("close_reason = %q", inc.CloseReason)
|
|
}
|
|
if !inc.LastSeen.Equal(at.Add(2 * time.Minute)) {
|
|
t.Fatalf("last_seen = %s, want the last actual occurrence", inc.LastSeen)
|
|
}
|
|
if !inc.ClosedAt.After(inc.LastSeen) {
|
|
t.Fatal("closed_at must be when Orchestra finalized it, not when the failure last happened")
|
|
}
|
|
}
|
|
|
|
// Recurrence is independent incidents. The same signature on two tasks is two,
|
|
// which is the evidence that means something; repeats inside one are intensity.
|
|
func TestTheSameSignatureOnAnotherTaskIsASecondIncident(t *testing.T) {
|
|
tr, s := tracker(t)
|
|
at := time.Unix(1700000000, 0).UTC()
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "w", Incarnation: "boot-1", TaskID: "task-a", LeaseEpoch: "e1",
|
|
Observations: ring("lease task-a not renewed: agent idle", 5, at), At: at,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "w", Incarnation: "boot-1", TaskID: "task-b", LeaseEpoch: "e2",
|
|
Observations: ring("lease task-b not renewed: agent idle", 2, at.Add(time.Minute)), At: at.Add(time.Minute),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opened, closed := 0, 0
|
|
for _, e := range s.Events(0) {
|
|
switch e.Type {
|
|
case domain.EventObservationIncidentOpened:
|
|
opened++
|
|
case domain.EventObservationIncidentClosed:
|
|
closed++
|
|
}
|
|
}
|
|
if opened != 2 {
|
|
t.Fatalf("opened = %d, want one incident per lease", opened)
|
|
}
|
|
if closed != 1 {
|
|
t.Fatalf("closed = %d, want the first lease finalized when the second began", closed)
|
|
}
|
|
}
|
|
|
|
// A restart cannot continue the previous process's symptom.
|
|
func TestAWorkerRestartClosesItsIncidents(t *testing.T) {
|
|
tr, s := tracker(t)
|
|
at := time.Unix(1700000000, 0).UTC()
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "w", Incarnation: "boot-1", TaskID: "task-a", LeaseEpoch: "e1",
|
|
Observations: ring("herdr unreachable", 9, at), At: at,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tr.Ingest(WorkerReport{WorkerID: "w", Incarnation: "boot-2", At: at.Add(time.Minute)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inc := closedIncident(t, s)
|
|
if inc.CloseReason != domain.ObservationCloseWorkerRestart || inc.RepeatCount != 9 {
|
|
t.Fatalf("incident = %+v", inc)
|
|
}
|
|
}
|
|
|
|
// An observation with no lease has no terminal boundary, so staleness of its
|
|
// last actual occurrence is what ends it.
|
|
func TestAWorkerLevelIncidentClosesOnQuietTimeout(t *testing.T) {
|
|
tr, s := tracker(t)
|
|
at := time.Unix(1700000000, 0).UTC()
|
|
if _, err := tr.Ingest(WorkerReport{
|
|
WorkerID: "w", Incarnation: "boot-1",
|
|
Observations: ring("heartbeat: connection refused", 4, at), At: at,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tr.Ingest(WorkerReport{WorkerID: "w", Incarnation: "boot-1", At: at.Add(time.Minute)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(s.OpenObservations()) != 1 {
|
|
t.Fatal("a worker-level incident closed before its quiet timeout")
|
|
}
|
|
if _, err := tr.Ingest(WorkerReport{WorkerID: "w", Incarnation: "boot-1", At: at.Add(QuietTimeout + time.Minute)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inc := closedIncident(t, s); inc.CloseReason != domain.ObservationCloseQuietTimeout {
|
|
t.Fatalf("close_reason = %q", inc.CloseReason)
|
|
}
|
|
}
|
|
|
|
// The signature is what makes recurrence countable across tasks.
|
|
func TestSignatureCollapsesIdsAndCounts(t *testing.T) {
|
|
a := domain.ObservationSignature("lease 06G4WJ9T4F35NZC4Z8QQXM9Z6G not renewed: agent status idle and pane unchanged")
|
|
b := domain.ObservationSignature("lease 06G4VF5HZW7Q4JBM3TTY7W1Y64 not renewed: agent status idle and pane unchanged")
|
|
if a != b {
|
|
t.Fatalf("the same failure on two tasks has two signatures:\n%s\n%s", a, b)
|
|
}
|
|
if c := domain.ObservationSignature("release 06G4WJ9T4F35NZC4Z8QQXM9Z6G commit: 409 superseded"); c == a {
|
|
t.Fatal("two different failures collapsed to one signature")
|
|
}
|
|
}
|