Convert a single-flight panic into an error

- flightGroup.Do recovers a panicking fn so the leader and every waiter get a non-nil error instead of a zero-value success served as 200 []
- Note in the README that facts_cache_bytes budgets body bytes only
This commit is contained in:
2026-09-05 20:55:09 +10:00
parent cab1d7ade0
commit 0c1fe7f1dd
3 changed files with 187 additions and 2 deletions
+20 -1
View File
@@ -3,7 +3,9 @@ package main
import (
"container/list"
"context"
"fmt"
"net/url"
"runtime/debug"
"sort"
"sync"
"time"
@@ -191,8 +193,20 @@ type flightCall struct {
err error
}
// flightPanic is a panic from a flight's fn, reported to the leader and to every
// waiter as an error so callers keep their error handling (stale fallback, 502)
// instead of seeing a zero-value success.
type flightPanic struct {
value any
stack []byte
}
func (p *flightPanic) Error() string {
return fmt.Sprintf("panic building response: %v\n%s", p.value, p.stack)
}
// Do returns fn's result and whether this caller shared another's in-flight run.
func (g *flightGroup) Do(key string, fn func() (cachedResponse, error)) (cachedResponse, error, bool) {
func (g *flightGroup) Do(key string, fn func() (cachedResponse, error)) (resp cachedResponse, err error, shared bool) {
g.mu.Lock()
if g.calls == nil {
g.calls = make(map[string]*flightCall)
@@ -208,6 +222,11 @@ func (g *flightGroup) Do(key string, fn func() (cachedResponse, error)) (cachedR
g.mu.Unlock()
defer func() {
if r := recover(); r != nil {
c.resp, c.err = cachedResponse{}, &flightPanic{value: r, stack: debug.Stack()}
resp, err = c.resp, c.err
}
// Done only after the results are stored, so waiters read them.
c.wg.Done()
g.mu.Lock()
delete(g.calls, key)