impl connection recovery
This commit is contained in:
+163
-6
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -33,11 +34,26 @@ const (
|
||||
MuxWnd = 0x04
|
||||
MuxPing = 0x05
|
||||
MuxPong = 0x06
|
||||
// MuxResume reattaches a parked stream to this conn (CID + our accepted
|
||||
// offset); MuxResumeAck carries the hub's accepted offset and a fresh CID.
|
||||
MuxResume = 0x07
|
||||
MuxResumeAck = 0x08
|
||||
|
||||
// MuxCtlSid is the reserved stream id carrying connection-scoped mux frames
|
||||
// (PING/PONG). Real streams are numbered from 1.
|
||||
MuxCtlSid = 0
|
||||
|
||||
// RST reason codes (optional trailing byte; absence means "unspecified").
|
||||
// Distinguishing them matters for resume: an unknown stream is terminal,
|
||||
// while "already bound" means a racing attempt won and this one must leave
|
||||
// the stream alone rather than tear down a player the hub just rebound.
|
||||
RstUnspecified = 0x00
|
||||
RstUnknownStream = 0x01
|
||||
RstAlreadyBound = 0x02
|
||||
RstResumeAbandoned = 0x03
|
||||
RstFlowControl = 0x04
|
||||
RstDialFailed = 0x05
|
||||
|
||||
FrameError = 0x7F
|
||||
|
||||
// SaturationThreshold caps how many streams share one worker conn once the
|
||||
@@ -52,6 +68,12 @@ const (
|
||||
// firewall) is never detected: the read loop parks forever, the dead conn
|
||||
// stays in the pool, and no player can be served until the client restarts.
|
||||
FlagWorkerHeartbeat = 0x02
|
||||
// FlagStreamResume enables stream resumption (PROTOCOL.md §7.5): a worker
|
||||
// conn drop parks its streams instead of killing them, the hub hangs the
|
||||
// player sockets, and the client reattaches each stream byte-exactly over a
|
||||
// fresh conn. Negotiated, so either side may decline and get today's
|
||||
// behaviour (immediate teardown) unchanged.
|
||||
FlagStreamResume = 0x04
|
||||
|
||||
// Per-stream flow-control window bounds (bytes). The advertised window is the
|
||||
// receiver's promise of how much un-credited DATA it will buffer per stream.
|
||||
@@ -64,6 +86,32 @@ const (
|
||||
DataChunkSize = 32 * 1024
|
||||
)
|
||||
|
||||
// Egress bandwidth shaping (see shaper.go and docs/architecture.md §6). These
|
||||
// are entirely client-local: nothing here appears on the wire.
|
||||
const (
|
||||
// MinBandwidth floors a configured cap. Below this the tunnel cannot carry a
|
||||
// Minecraft session at all, so such a value is a unit typo ("20bps" for
|
||||
// "20mbps") and is rejected rather than silently clamped.
|
||||
MinBandwidth = 8 * 1024
|
||||
|
||||
// ShaperBurstSeconds is how much transmission time the token bucket banks
|
||||
// while idle. Big enough to absorb a chunk-load spike; small enough that
|
||||
// releasing it cannot overrun the physical uplink and rebuild the standing
|
||||
// queue the cap exists to prevent.
|
||||
ShaperBurstSeconds = 0.2
|
||||
|
||||
// MinShaperBurst must exceed DataChunkSize: a request larger than the bucket
|
||||
// could never be afforded and would park forever.
|
||||
MinShaperBurst = 64 * 1024
|
||||
MaxShaperBurst = 4 << 20
|
||||
|
||||
// ShaperSliceSeconds bounds how long one stream holds the link before the
|
||||
// scheduler can switch, by sizing the send chunk to that much transmission
|
||||
// time. Above ~13 Mbps this yields DataChunkSize and nothing changes.
|
||||
ShaperSliceSeconds = 0.02
|
||||
MinShaperChunk = 4 * 1024
|
||||
)
|
||||
|
||||
// Timeouts. Every tunnel socket is covered by one of these: without them a
|
||||
// silently dropped path (no FIN/RST) leaves the client parked forever.
|
||||
const (
|
||||
@@ -85,6 +133,32 @@ const (
|
||||
// heartbeat timeout can never be short enough to cause spurious drops.
|
||||
// Applied in LoadConfig, i.e. to configs that come from disk.
|
||||
MinPingIntervalMs = 1000
|
||||
|
||||
// DefaultResumeGraceMs is how long a parked stream keeps trying to reattach
|
||||
// before giving up and closing the destination.
|
||||
//
|
||||
// Chosen against the backend, not the tunnel: a hung player stops answering
|
||||
// the game server's KeepAlive, and vanilla disconnects a silent client at
|
||||
// 30s. A longer grace would resume sessions the backend then kicks anyway.
|
||||
DefaultResumeGraceMs = 15000
|
||||
|
||||
// MinResumeGraceMs floors the grace so it can always fit at least one dial;
|
||||
// a grace shorter than HandshakeTimeout could never complete an attempt.
|
||||
MinResumeGraceMs = 2000
|
||||
|
||||
// ResumeRetryDelay paces reattach attempts after a failure. Short, because
|
||||
// the player is hanging for the whole grace period.
|
||||
ResumeRetryDelay = 500 * time.Millisecond
|
||||
|
||||
// maxControlBackoff caps the control-session reconnect delay. The hub holds
|
||||
// this client's routes only for its own registration grace, so a backoff that
|
||||
// grew past that would strand players it is hanging on our behalf.
|
||||
maxControlBackoff = 10 * time.Second
|
||||
|
||||
// ResumeAckTimeout bounds the wait for RESUME_ACK on a conn that completed
|
||||
// its handshake but then went quiet, so a wedged hub does not consume the
|
||||
// entire grace budget in one attempt.
|
||||
ResumeAckTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// heartbeatTimeout is how long a session may go without a reply before it is
|
||||
@@ -111,12 +185,87 @@ type Mapping struct {
|
||||
|
||||
// Config is the client configuration (PROTOCOL.md §9.2).
|
||||
type Config struct {
|
||||
Server string `json:"server"`
|
||||
PSK string `json:"psk"`
|
||||
MaxConn int `json:"maxConn"`
|
||||
PingIntervalMs int `json:"pingIntervalMs"`
|
||||
StreamWindowBytes int `json:"streamWindowBytes"` // per-stream receive window; 0 = default
|
||||
Mappings []Mapping `json:"mappings"`
|
||||
Server string `json:"server"`
|
||||
PSK string `json:"psk"`
|
||||
MaxConn int `json:"maxConn"`
|
||||
PingIntervalMs int `json:"pingIntervalMs"`
|
||||
StreamWindowBytes int `json:"streamWindowBytes"` // per-stream receive window; 0 = default
|
||||
// MaxBandwidth caps what the client sends to the hub, aggregated over every
|
||||
// stream on every worker conn — the direction that carries the game server's
|
||||
// output to the players, and the one a residential uplink runs out of first.
|
||||
// Empty means no limit. See parseBandwidth for the accepted syntax.
|
||||
MaxBandwidth string `json:"maxBandwidth"`
|
||||
// StreamResume enables stream resumption (PROTOCOL.md §7.5). A pointer so an
|
||||
// absent key means "on" while an explicit false disables it: with it off the
|
||||
// client never offers the flag, allocates no retransmit buffers, and behaves
|
||||
// exactly as a pre-resume client.
|
||||
StreamResume *bool `json:"streamResume"`
|
||||
// ResumeGraceMs bounds how long a parked stream keeps trying to reattach.
|
||||
// Clamped below the hub's advertised grace so the client always gives up
|
||||
// first and the hub is never left holding a player nobody will claim.
|
||||
ResumeGraceMs int `json:"resumeGraceMs"`
|
||||
// StatsIntervalMs enables the periodic performance summary; 0 (the default)
|
||||
// disables it and costs nothing.
|
||||
StatsIntervalMs int `json:"statsIntervalMs"`
|
||||
Mappings []Mapping `json:"mappings"`
|
||||
}
|
||||
|
||||
// resumeEnabled reports whether stream resumption is configured on.
|
||||
func (c *Config) resumeEnabled() bool { return c.StreamResume == nil || *c.StreamResume }
|
||||
|
||||
// resumeGrace is how long a parked stream may keep trying to reattach.
|
||||
func (c *Config) resumeGrace() time.Duration {
|
||||
ms := c.ResumeGraceMs
|
||||
if ms <= 0 {
|
||||
ms = DefaultResumeGraceMs
|
||||
}
|
||||
if ms < MinResumeGraceMs {
|
||||
ms = MinResumeGraceMs
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
// bandwidthUnits maps a rate suffix to its value in bytes per second. Bit units
|
||||
// are decimal because that is what ISPs quote; byte units are binary to match
|
||||
// streamWindowBytes. Ordered longest-suffix-first so "kbps" is not read as
|
||||
// "bps", nor "gb/s" as "b/s".
|
||||
var bandwidthUnits = []struct {
|
||||
suffix string
|
||||
mul float64
|
||||
}{
|
||||
{"gbps", 1e9 / 8}, {"gbit", 1e9 / 8},
|
||||
{"mbps", 1e6 / 8}, {"mbit", 1e6 / 8},
|
||||
{"kbps", 1e3 / 8}, {"kbit", 1e3 / 8},
|
||||
{"gb/s", 1 << 30}, {"mb/s", 1 << 20}, {"kb/s", 1 << 10},
|
||||
{"bps", 1.0 / 8},
|
||||
{"b/s", 1},
|
||||
}
|
||||
|
||||
// parseBandwidth converts a human-readable rate to bytes per second. The empty
|
||||
// string means "no limit" and yields 0.
|
||||
//
|
||||
// "20mbps" 20 megabits/s = 2500000 B/s
|
||||
// "512kbps" 512 kilobits/s = 64000 B/s
|
||||
// "2MB/s" 2 mebibytes/s = 2097152 B/s
|
||||
// "1500000" a bare number is already bytes per second
|
||||
func parseBandwidth(s string) (int64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
lower := strings.ToLower(s)
|
||||
num, mul := lower, 1.0
|
||||
for _, u := range bandwidthUnits {
|
||||
if strings.HasSuffix(lower, u.suffix) {
|
||||
num, mul = strings.TrimSpace(lower[:len(lower)-len(u.suffix)]), u.mul
|
||||
break
|
||||
}
|
||||
}
|
||||
v, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil || v <= 0 {
|
||||
return 0, fmt.Errorf(`maxBandwidth: cannot read %q as a rate (try "20mbps", "2MB/s", or bytes per second)`, s)
|
||||
}
|
||||
return int64(v * mul), nil
|
||||
}
|
||||
|
||||
// LoadConfig reads and validates a JSON config file.
|
||||
@@ -147,6 +296,14 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if c.PingIntervalMs < MinPingIntervalMs {
|
||||
c.PingIntervalMs = MinPingIntervalMs
|
||||
}
|
||||
// Parsed here only to fail fast on a bad value; New does the real conversion.
|
||||
bps, err := parseBandwidth(c.MaxBandwidth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bps > 0 && bps < MinBandwidth {
|
||||
return nil, fmt.Errorf("maxBandwidth %q is only %d B/s; the minimum is %d B/s", c.MaxBandwidth, bps, MinBandwidth)
|
||||
}
|
||||
if len(c.Mappings) == 0 {
|
||||
return nil, fmt.Errorf("at least one mapping is required")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user