| index e1d1a3eb9..d42704637 100644 |
| --- a/internal/rollout/status_test.go |
| +++ b/internal/rollout/status_test.go |
| @@ -2,8 +2,12 @@ package rollout |
| |
| import ( |
| "context" |
| + "runtime" |
| + "strings" |
| + "sync" |
| "sync/atomic" |
| "testing" |
| + "time" |
| |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -217,13 +221,13 @@ func TestPassForgetsHostsThatLeftTheBox(t *testing.T) { |
| // A host whose process is starting or stopping (the activator owns it and has |
| // no current generation) is transient: no serving build and not awake, though |
| // the last pass cached one. One that is awake or asleep is not (Astra R3 |
| -// section 5 item 10). |
| +// section 5 item 10). Nothing is installed, so no attempt runs: the harness's |
| +// swap goroutines would otherwise write the activator's generations under the |
| +// test. |
| func TestStatusReportsAStartingOrStoppingHostAsTransient(t *testing.T) { |
| h := newHarness(t, "a", "b", "c") |
| - h.stage("v2") |
| - h.install("v2", "a", "b", "c") |
| h.pass() |
| - if got := h.m.Status().Hosts["a"]; got.Transient || got.Serving != "v1" || !got.Awake { |
| + if got := h.m.Status().Hosts["a"]; got.Transient || got.Installed != "v1" || got.Serving != "v1" || !got.Awake { |
| t.Fatalf("a before = %+v, want awake on v1 and not transient", got) |
| } |
| h.act.mu.Lock() |
| @@ -235,7 +239,7 @@ func TestStatusReportsAStartingOrStoppingHostAsTransient(t *testing.T) { |
| // report must not repeat it for a host the activator says is mid-change. |
| r := h.m.Status() |
| a := r.Hosts["a"] |
| - if !a.Transient || a.Serving != "" || a.Awake || a.Installed != "v2" { |
| + if !a.Transient || a.Serving != "" || a.Awake || a.Installed != "v1" { |
| t.Fatalf("a = %+v, want transient, not awake, no serving build", a) |
| } |
| if b := r.Hosts["b"]; b.Transient { |
| @@ -247,13 +251,22 @@ func TestStatusReportsAStartingOrStoppingHostAsTransient(t *testing.T) { |
| } |
| |
| // Status asks the activator with no Manager or scheduler lock held (the lock |
| -// order on Manager). |
| +// order on Manager). The probe only TryLocks, so a violation fails the test |
| +// instead of deadlocking it. No attempt runs (nothing is installed), so no |
| +// other goroutine holds these locks. |
| func TestStatusAsksTheActivatorWithNoLockHeld(t *testing.T) { |
| - h := newHarness(t, "a") |
| - h.stage("v2") |
| - h.install("v2", "a") |
| + h := newHarness(t, "a", "b") |
| h.pass() |
| - probe := &lockProbeAct{fakeAct: h.act, m: h.m, t: t} |
| + h.m.mu.Lock() |
| + scheds := make([]*scheduler, 0, len(h.m.apps)) |
| + for _, s := range h.m.apps { |
| + scheds = append(scheds, s) |
| + } |
| + h.m.mu.Unlock() |
| + if len(scheds) == 0 { |
| + t.Fatal("the pass built no scheduler") |
| + } |
| + probe := &lockProbeAct{fakeAct: h.act, m: h.m, scheds: scheds, t: t} |
| h.m.d.Act = probe |
| h.m.Status() |
| if probe.asked.Load() == 0 { |
| @@ -265,29 +278,52 @@ func TestStatusAsksTheActivatorWithNoLockHeld(t *testing.T) { |
| // Manager's or a scheduler's mutex is held. |
| type lockProbeAct struct { |
| *fakeAct |
| - m *Manager |
| - t *testing.T |
| - asked atomic.Int64 |
| + m *Manager |
| + scheds []*scheduler |
| + t *testing.T |
| + asked atomic.Int64 |
| } |
| |
| func (p *lockProbeAct) check() { |
| + if !askedByStatus() { |
| + return // the scheduler's own goroutine asks too; only Status is under test |
| + } |
| p.asked.Add(1) |
| - if !p.m.mu.TryLock() { |
| - p.t.Error("activator asked with Manager.mu held") |
| - } else { |
| - p.m.mu.Unlock() |
| + // A lock that Status holds stays held for the whole ask, while a |
| + // scheduler goroutine's own hold is brief, so a violation is a lock that |
| + // cannot be taken for a long while. TryLock, never Lock: a violation |
| + // fails the test instead of deadlocking it. |
| + free := func(mu *sync.Mutex) bool { |
| + for end := time.Now().Add(300 * time.Millisecond); time.Now().Before(end); time.Sleep(time.Millisecond) { |
| + if mu.TryLock() { |
| + mu.Unlock() |
| + return true |
| + } |
| + } |
| + return false |
| } |
| - p.m.mu.Lock() |
| - scheds := make([]*scheduler, 0, len(p.m.apps)) |
| - for _, s := range p.m.apps { |
| - scheds = append(scheds, s) |
| + if !free(&p.m.mu) { |
| + p.t.Error("activator asked with Manager.mu held") |
| } |
| - p.m.mu.Unlock() |
| - for _, s := range scheds { |
| - if !s.mu.TryLock() { |
| + for _, s := range p.scheds { |
| + if !free(&s.mu) { |
| p.t.Error("activator asked with a scheduler.mu held") |
| - } else { |
| - s.mu.Unlock() |
| + } |
| + } |
| +} |
| + |
| +// askedByStatus reports whether the calling goroutine is inside |
| +// Manager.Status. |
| +func askedByStatus() bool { |
| + pcs := make([]uintptr, 32) |
| + frames := runtime.CallersFrames(pcs[:runtime.Callers(2, pcs)]) |
| + for { |
| + f, more := frames.Next() |
| + if strings.HasSuffix(f.Function, "(*Manager).Status") { |
| + return true |
| + } |
| + if !more { |
| + return false |
| } |
| } |
| } |
| @@ -301,3 +337,43 @@ func (p *lockProbeAct) Current(host string) (activator.Gen, bool) { |
| p.check() |
| return p.fakeAct.Current(host) |
| } |
| + |
| +// A host that joins a scheduler after Status read the names was never asked of |
| +// the activator, so nothing says it is settled: that report calls it |
| +// transient, and the next one asks. |
| +func TestStatusCallsAHostThatJoinedAfterTheNamesWereReadTransient(t *testing.T) { |
| + h := newHarness(t, "a") |
| + h.pass() |
| + h.m.mu.Lock() |
| + var s *scheduler |
| + for _, s = range h.m.apps { |
| + } |
| + h.m.mu.Unlock() |
| + var once atomic.Bool |
| + h.m.d.Act = &joinProbeAct{fakeAct: h.act, onAsk: func() { |
| + if once.CompareAndSwap(false, true) { // the names are read; now a host joins |
| + s.mu.Lock() |
| + s.q.hosts["late"] = &hostSlot{wantBuild: "v1", good: "v1", serving: "v1"} |
| + s.mu.Unlock() |
| + } |
| + }} |
| + r := h.m.Status() |
| + if late := r.Hosts["late"]; !late.Transient || late.Serving != "" || late.Awake { |
| + t.Fatalf("late = %+v, want transient for the report that never asked", late) |
| + } |
| + if a := r.Hosts["a"]; a.Transient || a.Serving != "v1" { |
| + t.Fatalf("a = %+v, want awake on v1", a) |
| + } |
| +} |
| + |
| +type joinProbeAct struct { |
| + *fakeAct |
| + onAsk func() |
| +} |
| + |
| +func (p *joinProbeAct) Current(host string) (activator.Gen, bool) { |
| + if askedByStatus() { // not the scheduler's own goroutine |
| + p.onAsk() |
| + } |
| + return p.fakeAct.Current(host) |
| +} |