internal/pkg: deduplicate dependency errors
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 2m44s
Test / ShareFS (push) Successful in 3m42s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (push) Successful in 4m43s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 2m44s

This significantly simplifies error reporting for caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-01-17 14:16:29 +09:00
parent 610ee13ab3
commit efc90c3221
3 changed files with 177 additions and 9 deletions

View File

@@ -394,7 +394,7 @@ type pendingArtifactDep struct {
// Address of result error slice populated during [Cache.Cure], dereferenced
// after acquiring errsMu if curing fails. No additional action is taken,
// [Cache] and its caller are responsible for further error handling.
errs *[]error
errs *DependencyCureError
// Address of mutex synchronising access to errs.
errsMu *sync.Mutex
@@ -1108,6 +1108,76 @@ func (c *Cache) Cure(a Artifact) (
return c.cure(a)
}
// CureError wraps a non-nil error returned attempting to cure an [Artifact].
type CureError struct {
Ident unique.Handle[ID]
Err error
}
// Unwrap returns the underlying error.
func (e *CureError) Unwrap() error { return e.Err }
// Error returns the error message from the underlying Err.
func (e *CureError) Error() string { return e.Err.Error() }
// A DependencyCureError wraps errors returned while curing dependencies.
type DependencyCureError []*CureError
// sort sorts underlying errors by their identifier.
func (e *DependencyCureError) sort() {
var identBuf [2]ID
slices.SortFunc(*e, func(a, b *CureError) int {
identBuf[0], identBuf[1] = a.Ident.Value(), b.Ident.Value()
return slices.Compare(identBuf[0][:], identBuf[1][:])
})
}
// unwrap recursively expands and deduplicates underlying errors.
func (e *DependencyCureError) unwrap() DependencyCureError {
errs := make(DependencyCureError, 0, len(*e))
for _, err := range *e {
if _e, ok := err.Err.(*DependencyCureError); ok {
errs = append(errs, _e.unwrap()...)
continue
}
errs = append(errs, err)
}
me := make(map[unique.Handle[ID]]*CureError, len(errs))
for _, err := range errs {
me[err.Ident] = err
}
return slices.AppendSeq(
make(DependencyCureError, 0, len(me)),
maps.Values(me),
)
}
// Unwrap returns a deduplicated slice of underlying errors.
func (e *DependencyCureError) Unwrap() []error {
errs := e.unwrap()
errs.sort()
_errs := make([]error, len(errs))
for i, err := range errs {
_errs[i] = err
}
return _errs
}
// Error returns a user-facing multiline error message.
func (e *DependencyCureError) Error() string {
errs := e.unwrap()
errs.sort()
if len(errs) == 0 {
return "invalid dependency cure outcome"
}
var buf strings.Builder
buf.WriteString("errors curing dependencies:")
for _, err := range errs {
buf.WriteString("\n\t" + Encode(err.Ident.Value()) + ": " + err.Error())
}
return buf.String()
}
// cure implements Cure without checking the full dependency graph.
func (c *Cache) cure(a Artifact) (
pathname *check.Absolute,
@@ -1290,7 +1360,7 @@ func (c *Cache) cure(a Artifact) (
var wg sync.WaitGroup
wg.Add(len(deps))
res := make([]*check.Absolute, len(deps))
errs := make([]error, 0, len(deps))
errs := make(DependencyCureError, 0, len(deps))
var errsMu sync.Mutex
for i, d := range deps {
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
@@ -1306,11 +1376,7 @@ func (c *Cache) cure(a Artifact) (
wg.Wait()
if len(errs) > 0 {
err = errors.Join(errs...)
if err == nil {
// unreachable
err = syscall.ENOTRECOVERABLE
}
err = &errs
return
}
for i, p := range res {
@@ -1411,7 +1477,7 @@ func (pending *pendingArtifactDep) cure(c *Cache) {
}
pending.errsMu.Lock()
*pending.errs = append(*pending.errs, err)
*pending.errs = append(*pending.errs, &CureError{c.Ident(pending.a), err})
pending.errsMu.Unlock()
}