fix(lifecycle): preserve mixed shutdown error identity
This commit is contained in:
1 parent
b9a5fa8bd8
commit
64c7e6b65d
3 files changed
+91
-28
No files matched your search
+16
-26
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user