impl connection recovery
This commit is contained in:
+107
-18
@@ -25,7 +25,9 @@ type Client struct {
|
||||
mappings map[string]Mapping // normalized pattern -> mapping
|
||||
pool *WorkerPool
|
||||
|
||||
streamWnd int // our advertised per-stream receive window (bytes)
|
||||
streamWnd int // our advertised per-stream receive window (bytes)
|
||||
shaper *Shaper // caps aggregate egress to the hub; nil when unlimited
|
||||
chunk int // DATA payload cap; shrinks below DataChunkSize at low rates
|
||||
|
||||
mu sync.Mutex
|
||||
ctrl *wire.FramedConn
|
||||
@@ -48,6 +50,19 @@ func New(cfg *Config) *Client {
|
||||
c.mappings[NormalizeAddress(m.Pattern)] = m
|
||||
}
|
||||
c.streamWnd = clampWindow(cfg.StreamWindowBytes)
|
||||
// Parsed here rather than in LoadConfig because a Config may also be built
|
||||
// directly (tests). LoadConfig has already rejected a malformed value on the
|
||||
// file path, so a failure here can only come from a hand-built Config.
|
||||
bps, err := parseBandwidth(cfg.MaxBandwidth)
|
||||
if err != nil {
|
||||
log.Printf("client: %v; continuing without a bandwidth limit", err)
|
||||
}
|
||||
c.shaper = NewShaper(bps)
|
||||
c.chunk = c.shaper.chunkSize()
|
||||
if c.shaper != nil {
|
||||
log.Printf("egress shaped to %d B/s (burst %d B, chunk %d B)",
|
||||
bps, int64(c.shaper.burst), c.shaper.chunk)
|
||||
}
|
||||
c.pool = newWorkerPool(c, cfg.MaxConn)
|
||||
return c
|
||||
}
|
||||
@@ -71,6 +86,10 @@ type session struct {
|
||||
fc *wire.FramedConn
|
||||
peerWnd int // hub's advertised per-stream receive window
|
||||
heartbeat bool // hub accepted mux-level PING/PONG on worker conns
|
||||
resume bool // hub accepted stream resumption (§7.5)
|
||||
// hubGrace is how long the hub will hang a parked player, as advertised in
|
||||
// SessionReady. Zero when resumption was not negotiated.
|
||||
hubGrace time.Duration
|
||||
}
|
||||
|
||||
// dialSession opens a TCP connection, performs the Intent-17 handshake, the
|
||||
@@ -120,6 +139,9 @@ func (c *Client) dialSession(magic byte) (sess *session, err error) {
|
||||
}
|
||||
ts := time.Now().UnixMilli()
|
||||
offered := FlagStreamFC | FlagWorkerHeartbeat
|
||||
if c.cfg.resumeEnabled() {
|
||||
offered |= FlagStreamResume
|
||||
}
|
||||
rekeyMsg := wire.NewWriter().U8(magic).VarInt(len(rnd)).Bytes(rnd).I64(ts).
|
||||
VarInt(offered).VarInt(c.streamWnd).Out()
|
||||
if err := fc.WriteFrame(rekeyMsg); err != nil {
|
||||
@@ -155,6 +177,22 @@ func (c *Client) dialSession(magic byte) (sess *session, err error) {
|
||||
if hubWnd > MaxStreamWindow {
|
||||
hubWnd = MaxStreamWindow
|
||||
}
|
||||
// Resumption is negotiated per connection, and the hub's grace period rides
|
||||
// along when it accepts. Our own grace is clamped strictly under the hub's:
|
||||
// the client must always give up first, or the hub drops a hanging player
|
||||
// while we are still mid-reattach. A hub that accepts the flag but omits the
|
||||
// grace is treated as not supporting it at all rather than guessed at.
|
||||
resume := flags&FlagStreamResume != 0
|
||||
var hubGrace time.Duration
|
||||
if resume {
|
||||
graceMs, gerr := r.VarInt()
|
||||
if gerr != nil || graceMs <= 0 {
|
||||
log.Printf("hub accepted stream resume without advertising a grace period; disabling resume")
|
||||
resume = false
|
||||
} else {
|
||||
hubGrace = time.Duration(graceMs) * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
// The session is live: drop the establishment deadline. From here on
|
||||
// liveness is the heartbeat's job (and WriteFrame bounds each write).
|
||||
@@ -162,14 +200,30 @@ func (c *Client) dialSession(magic byte) (sess *session, err error) {
|
||||
return nil, err
|
||||
}
|
||||
ok = true
|
||||
return &session{fc: fc, peerWnd: hubWnd, heartbeat: flags&FlagWorkerHeartbeat != 0}, nil
|
||||
return &session{
|
||||
fc: fc,
|
||||
peerWnd: hubWnd,
|
||||
heartbeat: flags&FlagWorkerHeartbeat != 0,
|
||||
resume: resume,
|
||||
hubGrace: hubGrace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// statsOn reports whether performance diagnostics are enabled. When off, no
|
||||
// counter struct is ever allocated and the instrumentation is a single branch.
|
||||
func (c *Client) statsOn() bool { return c.cfg.StatsIntervalMs > 0 }
|
||||
|
||||
// Start establishes the control session and registers all patterns. It returns
|
||||
// once the initial connection succeeds; subsequent drops are handled in the
|
||||
// background with reconnect.
|
||||
func (c *Client) Start(ctx context.Context) error {
|
||||
return c.connectControl(ctx)
|
||||
if err := c.connectControl(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.statsOn() {
|
||||
go c.statsLoop(ctx.Done())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) connectControl(ctx context.Context) error {
|
||||
@@ -220,17 +274,33 @@ func (c *Client) serveControl(ctx context.Context, ctrl *ctrlSession) {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
// Reconnect with backoff.
|
||||
for backoff := 500 * time.Millisecond; ctx.Err() == nil; backoff *= 2 {
|
||||
if backoff > 10*time.Second {
|
||||
backoff = 10 * time.Second
|
||||
// Reconnect with backoff, but try immediately first. While the control
|
||||
// session is down the hub has no live route for this client, so every
|
||||
// millisecond of delay is a player arriving to be told there is no such
|
||||
// server — and a session usually dies to a transient blip that the very next
|
||||
// dial would have survived. Sleeping first spent that window unconditionally.
|
||||
//
|
||||
// The wait is on ctx rather than time.Sleep so shutdown is not held up by a
|
||||
// backoff that has grown to the cap.
|
||||
for backoff := time.Duration(0); ctx.Err() == nil; {
|
||||
if backoff > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
if err := c.connectControl(ctx); err == nil {
|
||||
return
|
||||
} else {
|
||||
log.Printf("control reconnect failed: %v", err)
|
||||
}
|
||||
switch {
|
||||
case backoff == 0:
|
||||
backoff = 500 * time.Millisecond
|
||||
case backoff < maxControlBackoff:
|
||||
backoff = min(backoff*2, maxControlBackoff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,20 +369,38 @@ func (c *Client) handleControlRequest(cid []byte, pattern, ip string, port int)
|
||||
return
|
||||
}
|
||||
log.Printf("player %s:%d joined via pattern %q -> %s", ip, port, pattern, mapping.Destination)
|
||||
wc, sid, err := c.pool.Allocate()
|
||||
if err != nil {
|
||||
log.Printf("worker allocate failed: %v", err)
|
||||
|
||||
// Allocate and publish must agree on a live conn: Allocate hands out a
|
||||
// (conn, sid) pair that can die before we register on it, which would strand
|
||||
// the stream in a map nothing iterates. registerStream reports that, and we
|
||||
// simply pick another conn.
|
||||
var st *Stream
|
||||
var lg *leg
|
||||
for attempt := 0; attempt < allocateAttempts; attempt++ {
|
||||
wc, sid, err := c.pool.Allocate()
|
||||
if err != nil {
|
||||
log.Printf("worker allocate failed: %v", err)
|
||||
return
|
||||
}
|
||||
st = newStream(c, wc, sid, cid, mapping, ip, port)
|
||||
// Register before SYN so inbound DATA can never race ahead of the table,
|
||||
// and start the pumps before the (bounded) SYN write so a failed or slow
|
||||
// SYN cannot strand a stream that nothing would ever tear down.
|
||||
if wc.registerStream(sid, st) {
|
||||
lg = st.conn()
|
||||
break
|
||||
}
|
||||
st = nil
|
||||
}
|
||||
if st == nil {
|
||||
log.Printf("worker allocate failed: no live conn after %d attempts", allocateAttempts)
|
||||
return
|
||||
}
|
||||
st := newStream(wc, sid, cid, mapping, ip, port)
|
||||
// Register before SYN so inbound DATA can never race ahead of the table,
|
||||
// and start the pumps before the (bounded) SYN write so a failed or slow
|
||||
// SYN cannot strand a stream that nothing would ever tear down.
|
||||
wc.registerStream(sid, st)
|
||||
|
||||
go st.writeLoop()
|
||||
go st.run()
|
||||
if err := wc.sendSyn(sid, cid); err != nil {
|
||||
log.Printf("stream %d: SYN failed: %v", sid, err)
|
||||
if err := lg.wc.sendSyn(lg.sid, cid); err != nil {
|
||||
log.Printf("stream %d: SYN failed: %v", lg.sid, err)
|
||||
st.teardown(false)
|
||||
}
|
||||
}
|
||||
@@ -330,4 +418,5 @@ func (c *Client) Close() {
|
||||
_ = fc.Close()
|
||||
}
|
||||
c.pool.closeAll()
|
||||
c.shaper.Stop()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user