fix: harden resume/shutdown paths, tighten Intent-18 and PSK handshake handling
Client (Go) — resume correctness - C1: completeResume now re-parks the stream when replay fails mid-conn-loss. parked was cleared before the replay loop, so the dying conn's teardown would start a second resumeLoop and the two loops could strand the stream with neither alive. Resume stats are counted only after the replay lands. - C2: RST(ALREADY_BOUND) is retryable instead of terminating the loop. With C1 fixed there is never a genuine second attempt, so "already bound" means the hub still holds the stream on a half-open conn; the retry waits out that bind (bounded by the grace deadline, teardown on expiry) instead of returning and leaving the destination socket hung forever. Client (Go) — shutdown semantics - C3: Close() sets a closing flag and cancels an internal context; dialSession takes a ctx (DialContext + AfterFunc so shutdown aborts in-flight handshakes); the worker pool refuses new conns after closeAll (Allocate, background growth, cond waiters); serveControl's reconnect loop is gated by closing so Close works even when the caller's Start context is not cancelled; conn-loss teardown closes streams outright during shutdown instead of parking them for a reattach that will never come. Client (Go) — hygiene - C4: pingInterval() clamps at the single point a duration is derived, so a hand-built Config with PingIntervalMs <= 0 can no longer panic time.NewTicker (added DefaultPingIntervalMs). - E6: shaperStall is sampled right after shaper.Acquire, before the socket write, so a hub that is not reading is no longer charged to the bandwidth cap in the stats. - E7: stream log lines now carry conn%d/sid%d (leg.String()), making streams traceable across reattaches. - P5: mirror constants IntentReserved/RegisterOk/RegisterErrPattern added; RegisterAck dispatch logs rejection reasons via the named codes. Hub (Java) + PROTOCOL.md - P3: Intent 18 replies with a Minecraft status-response packet ([Len: VarInt][0x00][JSON: String]) and closes (socket.end, so the write always lands) instead of closing silently; documented in PROTOCOL.md §2. - P4: PSK address check is strict equality with the lowercase hex address; an uppercase/case-folded variant is now rejected per PROTOCOL.md §2. - P7: PROTOCOL.md §5 SessionReady row lists its real fields (Flags/RecvWindow/ResumeGraceMs) instead of "(none)". Verified: go vet, go test -race ./client/..., gradle test, full e2e suite (twice), resume e2e 3x, plus live probes of the hub with the real client codec (Intent-18 status reply, strict-lowercase PSK acceptance/rejection).%
This commit is contained in:
+33
-7
@@ -12,7 +12,12 @@ import (
|
||||
// Protocol constants (mirror of the Java Protocol class; see PROTOCOL.md).
|
||||
const (
|
||||
IntentRedapricot = 17
|
||||
ProtocolVersion = 767 // arbitrary; the hub ignores it
|
||||
// IntentReserved is the handshake intent reserved for redapricot
|
||||
// management/status (mirror of Protocol.INTENT_RESERVED). The hub never
|
||||
// pattern-matches it; it replies with a Minecraft status line and closes,
|
||||
// so an operator can probe the port without joining the protocol.
|
||||
IntentReserved = 18
|
||||
ProtocolVersion = 767 // arbitrary; the hub ignores it
|
||||
|
||||
MagicControl = 0x01
|
||||
MagicWorker = 0x02
|
||||
@@ -27,6 +32,10 @@ const (
|
||||
CtlPing = 0x05
|
||||
CtlPong = 0x06
|
||||
|
||||
// RegisterAck status codes (mirror of Protocol.REGISTER_OK/_ERR_PATTERN).
|
||||
RegisterOk = 0x00
|
||||
RegisterErrPattern = 0x01 // pattern is not a valid regular expression
|
||||
|
||||
MuxSyn = 0x00
|
||||
MuxData = 0x01
|
||||
MuxFin = 0x02
|
||||
@@ -45,8 +54,8 @@ const (
|
||||
|
||||
// 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.
|
||||
// while "already bound" means the hub has the stream on another conn — the
|
||||
// reattach retries until that bind dies and the hub re-parks the stream.
|
||||
RstUnspecified = 0x00
|
||||
RstUnknownStream = 0x01
|
||||
RstAlreadyBound = 0x02
|
||||
@@ -129,9 +138,15 @@ const (
|
||||
// a session is declared dead and dropped.
|
||||
MissedHeartbeats = 3
|
||||
|
||||
// DefaultPingIntervalMs is used when a config carries no (or a
|
||||
// non-positive) interval. LoadConfig applies the same default.
|
||||
DefaultPingIntervalMs = 20000
|
||||
|
||||
// MinPingIntervalMs floors the configured ping interval so the derived
|
||||
// heartbeat timeout can never be short enough to cause spurious drops.
|
||||
// Applied in LoadConfig, i.e. to configs that come from disk.
|
||||
// Applied wherever the interval is read, not just on the file path: a
|
||||
// hand-built Config (tests) carrying 0 would otherwise panic
|
||||
// time.NewTicker at the call site.
|
||||
MinPingIntervalMs = 1000
|
||||
|
||||
// DefaultResumeGraceMs is how long a parked stream keeps trying to reattach
|
||||
@@ -167,9 +182,20 @@ func (c *Config) heartbeatTimeout() time.Duration {
|
||||
return c.pingInterval() * MissedHeartbeats
|
||||
}
|
||||
|
||||
// pingInterval is the configured heartbeat period.
|
||||
// pingInterval is the configured heartbeat period, clamped at the single point
|
||||
// where a duration is derived. LoadConfig also clamps on the file path; this
|
||||
// covers Configs built directly (tests), where a 0 or negative PingIntervalMs
|
||||
// would panic time.NewTicker — a panic, not a log line, because the interval
|
||||
// feeds the heartbeat timeout too.
|
||||
func (c *Config) pingInterval() time.Duration {
|
||||
return time.Duration(c.PingIntervalMs) * time.Millisecond
|
||||
ms := c.PingIntervalMs
|
||||
if ms <= 0 {
|
||||
ms = DefaultPingIntervalMs
|
||||
}
|
||||
if ms < MinPingIntervalMs {
|
||||
ms = MinPingIntervalMs
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
// Mapping routes a registered pattern to a real destination.
|
||||
@@ -291,7 +317,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||
c.MaxConn = 8
|
||||
}
|
||||
if c.PingIntervalMs <= 0 {
|
||||
c.PingIntervalMs = 20000
|
||||
c.PingIntervalMs = DefaultPingIntervalMs
|
||||
}
|
||||
if c.PingIntervalMs < MinPingIntervalMs {
|
||||
c.PingIntervalMs = MinPingIntervalMs
|
||||
|
||||
Reference in New Issue
Block a user