fix(lifecycle): preserve mixed shutdown error identity

This commit is contained in:
end committed 2026-10-27 12:04:13 -07:00
1 parent b9a5fa8bd8
commit 64c7e6b65d
3 files changed
+91 -28

No files matched your search

+16 -26
View File
@@ -131,44 +131,32 @@ func (a *App) stopLocked() {
a.notifyLocked()
}
// remaining removes only cancellation leaves caused by established shutdown,
// preserving independent components even inside a wrapped errors.Join.
func remaining(err, cancellation error) (error, bool) {
// cancellationOnly recognizes expected cancellation without rebuilding error trees.
func cancellationOnly(err, cancellation error) bool {
if err == nil {
return nil, false
return false
}
if err == cancellation {
return nil, true
return true
}
if many, ok := err.(interface{ Unwrap() []error }); ok {
var kept []error
changed := false
for _, child := range many.Unwrap() {
e, removed := remaining(child, cancellation)
changed = changed || removed
if e != nil {
kept = append(kept, e)
children := many.Unwrap()
for _, child := range children {
if !cancellationOnly(child, cancellation) {
return false
}
}
if changed {
return errors.Join(kept...), true
}
return len(children) > 0
}
if one, ok := err.(interface{ Unwrap() error }); ok {
kept, changed := remaining(one.Unwrap(), cancellation)
if changed {
if kept == nil {
return nil, true
}
return fmt.Errorf("%s: %w", err.Error(), kept), true
}
return cancellationOnly(one.Unwrap(), cancellation)
}
return err, false
return false
}
func (a *App) recordLocked(ctx context.Context, phase, name string, err error) {
if a.stopping && context.Cause(ctx) == a.shutdownCause {
err, _ = remaining(err, ctx.Err())
if a.stopping && context.Cause(ctx) == a.shutdownCause && cancellationOnly(err, ctx.Err()) {
err = nil
}
if err != nil {
a.errs = append(a.errs, fmt.Errorf("%s %q: %w", phase, name, err))
@@ -196,7 +184,9 @@ func (a *App) launchLocked(ctx context.Context, phase, name string, fn func(cont
// In-flight setup is awaited within that budget before rollback; abandoned setup
// is never torn down later. Deadline or second signal abandons outstanding work.
// Observed failures are joined; expected callback cancellation during established
// shutdown is omitted, while initiating failures and every teardown error remain.
// shutdown is omitted only for cancellation-only error trees. Mixed errors remain
// intact to preserve wrapper types, identity and matching, so may still match
// cancellation. Initiating failures and every teardown error remain.
// Cancellation is omitted only when context.Cause proves App caused it; parent
// propagation and independently canceled descendants are retained conservatively.
// Run never recovers panics, closes Signals or exits the process.