From b159efd65673c5c8a8c609f9b4534fb304b8e671 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Fri, 16 Oct 2026 23:26:01 +0000 Subject: [PATCH] Keep gitea's forge-account provider running when a watch is refused, so a later grant is picked up (issue 955) --- .../gitea/cmd/forge-account-provider/main.go | 4 +- .../cmd/forge-account-provider/service.go | 38 ++++++++++ .../forge-account-provider/service_test.go | 69 +++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/modules/gitea/cmd/forge-account-provider/main.go b/modules/gitea/cmd/forge-account-provider/main.go index 1f0148a8..aa4b7c56 100644 --- a/modules/gitea/cmd/forge-account-provider/main.go +++ b/modules/gitea/cmd/forge-account-provider/main.go @@ -115,9 +115,7 @@ func main() { pass() }() go func() { - if err := p.Run(ctx); err != nil { - say("the provider stopped: %v", err) - } + svc.KeepRunning(ctx, p.Run) }() if err := stdio.Serve("", nil); err != nil { say("%v", err) diff --git a/modules/gitea/cmd/forge-account-provider/service.go b/modules/gitea/cmd/forge-account-provider/service.go index b6f391ad..11716bf3 100644 --- a/modules/gitea/cmd/forge-account-provider/service.go +++ b/modules/gitea/cmd/forge-account-provider/service.go @@ -407,3 +407,41 @@ func (b *valuesBus) Delete(ctx context.Context, bucket, key string) error { b.mu.Unlock() return b.Bus.Delete(ctx, bucket, key) } + +// Backoff of KeepRunning: the first pause and the longest. +const ( + restartFirst = 5 * time.Second + restartLast = 2 * time.Minute +) + +// KeepRunning runs the SDK provider and starts it again whenever it ends, however it ended (a watch or a read the +// runtime refuses, because a grant has not reached it yet, ends Run): a provider stopped for good answers no consumer +// until something restarts the process, and nothing does when its bundle is unchanged (issue 955). It says so once +// per spell of the same error, pauses 5 s doubling to 2 min between starts, and returns only when ctx ends. +func (s *Service) KeepRunning(ctx context.Context, run func(context.Context) error) { + pause := restartFirst + lastSaid := "" + for ctx.Err() == nil { + started := time.Now() + err := run(ctx) + if ctx.Err() != nil { + return + } + if time.Since(started) >= restartLast { + // It served a while: a new spell. + pause, lastSaid = restartFirst, "" + } + why := "it returned without an error" + if err != nil { + why = err.Error() + } + if why != lastSaid { + s.cfg.Log("the provider stopped: %s; starting it again every %s up to %s until it stays up", why, pause, restartLast) + lastSaid = why + } + s.Pause(pause) + if pause *= 2; pause > restartLast { + pause = restartLast + } + } +} diff --git a/modules/gitea/cmd/forge-account-provider/service_test.go b/modules/gitea/cmd/forge-account-provider/service_test.go index ebc52291..d8203833 100644 --- a/modules/gitea/cmd/forge-account-provider/service_test.go +++ b/modules/gitea/cmd/forge-account-provider/service_test.go @@ -471,3 +471,72 @@ func TestAPassBeforeTheKeysAreLoadedRemovesAndDeletesNothing(t *testing.T) { t.Fatalf("keys = %s", got) } } + +// Issue 955: a watch the runtime refuses (a grant that comes later) must not end the provider for good. +func TestKeepRunningStartsTheProviderAgainAfterARefusedWatch(t *testing.T) { + r := newRig(t) + var pauses []time.Duration + var said []string + r.svc.Pause = func(d time.Duration) { pauses = append(pauses, d) } + r.svc.cfg.Log = func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) } + ctx, stop := context.WithCancel(context.Background()) + defer stop() + calls := 0 + serving := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + r.svc.KeepRunning(ctx, func(ctx context.Context) error { + calls++ + if calls <= 2 { + return errors.New(`watching mesh-controller_assignments: gitea reads no state called "mesh-controller.assignments"`) + } + close(serving) + <-ctx.Done() + return ctx.Err() + }) + }() + select { + case <-serving: + case <-time.After(5 * time.Second): + t.Fatal("the provider was not started again after its watch was refused") + } + stop() + <-done + if calls != 3 { + t.Fatalf("run %d times", calls) + } + if len(pauses) != 2 || pauses[0] != 5*time.Second || pauses[1] != 10*time.Second { + t.Fatalf("the backoff is %v", pauses) + } + if len(said) == 0 || !strings.Contains(said[0], "mesh-controller.assignments") { + t.Fatalf("a refusal is said loudly: %v", said) + } + loud := 0 + for _, s := range said { + if strings.Contains(s, "stopped") { + loud++ + } + } + if loud != 1 { + t.Fatalf("the same refusal is said %d times in one spell: %v", loud, said) + } +} + +func TestKeepRunningBacksOffToTwoMinutes(t *testing.T) { + r := newRig(t) + var pauses []time.Duration + r.svc.Pause = func(d time.Duration) { pauses = append(pauses, d) } + ctx, stop := context.WithCancel(context.Background()) + calls := 0 + r.svc.KeepRunning(ctx, func(context.Context) error { + if calls++; calls == 9 { + stop() + } + return errors.New("refused") + }) + last := pauses[len(pauses)-1] + if pauses[0] != 5*time.Second || last != 2*time.Minute { + t.Fatalf("the backoff is %v", pauses) + } +}