Mult Protocol
This commit is contained in:
@@ -571,41 +571,40 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
// 0 = persistent (CLI explicit)
|
||||
// 1 = auto: persistent when the path probe succeeds, otherwise one request/connection
|
||||
// N>=2 = force connection rotation after N logical requests
|
||||
if reconnect == 1 {
|
||||
if profile.persistent {
|
||||
reconnect = 0
|
||||
fmt.Printf("path probe: reconnect mode auto -> persistent\n")
|
||||
} else {
|
||||
fmt.Printf("path probe: reconnect mode auto -> every request\n")
|
||||
}
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
control := newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout)
|
||||
// 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)
|
||||
payload, err := encodeOpen(token, targetHost, targetPort)
|
||||
if err != nil {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
status, body, err := control.single(wire.ModeOpen, sid, 0, payload)
|
||||
status, body, err := uploadLane.single(wire.ModeOpen, sid, 0, payload)
|
||||
if err != nil {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, err
|
||||
}
|
||||
if status == wire.StatusError {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("%s", string(body))
|
||||
}
|
||||
if status != wire.StatusOK || len(body) != 4 {
|
||||
control.Close()
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("bad OPEN response")
|
||||
}
|
||||
serverMax := int(binary.BigEndian.Uint32(body))
|
||||
control.Close()
|
||||
if serverMax < opts.minSize {
|
||||
uploadLane.Close()
|
||||
return nil, fmt.Errorf("server maximum chunk %d is below client minimum %d", serverMax, opts.minSize)
|
||||
}
|
||||
if opts.maxSize > serverMax {
|
||||
@@ -624,13 +623,12 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
sid: sid,
|
||||
opts: opts,
|
||||
serverMax: serverMax,
|
||||
uploadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
uploadLane: uploadLane,
|
||||
downloadLane: newRequestLane(serverAddr, opts.tcpBuffer, reconnect, opts.txnTimeout),
|
||||
// Start at the user-configured ceiling. On transport failures the
|
||||
// pipeline is halved; successful data responses grow it back by one,
|
||||
// always staying inside minPipeline..maxPipeline. When the two bounds
|
||||
// are equal the depth is pinned and never adapts, which is what paths
|
||||
// that only work at one specific batch size need.
|
||||
// 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
|
||||
// what paths that only work at one specific batch size need.
|
||||
pipeline: opts.maxPipeline,
|
||||
minPipeline: opts.minPipeline,
|
||||
maxPipeline: opts.maxPipeline,
|
||||
@@ -640,53 +638,6 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// pinnedBatch reports whether the batch depth is fixed. A pinned depth never
|
||||
// grows or shrinks: some paths only deliver correctly at one specific number of
|
||||
// records per request, so the adaptive controller must stay out of the way.
|
||||
func (c *chunkConn) pinnedBatch() bool { return c.minPipeline >= c.maxPipeline }
|
||||
|
||||
// batchCount is how many records the next download request will ask for.
|
||||
func (c *chunkConn) batchCount(chunk int) int {
|
||||
count := c.pipeline
|
||||
if count < c.minPipeline {
|
||||
count = c.minPipeline
|
||||
}
|
||||
if count > c.maxPipeline {
|
||||
count = c.maxPipeline
|
||||
}
|
||||
// Bound each batch to roughly 1 MiB of useful data, but never below the
|
||||
// configured floor: a pinned depth is a path requirement, not a hint.
|
||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
||||
count = maxInt(maxCount, c.minPipeline)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// growPipeline widens the batch by one after a successful data response.
|
||||
func (c *chunkConn) growPipeline() {
|
||||
if c.pinnedBatch() {
|
||||
return
|
||||
}
|
||||
if c.pipeline < c.maxPipeline {
|
||||
c.pipeline++
|
||||
}
|
||||
}
|
||||
|
||||
// shrinkPipeline halves the batch after a transport failure. It reports the old
|
||||
// and new depth, and whether anything actually changed; when it returns false
|
||||
// the caller should shrink the record size instead.
|
||||
func (c *chunkConn) shrinkPipeline() (int, int, bool) {
|
||||
if c.pinnedBatch() || c.pipeline <= c.minPipeline {
|
||||
return c.pipeline, c.pipeline, false
|
||||
}
|
||||
old := c.pipeline
|
||||
c.pipeline /= 2
|
||||
if c.pipeline < c.minPipeline {
|
||||
c.pipeline = c.minPipeline
|
||||
}
|
||||
return old, c.pipeline, old != c.pipeline
|
||||
}
|
||||
|
||||
func (c *chunkConn) fillReadBuffer() error {
|
||||
if c.eof {
|
||||
return io.EOF
|
||||
@@ -694,7 +645,18 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
minFailures := 0
|
||||
for len(c.readBuf) == 0 && !c.eof {
|
||||
chunk := c.downSizer.Current()
|
||||
count := c.batchCount(chunk)
|
||||
count := c.pipeline
|
||||
if count < c.minPipeline {
|
||||
count = c.minPipeline
|
||||
}
|
||||
if count > c.maxPipeline {
|
||||
count = c.maxPipeline
|
||||
}
|
||||
// Bound each batch to roughly 1 MiB of useful data, but never below the
|
||||
// configured floor: a pinned depth is a path requirement, not a hint.
|
||||
if maxCount := (1024 * 1024) / maxInt(chunk, 1); maxCount < count {
|
||||
count = maxInt(maxCount, c.minPipeline)
|
||||
}
|
||||
|
||||
data, status, err := c.downloadLane.download(c.sid, c.downloadOffset, c.consumedOffset, chunk, count)
|
||||
for _, part := range data {
|
||||
@@ -703,16 +665,20 @@ func (c *chunkConn) fillReadBuffer() error {
|
||||
}
|
||||
if len(data) > 0 {
|
||||
c.downSizer.Success(chunk)
|
||||
c.growPipeline()
|
||||
if c.pipeline < c.maxPipeline {
|
||||
c.pipeline++
|
||||
}
|
||||
minFailures = 0
|
||||
}
|
||||
if err != nil {
|
||||
// Shrink the batch first, then the record size. When the batch is
|
||||
// pinned (min == max) the depth is left alone entirely and only the
|
||||
// record size adapts.
|
||||
if old, next, shrank := c.shrinkPipeline(); shrank {
|
||||
if c.opts.adaptLog {
|
||||
fmt.Printf("adaptive download batch: %d -> %d after transport failure\n", old, next)
|
||||
if c.pipeline > c.minPipeline {
|
||||
old := c.pipeline
|
||||
c.pipeline /= 2
|
||||
if c.pipeline < c.minPipeline {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
old, next := c.downSizer.Failure(chunk)
|
||||
@@ -808,9 +774,9 @@ func (c *chunkConn) Write(p []byte) (int, error) {
|
||||
|
||||
func (c *chunkConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
lane := newRequestLane(c.uploadLane.serverAddr, c.opts.tcpBuffer, 1, c.opts.txnTimeout)
|
||||
_, _, _ = lane.single(wire.ModeClose, c.sid, 0, nil)
|
||||
lane.Close()
|
||||
// Reuse the upload lane rather than dialling a connection just to say
|
||||
// goodbye; that was a second wasted dial per flow.
|
||||
_, _, _ = c.uploadLane.single(wire.ModeClose, c.sid, 0, nil)
|
||||
c.uploadLane.Close()
|
||||
c.downloadLane.Close()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user