Files
Quentin McGaw 4a78989d9d chore: do not use sentinel errors when unneeded
- main reason being it's a burden to always define sentinel errors at global scope, wrap them with `%w` instead of using a string directly
- only use sentinel errors when it has to be checked using `errors.Is`
- replace all usage of these sentinel errors in `fmt.Errorf` with direct strings that were in the sentinel error
- exclude the sentinel error definition requirement from .golangci.yml
- update unit tests to use ContainersError instead of ErrorIs so it stays as a "not a change detector test" without requiring a sentinel error
2026-05-02 03:29:46 +00:00

106 lines
2.4 KiB
Go

package resolver
import (
"context"
"fmt"
"net/netip"
)
type Parallel struct {
repeatResolver *Repeat
}
func NewParallelResolver(dialer Dialer) *Parallel {
return &Parallel{
repeatResolver: NewRepeat(dialer),
}
}
type ParallelSettings struct {
// Hosts to resolve in parallel.
Hosts []string
Repeat RepeatSettings
FailEarly bool
// Maximum ratio of the hosts failing DNS resolution
// divided by the total number of hosts requested.
// This value is between 0 and 1. Note this is only
// applicable if FailEarly is not set to true.
MaxFailRatio float64
}
type parallelResult struct {
host string
IPs []netip.Addr
}
func (pr *Parallel) Resolve(ctx context.Context, settings ParallelSettings) (
hostToIPs map[string][]netip.Addr, warnings []string, err error,
) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan parallelResult)
defer close(results)
errors := make(chan error)
defer close(errors)
for _, host := range settings.Hosts {
go pr.resolveAsync(ctx, host, settings.Repeat, results, errors)
}
hostToIPs = make(map[string][]netip.Addr, len(settings.Hosts))
maxFails := int(settings.MaxFailRatio * float64(len(settings.Hosts)))
for range settings.Hosts {
select {
case newErr := <-errors:
if settings.FailEarly {
if err == nil {
// only set the error to the first error encountered
// and not the context canceled errors coming after.
err = newErr
cancel()
}
break
}
// do not add warnings coming from the call to cancel()
if len(warnings) < maxFails {
warnings = append(warnings, newErr.Error())
}
if len(warnings) == maxFails {
cancel() // cancel only once when we reach maxFails
}
case result := <-results:
hostToIPs[result.host] = result.IPs
}
}
if err != nil { // fail early
return nil, warnings, err
}
failureRatio := float64(len(warnings)) / float64(len(settings.Hosts))
if failureRatio > settings.MaxFailRatio {
return hostToIPs, warnings,
fmt.Errorf("maximum failure ratio reached: %.2f failure ratio reached", failureRatio)
}
return hostToIPs, warnings, nil
}
func (pr *Parallel) resolveAsync(ctx context.Context, host string,
settings RepeatSettings, results chan<- parallelResult, errors chan<- error,
) {
IPs, err := pr.repeatResolver.Resolve(ctx, host, settings)
if err != nil {
errors <- err
return
}
results <- parallelResult{
host: host,
IPs: IPs,
}
}