New
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"dragontcp/internal/cover"
|
||||
"dragontcp/internal/protocol"
|
||||
"dragontcp/internal/wire"
|
||||
)
|
||||
@@ -29,6 +30,9 @@ type chunkClientOptions struct {
|
||||
tcpBuffer int
|
||||
minPipeline int
|
||||
maxPipeline int
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
skipPathProbe bool
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
@@ -120,6 +124,10 @@ func (s *adaptiveSizer) Success(attempted int) {
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) Failure(attempted int) (int, int) {
|
||||
return s.FailureReason(attempted, nil)
|
||||
}
|
||||
|
||||
func (s *adaptiveSizer) FailureReason(attempted int, cause error) (int, int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.current
|
||||
@@ -147,7 +155,11 @@ func (s *adaptiveSizer) Failure(attempted int) (int, int) {
|
||||
}
|
||||
s.current = next
|
||||
if s.logChanges && old != next {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
if cause != nil {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure: %v\n", s.name, old, next, cause)
|
||||
} else {
|
||||
fmt.Printf("adaptive %s chunk: %d -> %d after transport failure\n", s.name, old, next)
|
||||
}
|
||||
}
|
||||
return old, next
|
||||
}
|
||||
@@ -163,19 +175,39 @@ type requestLane struct {
|
||||
tcpBuffer int
|
||||
reconnectEvery int
|
||||
timeout time.Duration
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
autoReconnect bool
|
||||
pc *physicalConn
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration) *requestLane {
|
||||
func newRequestLane(serverAddr string, tcpBuffer, reconnectEvery int, timeout time.Duration, headerMask byte, coverProfile cover.Profile) *requestLane {
|
||||
autoReconnect := reconnectEvery == 1
|
||||
if autoReconnect {
|
||||
// Auto starts persistent. If a request fails only after this lane has
|
||||
// already completed traffic on the connection, it learns that reuse is
|
||||
// unsafe and switches itself to one request per connection.
|
||||
reconnectEvery = 0
|
||||
}
|
||||
return &requestLane{
|
||||
serverAddr: serverAddr,
|
||||
tcpBuffer: tcpBuffer,
|
||||
reconnectEvery: reconnectEvery,
|
||||
timeout: timeout,
|
||||
headerMask: headerMask,
|
||||
coverProfile: coverProfile,
|
||||
autoReconnect: autoReconnect,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *requestLane) transportFailureLocked(reused bool) {
|
||||
if l.autoReconnect && reused {
|
||||
l.reconnectEvery = 1
|
||||
}
|
||||
l.discardLocked()
|
||||
}
|
||||
|
||||
func (l *requestLane) discardLocked() {
|
||||
if l.pc != nil {
|
||||
_ = l.pc.conn.Close()
|
||||
@@ -204,6 +236,10 @@ func (l *requestLane) ensureLocked() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cover.WritePreface(conn, l.coverProfile); err != nil {
|
||||
_ = conn.Close()
|
||||
return err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, l.tcpBuffer)
|
||||
l.pc = &physicalConn{conn: conn}
|
||||
@@ -220,30 +256,38 @@ func (l *requestLane) Close() {
|
||||
func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload []byte) (byte, []byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequest(l.pc.conn, mode, sid, seq, payload); err != nil {
|
||||
l.discardLocked()
|
||||
return 0, nil, err
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequestProfileEncoding(l.pc.conn, mode, sid, seq, payload, l.headerMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
if status != wire.StatusError && len(body) > 0 && !l.coverProfile.Clear {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
|
||||
}
|
||||
return status, body, nil
|
||||
}
|
||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
||||
if err != nil {
|
||||
l.discardLocked()
|
||||
return 0, nil, err
|
||||
}
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
if status != wire.StatusError && len(body) > 0 {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, mode, seq)
|
||||
}
|
||||
return status, body, nil
|
||||
return 0, nil, fmt.Errorf("request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
// download sends one compact request and consumes up to count response records.
|
||||
@@ -252,61 +296,71 @@ func (l *requestLane) single(mode byte, sid wire.SessionID, seq uint64, payload
|
||||
func (l *requestLane) download(sid wire.SessionID, startOffset, ackOffset uint64, maxChunk, count int) ([][]byte, byte, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
timeout := l.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
payload := make([]byte, 14)
|
||||
binary.BigEndian.PutUint64(payload[0:8], ackOffset)
|
||||
binary.BigEndian.PutUint32(payload[8:12], uint32(maxChunk))
|
||||
binary.BigEndian.PutUint16(payload[12:14], uint16(count))
|
||||
if err := wire.WriteRequest(l.pc.conn, wire.ModeDownload, sid, startOffset, payload); err != nil {
|
||||
l.discardLocked()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
out := make([][]byte, 0, count)
|
||||
offset := startOffset
|
||||
lastStatus := wire.StatusOK
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponse(l.pc.conn)
|
||||
if err != nil {
|
||||
l.discardLocked()
|
||||
return out, lastStatus, err
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := l.ensureLocked(); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
lastStatus = status
|
||||
switch status {
|
||||
case wire.StatusData:
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
|
||||
if len(body) == 0 {
|
||||
reused := l.pc.requests > 0
|
||||
_ = l.pc.conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err := wire.WriteRequestProfileEncoding(l.pc.conn, wire.ModeDownload, sid, startOffset, payload, l.headerMask, l.coverProfile.Clear); err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
continue
|
||||
}
|
||||
|
||||
out := make([][]byte, 0, count)
|
||||
offset := startOffset
|
||||
lastStatus := wire.StatusOK
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponseProfile(l.pc.conn, l.headerMask)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
l.transportFailureLocked(reused)
|
||||
goto retry
|
||||
}
|
||||
lastStatus = status
|
||||
switch status {
|
||||
case wire.StatusData:
|
||||
if !l.coverProfile.Clear {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeDownload, offset)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("empty DATA response")
|
||||
}
|
||||
out = append(out, body)
|
||||
offset += uint64(len(body))
|
||||
case wire.StatusWait, wire.StatusEOF:
|
||||
i = count // stop after this response
|
||||
case wire.StatusError:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("empty DATA response")
|
||||
return out, status, fmt.Errorf("%s", string(body))
|
||||
default:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("unknown response status %d", status)
|
||||
}
|
||||
if status == wire.StatusWait || status == wire.StatusEOF {
|
||||
break
|
||||
}
|
||||
out = append(out, body)
|
||||
offset += uint64(len(body))
|
||||
case wire.StatusWait, wire.StatusEOF:
|
||||
i = count // stop after this response
|
||||
case wire.StatusError:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("%s", string(body))
|
||||
default:
|
||||
l.discardLocked()
|
||||
return out, status, fmt.Errorf("unknown response status %d", status)
|
||||
}
|
||||
if status == wire.StatusWait || status == wire.StatusEOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return out, lastStatus, nil
|
||||
l.pc.requests++
|
||||
_ = l.pc.conn.SetDeadline(time.Time{})
|
||||
l.closeAfterLocked()
|
||||
return out, lastStatus, nil
|
||||
retry:
|
||||
}
|
||||
return nil, 0, fmt.Errorf("download request failed after reconnect: %w", lastErr)
|
||||
}
|
||||
|
||||
type pathProfile struct {
|
||||
@@ -364,7 +418,7 @@ func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, cand
|
||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
||||
timeout = 2500 * time.Millisecond
|
||||
}
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout)
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 1, timeout, opts.headerMask, opts.coverProfile)
|
||||
defer lane.Close()
|
||||
seq := probeSeq.Add(1)
|
||||
|
||||
@@ -405,7 +459,7 @@ func probePersistent(serverAddr, token string, opts chunkClientOptions) bool {
|
||||
if timeout <= 0 || timeout > 2500*time.Millisecond {
|
||||
timeout = 2500 * time.Millisecond
|
||||
}
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout)
|
||||
lane := newRequestLane(serverAddr, opts.tcpBuffer, 0, timeout, opts.headerMask, opts.coverProfile)
|
||||
defer lane.Close()
|
||||
for i := 0; i < 8; i++ {
|
||||
seq := probeSeq.Add(1)
|
||||
@@ -459,7 +513,7 @@ func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte)
|
||||
}
|
||||
|
||||
func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile {
|
||||
key := fmt.Sprintf("%s|%s|%d|%d", serverAddr, token, opts.minSize, opts.maxSize)
|
||||
key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear)
|
||||
profileState.Lock()
|
||||
if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute {
|
||||
p := profileState.p
|
||||
@@ -533,6 +587,31 @@ type chunkConn struct {
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// appendChunkParts keeps the single-response fast path zero-copy. For a batch,
|
||||
// it reserves the complete size once rather than repeatedly growing and copying
|
||||
// the aggregate read buffer.
|
||||
func appendChunkParts(dst []byte, parts [][]byte) []byte {
|
||||
if len(parts) == 0 {
|
||||
return dst
|
||||
}
|
||||
if len(dst) == 0 && len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
total := len(dst)
|
||||
for _, part := range parts {
|
||||
total += len(part)
|
||||
}
|
||||
if cap(dst) < total {
|
||||
grown := make([]byte, len(dst), total)
|
||||
copy(grown, dst)
|
||||
dst = grown
|
||||
}
|
||||
for _, part := range parts {
|
||||
dst = append(dst, part...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts chunkClientOptions) (net.Conn, error) {
|
||||
if opts.minSize < 32 {
|
||||
opts.minSize = 32
|
||||
@@ -565,16 +644,21 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
opts.minPipeline = opts.maxPipeline
|
||||
}
|
||||
|
||||
profile := getPathProfile(serverAddr, token, opts)
|
||||
profile := pathProfile{
|
||||
upload: opts.minSize,
|
||||
download: opts.minSize,
|
||||
persistent: false,
|
||||
at: time.Now(),
|
||||
}
|
||||
if !opts.skipPathProbe {
|
||||
profile = getPathProfile(serverAddr, token, opts)
|
||||
}
|
||||
reconnect := opts.reconnectEvery
|
||||
// Compatibility-friendly reconnect modes:
|
||||
// 0 = persistent (CLI explicit)
|
||||
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
|
||||
// 1 = auto: start persistent, then learn one request/connection only if
|
||||
// reuse fails during real traffic
|
||||
// N>=2 = force connection rotation after N logical requests
|
||||
// Resolved silently: this runs once per proxied flow, so it must never log.
|
||||
if reconnect == 1 && profile.persistent {
|
||||
reconnect = 0
|
||||
}
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
@@ -583,7 +667,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
// OPEN rides the upload lane instead of a throwaway connection. A dedicated
|
||||
// control connection cost one extra dial per proxied flow, which shows up on
|
||||
// the server as connection churn on top of the steady-state count.
|
||||
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
||||
uploadLane := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile)
|
||||
payload, err := encodeOpen(token, targetHost, targetPort)
|
||||
if err != nil {
|
||||
uploadLane.Close()
|
||||
@@ -624,7 +708,7 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
opts: opts,
|
||||
serverMax: serverMax,
|
||||
uploadLane: uploadLane,
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout, opts.headerMask, opts.coverProfile),
|
||||
// Start at the configured ceiling. On transport failure the batch is
|
||||
// halved but never below minPipeline; successful data grows it back by
|
||||
// one. When min == max the depth is pinned and never adapts, which is
|
||||
@@ -659,8 +743,8 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
}
|
||||
|
||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
||||
c.readBuf = appendChunkParts(c.readBuf, data)
|
||||
for _, part := range data {
|
||||
c.readBuf = append(c.readBuf, part...)
|
||||
c.downloadOffset += uint64(len(part))
|
||||
}
|
||||
if len(data) > 0 {
|
||||
@@ -678,10 +762,10 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
c.pipeline = c.minPipeline
|
||||
}
|
||||
if c.opts.adaptLog && old != c.pipeline {
|
||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure\n", old, c.pipeline)
|
||||
fmt.Printf("adaptive download pipeline: %d -> %d after transport failure: %v\n", old, c.pipeline, err)
|
||||
}
|
||||
} else {
|
||||
old, next := c.downSizer.Failure(chunk)
|
||||
old, next := c.downSizer.FailureReason(chunk, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
minFailures++
|
||||
if minFailures >= 8 {
|
||||
@@ -745,7 +829,7 @@ func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
n := minInt(size, len(p))
|
||||
status, body, err := c.uploadLane.single(wire.ModeUpload, c.sid, c.upOffset, p[:n])
|
||||
if err != nil {
|
||||
old, next := c.upSizer.Failure(size)
|
||||
old, next := c.upSizer.FailureReason(size, err)
|
||||
if old == next && next == c.opts.minSize {
|
||||
minFailures++
|
||||
if minFailures >= 8 {
|
||||
|
||||
Reference in New Issue
Block a user