resolver: a query cut short by the caller's context records no reason (closes #298)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
When the caller's context ends, the next query returns ErrContextCanceled before anything is sent. querySingleType recorded that as a network error, which outranks a timeout, so a nameserver that had only timed out could be reported as "network error: context canceled". It now records no reason for it. Model: opus-5-5
This commit is contained in:
@@ -19,6 +19,9 @@ milestone: https://git.eeqj.de/sneak/dnswatcher/milestone/7
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-15: a query cut short by its caller's context records no reason about
|
||||||
|
the nameserver, so one whose only failures were timeouts is `timeout`, not a
|
||||||
|
network error (closes #298).
|
||||||
- 2026-10-15: the resolver no longer says its query timeout grows on each try;
|
- 2026-10-15: the resolver no longer says its query timeout grows on each try;
|
||||||
every try waits the same 2 seconds, and the unused `timeoutMultiplier` is gone
|
every try waits the same 2 seconds, and the unused `timeoutMultiplier` is gone
|
||||||
(closes #296).
|
(closes #296).
|
||||||
|
|||||||
@@ -863,6 +863,10 @@ func (r *Resolver) querySingleType(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("query %s @%s: %w", hostname, nsIP, err)
|
err = fmt.Errorf("query %s @%s: %w", hostname, nsIP, err)
|
||||||
}
|
}
|
||||||
|
case errors.Is(err, ErrContextCanceled):
|
||||||
|
// The caller's context ended and cut the query short. That
|
||||||
|
// says nothing about the nameserver, so no reason is
|
||||||
|
// recorded.
|
||||||
case isTimeout(err):
|
case isTimeout(err):
|
||||||
state.gotTimeout = true
|
state.gotTimeout = true
|
||||||
case errors.Is(err, ErrRefused):
|
case errors.Is(err, ErrRefused):
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -132,3 +134,33 @@ func TestReadReply(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestQuerySingleType_ContextEnded asks a nameserver about one record
|
||||||
|
// type with a context that has already ended, so no query is sent. A
|
||||||
|
// query the context cut short says nothing about the nameserver and
|
||||||
|
// records no reason: a nameserver whose earlier query timed out is
|
||||||
|
// still reported as timed out, not as a network error.
|
||||||
|
func TestQuerySingleType_ContextEnded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := NewFromLogger(slog.New(slog.DiscardHandler))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
resp := &NameserverResponse{Records: map[string][]string{}, Status: StatusOK}
|
||||||
|
|
||||||
|
// The query for an earlier record type timed out.
|
||||||
|
state := queryState{gotTimeout: true}
|
||||||
|
|
||||||
|
_, err := r.querySingleType(
|
||||||
|
ctx, []string{"192.0.2.1"}, "example.com.", dns.TypeA, resp, &state,
|
||||||
|
)
|
||||||
|
require.ErrorIs(t, err, ErrContextCanceled)
|
||||||
|
assert.Equal(t, queryState{gotTimeout: true}, state)
|
||||||
|
|
||||||
|
classifyResponse(resp, state)
|
||||||
|
|
||||||
|
assert.Equal(t, StatusTimeout, resp.Status)
|
||||||
|
assert.Equal(t, "all queries timed out", resp.Error)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user