Loosen connect timeouts for slow links · Entire
Loosen connect timeouts for slow links
fcc4748→main·
toothbrush·3w ago·5 files·+138 added/-30 removed
Bump the default per-host TCP connect timeout 2s -> 4s (satellite links saw no node respond under 2s on push), and give the cold replica-discovery probe its own more patient 10s budget — that first "which nodes serve this repo" contact pays a cold DNS+TLS handshake to a possibly-distant entry LB and has no failover to fall back on, so a tight budget fails the whole clone/fetch. Replica failover keeps the short default so dead nodes are still skipped quickly.
An explicit ENTIRE_CONNECT_TIMEOUT_SECONDS now overrides every connect timeout (discovery included) via os.LookupEnv, instead of being floored.
git-remote-entire's auth client (cluster /.well-known discovery + token exchange) also uses the patient discovery budget: it talks to single control-plane hosts with no failover.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
926e86709119View transcript
Changes
5
cmd/git-remote-entire
- Mmain.go+6/-1
internal
- entireclient/httpclient
- Mtransport.go+62/-13
- Mtransport_test.go+23
- remotehelper/transport
- Mproxy.go+27/-14
- Mproxy_test.go+20/-2
- entireclient/httpclient
102 unmodified lines
103
104
105
106
107
108
109
110
111
112
113
109
114
115
116
117
102 unmodified lines
clusterBaseURL := nodeCfg.EntryURL
repoSlug := parsedURL.Path
// This client drives the auth path only: cluster /.well-known discovery
// and the token exchange. Both talk to a single control-plane host with no
// failover to fall back on, so they get the patient discovery dial budget
// (DefaultDiscoveryDialTimeout) rather than the short failover one — a slow
// cold connect here would otherwise fail the whole clone/fetch.
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &httpclient.UserAgentTransport{
Next: httpclient.NewTransport(skipTLS),
Next: httpclient.NewDiscoveryTransport(skipTLS),
UA: httpUserAgent,
},
}
Mcmd/git-remote-entire/main.go+6/-1
15 unmodified lines
16
17
18
19
20
21
22
23
24
25
23
24
26
27
28
26
27
28
29
30
31
32
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
36
37
53
54
55
39
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
1 unmodified line
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
48
97
98
99
100
15 unmodified lines
"time"
// EnvConnectTimeout is the env var that overrides every connect timeout.
const EnvConnectTimeout = "ENTIRE_CONNECT_TIMEOUT_SECONDS"
// DefaultDialTimeout is the per-host TCP connect timeout. Short by default
// so failover paths skip dead nodes quickly, but long enough to absorb a
// slow initial connect (cold DNS, TLS-fronting LB, distant region) that a
// tighter budget would trip; override via ENTIRE_CONNECT_TIMEOUT_SECONDS on
// slow links where even this trips before the node can answer.
const DefaultDialTimeout = 2 * time.Second
// slow links (e.g. satellite) where even this trips before the node answers.
const DefaultDialTimeout = 4 * time.Second
// DialTimeout returns the configured dial timeout, honoring
// ENTIRE_CONNECT_TIMEOUT_SECONDS. Invalid values fall back to
// DefaultDialTimeout with a warning to stderr.
func DialTimeout() time.Duration {
v := os.Getenv("ENTIRE_CONNECT_TIMEOUT_SECONDS")
if v == "" {
return DefaultDialTimeout
// DefaultDiscoveryDialTimeout is the per-host TCP connect budget for the
// initial replica-discovery request — the cold info/refs probe to the
// cluster entry domain that answers "which nodes serve this repo". It is
// deliberately longer than DefaultDialTimeout: this first contact often pays
// a cold DNS + TLS handshake to a possibly-distant entry LB, and unlike
// replica failover there is no second node to roll to, so tripping here fails
// the whole clone/fetch. Replica failover keeps the short DefaultDialTimeout
// so dead nodes are still skipped quickly.
// An explicit ENTIRE_CONNECT_TIMEOUT_SECONDS overrides this too: a user who
// sets it gets that one value for every connect, discovery included.
const DefaultDiscoveryDialTimeout = 10 * time.Second
// envConnectTimeout reads ENTIRE_CONNECT_TIMEOUT_SECONDS. It returns the
// parsed duration and true only when the var is set to a positive integer;
// an unset/blank var yields false, and an invalid value warns to stderr and
// yields false so callers fall back to their own default.
func envConnectTimeout() (time.Duration, bool) {
v, ok := os.LookupEnv(EnvConnectTimeout)
if !ok || v == "" {
return 0, false
}
secs, err := strconv.Atoi(v)
if err != nil || secs <= 0 {
fmt.Fprintf(os.Stderr, "httpclient: ignoring invalid ENTIRE_CONNECT_TIMEOUT_SECONDS=%q, using default %s\n", v, DefaultDialTimeout)
return DefaultDialTimeout
fmt.Fprintf(os.Stderr, "httpclient: ignoring invalid %s=%q, using defaults\n", EnvConnectTimeout, v)
return 0, false
}
return time.Duration(secs) * time.Second
return time.Duration(secs) * time.Second, true
}
// DialTimeout returns the connect timeout for ordinary requests (and replica
// failover): the ENTIRE_CONNECT_TIMEOUT_SECONDS override if set, else
// DefaultDialTimeout.
func DialTimeout() time.Duration {
if d, ok := envConnectTimeout(); ok {
return d
}
return DefaultDialTimeout
}
// DiscoveryDialTimeout returns the connect budget for the discovery probe.
// When ENTIRE_CONNECT_TIMEOUT_SECONDS is set it wins outright (the user's
// chosen value applies to every connect); otherwise it falls back to the
// more patient DefaultDiscoveryDialTimeout rather than DefaultDialTimeout.
func DiscoveryDialTimeout() time.Duration {
if d, ok := envConnectTimeout(); ok {
return d
}
return DefaultDiscoveryDialTimeout
}
// NewTransport builds an *http.Transport with the configured dial timeout
1 unmodified line
// own RoundTripper as needed (e.g. debug logging) and assemble their own
// *http.Client around it.
func NewTransport(skipTLSVerify bool) *http.Transport {
return newTransport(skipTLSVerify, DialTimeout())
}
// NewDiscoveryTransport is NewTransport with the longer discovery connect
// budget (DiscoveryDialTimeout). Use it for the initial replica-discovery
// probe; see DefaultDiscoveryDialTimeout for why it is more patient.
func NewDiscoveryTransport(skipTLSVerify bool) *http.Transport {
return newTransport(skipTLSVerify, DiscoveryDialTimeout())
}
func newTransport(skipTLSVerify bool, dialTimeout time.Duration) *http.Transport {
return &http.Transport{
DialContext: (&net.Dialer{Timeout: DialTimeout()}).DialContext,
DialContext: (&net.Dialer{Timeout: dialTimeout}).DialContext,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: skipTLSVerify, //nolint:gosec // intentional for local development