fix: quick reconnect after sleep or a network outage - the tunnel notices a dead connection, retries every 5 s at most, the WebSocket returns as soon as the server answers; version 0.1.1

This commit is contained in:
redtea committed 2026-10-15 00:07:32 +03:00
1 parent 5eca9ca215
commit b47e824692
3 files changed
+49 -13

No files matched your search

+1 -1
View File
@@ -1 +1 @@
0.1.0
0.1.1
+40 -6
View File
@@ -131,9 +131,7 @@ func (a *Agent) Run(ctx context.Context) error {
case <-ctx.Done():
return nil
}
if backoff < 15*time.Second {
backoff *= 2
}
backoff = min(backoff*2, retryCap)
continue
case proto.TransportAuto:
log.Printf("websocket: %v - long polling, websocket again in a minute", err)
@@ -167,6 +165,10 @@ func (a *Agent) applyTransport(v string) {
func (a *Agent) longPoll(ctx context.Context, d time.Duration) {
until := time.Now().Add(d)
backoff := time.Second
// The server was out of reach (a network down, a machine asleep): once
// a poll gets through again, "auto" tries the WebSocket at once rather
// than at the end of the minute - it was the network, not the WebSocket.
lost := false
for ctx.Err() == nil {
if d > 0 && (time.Now().After(until) || a.mode() == proto.TransportWS) {
return
@@ -174,6 +176,21 @@ func (a *Agent) longPoll(ctx context.Context, d time.Duration) {
if d == 0 && a.mode() != proto.TransportLP {
return
}
// Back from out of reach in "auto": a quick request tells when the
// server answers again, and the WebSocket is tried then; a poll
// would sit held on the server first.
if lost && d > 0 {
if a.reachable(ctx) {
return
}
select {
case <-time.After(backoff):
case <-ctx.Done():
return
}
backoff = min(backoff*2, retryCap)
continue
}
// In "auto" the minute is a minute: a poll still held then is cut
// short (the server keeps its job for the next one).
reqCtx, cancel := ctx, context.CancelFunc(func() {})
@@ -193,15 +210,32 @@ func (a *Agent) longPoll(ctx context.Context, d time.Duration) {
case <-ctx.Done():
return
}
if backoff < 15*time.Second {
backoff *= 2
}
backoff = min(backoff*2, retryCap)
lost = true
continue
}
backoff = time.Second
}
}
// reachable reports whether the server answers a quick request.
func (a *Agent) reachable(ctx context.Context) bool {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.Host+"/api/agent/config", nil)
a.authHeaders(req)
resp, err := a.http.Do(req)
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode < 300
}
// retryCap bounds the wait between attempts to reach the server, so a
// machine back from sleep or a network back up is reconnected in seconds.
const retryCap = 5 * time.Second
func (a *Agent) enroll(ctx context.Context) error {
host, _ := os.Hostname()
body, _ := json.Marshal(proto.EnrollRequest{
+8 -6
View File
@@ -26,9 +26,7 @@ func (a *Agent) runTunnel(ctx context.Context, addr string) {
case <-ctx.Done():
return
}
if backoff < 15*time.Second {
backoff *= 2
}
backoff = min(backoff*2, retryCap)
continue
}
backoff = time.Second
@@ -96,8 +94,14 @@ func (t *tunClient) write(f tunnel.Frame) error {
return tunnel.WriteFrame(t.conn, f)
}
// tunnelSilence is how long the tunnel may say nothing before it is taken
// for dead: the server pings every 20 s, so this is three missed pings - a
// connection a NAT forgot while the machine slept, say.
const tunnelSilence = 65 * time.Second
func (t *tunClient) readLoop() error {
for {
_ = t.conn.SetReadDeadline(time.Now().Add(tunnelSilence))
f, err := tunnel.ReadFrame(t.conn)
if err != nil {
t.shutdown()
@@ -207,9 +211,7 @@ func (a *Agent) runUDP(ctx context.Context, addr string) {
case <-ctx.Done():
return
}
if backoff < 15*time.Second {
backoff *= 2
}
backoff = min(backoff*2, retryCap)
continue
}
backoff = time.Second