package hostexec import ( "context" "encoding/json" "errors" "github.com/carlosframework/platform/internal/release" "os" "testing" "time" ) func TestPlan14Fix2RenewReplyLostSettlesAfterExpiry(t *testing.T) { b, _, remote, clock, q, p := brokerFixture(t) out, err := b.Execute(t.Context(), p, q) if err != nil { t.Fatal(err) } r, err := b.read(q.Host) if err != nil { t.Fatal(err) } key, _ := Key(q.Host, r.Authority.Box) r.Expires = r.Expires.Add(Renewal) raw, _ := json.Marshal(Record{Op: q.Op, Expires: r.Expires}) if _, err := remote.Store.Put(t.Context(), key, raw, r.LeaseETag); err != nil { t.Fatal(err) } r.Status = "pending" if err := b.write(r); err != nil { t.Fatal(err) } q.Operation, q.Token, q.ID = "executionSettle", out.Token, "settle-lost-response" if _, err := b.Execute(t.Context(), p, q); err == nil { t.Fatal("unexpired unknown ETag deleted") } clock.advance(TTL + Renewal + time.Second) if _, err := b.Execute(t.Context(), p, q); err != nil { t.Fatal(err) } if _, _, err := remote.Get(t.Context(), key); !errors.Is(err, os.ErrNotExist) { t.Fatal("remote lease retained", err) } } func TestPlan14Fix2TransientRenewalRetries(t *testing.T) { for _, kind := range []string{"authority", "put"} { t.Run(kind, func(t *testing.T) { b, _, remote, _, q, p := brokerFixture(t) out, err := b.Execute(t.Context(), p, q) if err != nil { t.Fatal(err) } q.Operation, q.Token, q.ID = "executionRenew", out.Token, "renew" calls := 0 orig := b.cfg.Authority if kind == "authority" { b.cfg.Authority = func(ctx context.Context, q ExecutionRequest, p ControllerProcess) (ExecutionAuthority, error) { calls++ if calls == 1 { return ExecutionAuthority{}, errors.New("transient transport") } return orig(ctx, q, p) } } else { remote.failAt = 2 } renewed, err := b.Execute(t.Context(), p, q) if err != nil { t.Fatal(err) } if renewed.Status != "active" || renewed.Token == out.Token { t.Fatal("renewal not committed", renewed) } }) } } func TestPlan14Fix2LostMonitorBacksOff(t *testing.T) { b, d, _, clock, q, p := brokerFixture(t) if _, err := b.Execute(t.Context(), p, q); err != nil { t.Fatal(err) } r, err := b.read(q.Host) if err != nil { t.Fatal(err) } if err := b.deny(t.Context(), r, nil); err != nil { t.Fatal(err) } attempts := 0 b.cfg.Settle = func(context.Context, ExecutionRecord) error { attempts++; return release.ErrCASConflict } _ = b.Monitor(t.Context()) writes := d.writes for i := 0; i < 9; i++ { clock.advance(100 * time.Millisecond) _ = b.Monitor(t.Context()) } if attempts != 1 || d.writes != writes { t.Fatal("lost record hot-loop", attempts, d.writes, writes) } clock.advance(101 * time.Millisecond) _ = b.Monitor(t.Context()) if attempts != 2 { t.Fatal("backoff never retried", attempts) } }