mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
Add mux-aware batching limits and burst concurrency control
This commit is contained in:
+35
@@ -195,6 +195,41 @@ MAX_BATCH_BYTES = 262144
|
|||||||
# Allowed: integer >= 1
|
# Allowed: integer >= 1
|
||||||
WORKER_COUNT = 4
|
WORKER_COUNT = 4
|
||||||
|
|
||||||
|
# MAX_CONCURRENT_BATCHES:
|
||||||
|
# Global cap for how many relay HTTP batches may be in-flight at the same time.
|
||||||
|
# Under light load the client intentionally stays at 1 active batch; when queued
|
||||||
|
# bytes reach MUX_BURST_THRESHOLD_BYTES it may expand up to this cap.
|
||||||
|
# This value must be <= WORKER_COUNT.
|
||||||
|
# Default: 4
|
||||||
|
# Allowed: integer 1..WORKER_COUNT
|
||||||
|
MAX_CONCURRENT_BATCHES = 4
|
||||||
|
|
||||||
|
# MAX_PACKETS_PER_SOCKS_PER_BATCH:
|
||||||
|
# Fairness limit per mux round. One SOCKS connection may contribute at most this
|
||||||
|
# many packets to a single HTTP batch, which prevents a hot stream from filling
|
||||||
|
# the whole batch alone.
|
||||||
|
# Default: 2
|
||||||
|
# Allowed: integer >= 1
|
||||||
|
MAX_PACKETS_PER_SOCKS_PER_BATCH = 2
|
||||||
|
|
||||||
|
# MUX_ROTATE_EVERY_BATCHES:
|
||||||
|
# Controls how often the round-robin batch start cursor moves to the next SOCKS
|
||||||
|
# connection. 1 means rotate every batch, 2 means hold the same start point for
|
||||||
|
# two batches before moving, and so on.
|
||||||
|
# Default: 1
|
||||||
|
# Allowed: integer >= 1
|
||||||
|
MUX_ROTATE_EVERY_BATCHES = 1
|
||||||
|
|
||||||
|
# MUX_BURST_THRESHOLD_BYTES:
|
||||||
|
# Total queued outbound payload bytes across all active SOCKS connections that
|
||||||
|
# triggers burst mode. Below this threshold the client behaves conservatively
|
||||||
|
# with 1 active batch and smaller effective batch shapes; at or above it, the
|
||||||
|
# client uses faster polling and may scale up to MAX_CONCURRENT_BATCHES.
|
||||||
|
# Must be >= MAX_CHUNK_SIZE.
|
||||||
|
# Default: 131072 (128 KiB)
|
||||||
|
# Allowed: integer >= MAX_CHUNK_SIZE
|
||||||
|
MUX_BURST_THRESHOLD_BYTES = 131072
|
||||||
|
|
||||||
# HTTP_REQUEST_TIMEOUT_MS:
|
# HTTP_REQUEST_TIMEOUT_MS:
|
||||||
# Timeout for a single relay HTTP request.
|
# Timeout for a single relay HTTP request.
|
||||||
# If exceeded, in-flight packets may be retried according to ACK policy.
|
# If exceeded, in-flight packets may be retried according to ACK policy.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type Client struct {
|
|||||||
workCh chan struct{}
|
workCh chan struct{}
|
||||||
|
|
||||||
lastPollUnixMS atomic.Int64
|
lastPollUnixMS atomic.Int64
|
||||||
|
activeBatches atomic.Int64
|
||||||
batchCursor atomic.Uint64
|
batchCursor atomic.Uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -46,8 +47,6 @@ func (c *Client) startSendWorkers(ctx context.Context, wg *sync.WaitGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *sendWorker) run(ctx context.Context, c *Client) {
|
func (w *sendWorker) run(ctx context.Context, c *Client) {
|
||||||
pollInterval := time.Duration(c.cfg.WorkerPollIntervalMS) * time.Millisecond
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -57,16 +56,27 @@ func (w *sendWorker) run(ctx context.Context, c *Client) {
|
|||||||
|
|
||||||
c.reclaimExpiredInFlight()
|
c.reclaimExpiredInFlight()
|
||||||
c.reclaimExpiredReorder()
|
c.reclaimExpiredReorder()
|
||||||
batch, selected := c.buildNextBatch()
|
connections := c.socksConnections.Snapshot()
|
||||||
|
totalQueuedBytes := queuedBytesAcross(connections)
|
||||||
|
waitInterval := c.effectiveWaitInterval(totalQueuedBytes)
|
||||||
|
|
||||||
|
if !c.tryAcquireBatchSlot(totalQueuedBytes) {
|
||||||
|
c.waitForSendWork(ctx, c.jitterDuration(waitInterval))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
batch, selected := c.buildNextBatch(connections, totalQueuedBytes)
|
||||||
if len(batch.Packets) == 0 {
|
if len(batch.Packets) == 0 {
|
||||||
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
|
c.releaseBatchSlot()
|
||||||
|
c.waitForSendWork(ctx, c.jitterDuration(waitInterval))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := batch.Validate(); err != nil {
|
if err := batch.Validate(); err != nil {
|
||||||
c.log.Errorf("<red>worker=<cyan>%d</cyan> invalid batch: <cyan>%v</cyan></red>", w.id, err)
|
c.log.Errorf("<red>worker=<cyan>%d</cyan> invalid batch: <cyan>%v</cyan></red>", w.id, err)
|
||||||
c.requeueSelected(selected)
|
c.requeueSelected(selected)
|
||||||
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
|
c.releaseBatchSlot()
|
||||||
|
c.waitForSendWork(ctx, c.jitterDuration(waitInterval))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +86,19 @@ func (w *sendWorker) run(ctx context.Context, c *Client) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Errorf("<red>worker=<cyan>%d</cyan> encrypt batch failed: <cyan>%v</cyan></red>", w.id, err)
|
c.log.Errorf("<red>worker=<cyan>%d</cyan> encrypt batch failed: <cyan>%v</cyan></red>", w.id, err)
|
||||||
c.requeueSelected(selected)
|
c.requeueSelected(selected)
|
||||||
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
|
c.releaseBatchSlot()
|
||||||
|
c.waitForSendWork(ctx, c.jitterDuration(waitInterval))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.postBatch(ctx, c, batch, body); err != nil {
|
if err := w.postBatch(ctx, c, batch, body); err != nil {
|
||||||
c.log.Warnf("<yellow>worker=<cyan>%d</cyan> send failed for batch=<cyan>%s</cyan>: <cyan>%v</cyan></yellow>", w.id, batch.BatchID, err)
|
c.log.Warnf("<yellow>worker=<cyan>%d</cyan> send failed for batch=<cyan>%s</cyan>: <cyan>%v</cyan></yellow>", w.id, batch.BatchID, err)
|
||||||
c.requeueSelected(selected)
|
c.requeueSelected(selected)
|
||||||
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
|
c.releaseBatchSlot()
|
||||||
|
c.waitForSendWork(ctx, c.jitterDuration(waitInterval))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
c.releaseBatchSlot()
|
||||||
|
|
||||||
c.log.Debugf(
|
c.log.Debugf(
|
||||||
"<green>worker=<cyan>%d</cyan> sent batch=<cyan>%s</cyan> packets=<cyan>%d</cyan> bytes=<cyan>%d</cyan></green>",
|
"<green>worker=<cyan>%d</cyan> sent batch=<cyan>%s</cyan> packets=<cyan>%d</cyan> bytes=<cyan>%d</cyan></green>",
|
||||||
@@ -105,20 +118,30 @@ func (c *Client) waitForSendWork(ctx context.Context, interval time.Duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
|
func (c *Client) buildNextBatch(connections []*SOCKSConnection, totalQueuedBytes int) (protocol.Batch, []dequeuedPacket) {
|
||||||
connections := c.socksConnections.Snapshot()
|
|
||||||
if len(connections) == 0 {
|
if len(connections) == 0 {
|
||||||
return protocol.Batch{}, nil
|
return protocol.Batch{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(connections, func(i, j int) bool {
|
||||||
|
return connections[i].ID < connections[j].ID
|
||||||
|
})
|
||||||
|
|
||||||
start := 0
|
start := 0
|
||||||
if len(connections) > 1 {
|
if len(connections) > 1 {
|
||||||
start = int(c.batchCursor.Add(1)-1) % len(connections)
|
rotationEvery := c.cfg.MuxRotateEveryBatches
|
||||||
|
if rotationEvery < 1 {
|
||||||
|
rotationEvery = 1
|
||||||
|
}
|
||||||
|
turn := c.batchCursor.Add(1) - 1
|
||||||
|
start = int((turn / uint64(rotationEvery)) % uint64(len(connections)))
|
||||||
}
|
}
|
||||||
maxPackets, maxBatchBytes := c.effectiveBatchLimits()
|
maxPackets, maxBatchBytes := c.effectiveBatchLimits(totalQueuedBytes)
|
||||||
|
maxPerSOCKS := c.cfg.MaxPacketsPerSOCKSPerBatch
|
||||||
|
|
||||||
selected := make([]dequeuedPacket, 0, maxPackets)
|
selected := make([]dequeuedPacket, 0, maxPackets)
|
||||||
packets := make([]protocol.Packet, 0, maxPackets)
|
packets := make([]protocol.Packet, 0, maxPackets)
|
||||||
|
selectedPerSOCKS := make(map[uint64]int, len(connections))
|
||||||
totalBytes := 0
|
totalBytes := 0
|
||||||
|
|
||||||
for len(selected) < maxPackets {
|
for len(selected) < maxPackets {
|
||||||
@@ -130,6 +153,9 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
socksConn := connections[(start+offset)%len(connections)]
|
socksConn := connections[(start+offset)%len(connections)]
|
||||||
|
if selectedPerSOCKS[socksConn.ID] >= maxPerSOCKS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
item := socksConn.DequeuePacket()
|
item := socksConn.DequeuePacket()
|
||||||
if item == nil {
|
if item == nil {
|
||||||
@@ -147,6 +173,7 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
|
|||||||
item: item,
|
item: item,
|
||||||
})
|
})
|
||||||
packets = append(packets, item.Packet)
|
packets = append(packets, item.Packet)
|
||||||
|
selectedPerSOCKS[socksConn.ID]++
|
||||||
totalBytes += packetBytes
|
totalBytes += packetBytes
|
||||||
progress = true
|
progress = true
|
||||||
}
|
}
|
||||||
@@ -157,7 +184,7 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(packets) == 0 {
|
if len(packets) == 0 {
|
||||||
if pingBatch, ok := c.buildPollBatch(connections); ok {
|
if pingBatch, ok := c.buildPollBatch(connections, totalQueuedBytes); ok {
|
||||||
return pingBatch, nil
|
return pingBatch, nil
|
||||||
}
|
}
|
||||||
return protocol.Batch{}, nil
|
return protocol.Batch{}, nil
|
||||||
@@ -167,7 +194,7 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
|
|||||||
return batch, selected
|
return batch, selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) buildPollBatch(connections []*SOCKSConnection) (protocol.Batch, bool) {
|
func (c *Client) buildPollBatch(connections []*SOCKSConnection, totalQueuedBytes int) (protocol.Batch, bool) {
|
||||||
if len(connections) == 0 {
|
if len(connections) == 0 {
|
||||||
return protocol.Batch{}, false
|
return protocol.Batch{}, false
|
||||||
}
|
}
|
||||||
@@ -175,7 +202,7 @@ func (c *Client) buildPollBatch(connections []*SOCKSConnection) (protocol.Batch,
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
nowUnixMS := now.UnixMilli()
|
nowUnixMS := now.UnixMilli()
|
||||||
lastUnixMS := c.lastPollUnixMS.Load()
|
lastUnixMS := c.lastPollUnixMS.Load()
|
||||||
minInterval := c.jitterDuration(time.Duration(c.cfg.IdlePollIntervalMS) * time.Millisecond)
|
minInterval := c.jitterDuration(c.effectiveIdlePollInterval(totalQueuedBytes))
|
||||||
if lastUnixMS > 0 && nowUnixMS-lastUnixMS < minInterval.Milliseconds() {
|
if lastUnixMS > 0 && nowUnixMS-lastUnixMS < minInterval.Milliseconds() {
|
||||||
return protocol.Batch{}, false
|
return protocol.Batch{}, false
|
||||||
}
|
}
|
||||||
@@ -190,9 +217,17 @@ func (c *Client) buildPollBatch(connections []*SOCKSConnection) (protocol.Batch,
|
|||||||
return batch, true
|
return batch, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) effectiveBatchLimits() (int, int) {
|
func (c *Client) effectiveBatchLimits(totalQueuedBytes int) (int, int) {
|
||||||
maxPackets := c.cfg.MaxPacketsPerBatch
|
maxPackets := c.cfg.MaxPacketsPerBatch
|
||||||
maxBatchBytes := c.cfg.MaxBatchBytes
|
maxBatchBytes := c.cfg.MaxBatchBytes
|
||||||
|
if totalQueuedBytes < c.cfg.MuxBurstThresholdBytes {
|
||||||
|
if reducedPackets := maxPackets / 2; reducedPackets >= 1 {
|
||||||
|
maxPackets = reducedPackets
|
||||||
|
}
|
||||||
|
if reducedBytes := maxBatchBytes / 2; reducedBytes >= c.cfg.MaxChunkSize {
|
||||||
|
maxBatchBytes = reducedBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
if !c.cfg.HTTPBatchRandomize {
|
if !c.cfg.HTTPBatchRandomize {
|
||||||
return maxPackets, maxBatchBytes
|
return maxPackets, maxBatchBytes
|
||||||
}
|
}
|
||||||
@@ -214,6 +249,72 @@ func (c *Client) effectiveBatchLimits() (int, int) {
|
|||||||
return maxPackets, maxBatchBytes
|
return maxPackets, maxBatchBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) effectiveWaitInterval(totalQueuedBytes int) time.Duration {
|
||||||
|
interval := time.Duration(c.cfg.WorkerPollIntervalMS) * time.Millisecond
|
||||||
|
if totalQueuedBytes >= c.cfg.MuxBurstThresholdBytes {
|
||||||
|
if burst := interval / 2; burst >= 25*time.Millisecond {
|
||||||
|
return burst
|
||||||
|
}
|
||||||
|
return 25 * time.Millisecond
|
||||||
|
}
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) effectiveIdlePollInterval(totalQueuedBytes int) time.Duration {
|
||||||
|
interval := time.Duration(c.cfg.IdlePollIntervalMS) * time.Millisecond
|
||||||
|
if totalQueuedBytes >= c.cfg.MuxBurstThresholdBytes {
|
||||||
|
if burst := interval / 2; burst >= time.Duration(c.cfg.WorkerPollIntervalMS)*time.Millisecond {
|
||||||
|
return burst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) effectiveConcurrentBatches(totalQueuedBytes int) int {
|
||||||
|
if totalQueuedBytes >= c.cfg.MuxBurstThresholdBytes {
|
||||||
|
return c.cfg.MaxConcurrentBatches
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) tryAcquireBatchSlot(totalQueuedBytes int) bool {
|
||||||
|
limit := c.effectiveConcurrentBatches(totalQueuedBytes)
|
||||||
|
if limit < 1 {
|
||||||
|
limit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
current := c.activeBatches.Load()
|
||||||
|
if int(current) >= limit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.activeBatches.CompareAndSwap(current, current+1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) releaseBatchSlot() {
|
||||||
|
for {
|
||||||
|
current := c.activeBatches.Load()
|
||||||
|
if current <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.activeBatches.CompareAndSwap(current, current-1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queuedBytesAcross(connections []*SOCKSConnection) int {
|
||||||
|
total := 0
|
||||||
|
for _, socksConn := range connections {
|
||||||
|
_, queuedBytes := socksConn.QueueSnapshot()
|
||||||
|
total += queuedBytes
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) jitterDuration(base time.Duration) time.Duration {
|
func (c *Client) jitterDuration(base time.Duration) time.Duration {
|
||||||
if base <= 0 || c.cfg.HTTPTimingJitterMS <= 0 {
|
if base <= 0 || c.cfg.HTTPTimingJitterMS <= 0 {
|
||||||
return base
|
return base
|
||||||
|
|||||||
@@ -151,12 +151,16 @@ func TestSOCKSConnectionInboundDataWaitsForConnectAck(t *testing.T) {
|
|||||||
|
|
||||||
func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
||||||
cfg := config.Config{
|
cfg := config.Config{
|
||||||
MaxChunkSize: 1024,
|
MaxChunkSize: 1024,
|
||||||
MaxPacketsPerBatch: 1,
|
MaxPacketsPerBatch: 1,
|
||||||
MaxBatchBytes: 4096,
|
MaxBatchBytes: 4096,
|
||||||
WorkerCount: 1,
|
WorkerCount: 1,
|
||||||
MaxQueueBytesPerSOCKS: 4096,
|
MaxConcurrentBatches: 1,
|
||||||
HTTPBatchRandomize: false,
|
MaxPacketsPerSOCKSPerBatch: 1,
|
||||||
|
MuxRotateEveryBatches: 1,
|
||||||
|
MuxBurstThresholdBytes: 1024,
|
||||||
|
MaxQueueBytesPerSOCKS: 4096,
|
||||||
|
HTTPBatchRandomize: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := New(cfg, nil)
|
client := New(cfg, nil)
|
||||||
@@ -174,7 +178,8 @@ func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
|||||||
|
|
||||||
seen := make(map[uint64]bool)
|
seen := make(map[uint64]bool)
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
batch, selected := client.buildNextBatch()
|
connections := client.socksConnections.Snapshot()
|
||||||
|
batch, selected := client.buildNextBatch(connections, queuedBytesAcross(connections))
|
||||||
if len(batch.Packets) != 1 || len(selected) != 1 {
|
if len(batch.Packets) != 1 || len(selected) != 1 {
|
||||||
t.Fatalf("iteration %d: expected one selected packet, got packets=%d selected=%d", i, len(batch.Packets), len(selected))
|
t.Fatalf("iteration %d: expected one selected packet, got packets=%d selected=%d", i, len(batch.Packets), len(selected))
|
||||||
}
|
}
|
||||||
@@ -188,3 +193,68 @@ func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
|||||||
t.Fatalf("expected all 3 socks connections to be selected once, got %d unique selections", len(seen))
|
t.Fatalf("expected all 3 socks connections to be selected once, got %d unique selections", len(seen))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildNextBatchHonorsPerSOCKSPacketLimit(t *testing.T) {
|
||||||
|
cfg := config.Config{
|
||||||
|
MaxChunkSize: 1024,
|
||||||
|
MaxPacketsPerBatch: 4,
|
||||||
|
MaxBatchBytes: 4096,
|
||||||
|
WorkerCount: 2,
|
||||||
|
MaxConcurrentBatches: 2,
|
||||||
|
MaxPacketsPerSOCKSPerBatch: 1,
|
||||||
|
MuxRotateEveryBatches: 1,
|
||||||
|
MuxBurstThresholdBytes: 1024,
|
||||||
|
MaxQueueBytesPerSOCKS: 4096,
|
||||||
|
HTTPBatchRandomize: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg, nil)
|
||||||
|
client.chunkPolicy = newChunkPolicy(cfg)
|
||||||
|
|
||||||
|
conn1 := client.socksConnections.New(client.clientSessionKey, "127.0.0.1:1001", client.chunkPolicy)
|
||||||
|
conn2 := client.socksConnections.New(client.clientSessionKey, "127.0.0.1:1002", client.chunkPolicy)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if err := conn1.EnqueuePacket(conn1.BuildSOCKSDataPacket([]byte("a"), false)); err != nil {
|
||||||
|
t.Fatalf("enqueue conn1 packet %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := conn2.EnqueuePacket(conn2.BuildSOCKSDataPacket([]byte("b"), false)); err != nil {
|
||||||
|
t.Fatalf("enqueue conn2 packet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connections := client.socksConnections.Snapshot()
|
||||||
|
batch, selected := client.buildNextBatch(connections, queuedBytesAcross(connections))
|
||||||
|
if len(batch.Packets) != 2 || len(selected) != 2 {
|
||||||
|
t.Fatalf("expected 2 selected packets, got packets=%d selected=%d", len(batch.Packets), len(selected))
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := map[uint64]int{}
|
||||||
|
for _, packet := range batch.Packets {
|
||||||
|
counts[packet.SOCKSID]++
|
||||||
|
}
|
||||||
|
if counts[conn1.ID] != 1 {
|
||||||
|
t.Fatalf("expected conn1 to contribute exactly 1 packet, got %d", counts[conn1.ID])
|
||||||
|
}
|
||||||
|
if counts[conn2.ID] != 1 {
|
||||||
|
t.Fatalf("expected conn2 to contribute exactly 1 packet, got %d", counts[conn2.ID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveConcurrentBatchesUsesBurstThreshold(t *testing.T) {
|
||||||
|
cfg := config.Config{
|
||||||
|
WorkerCount: 4,
|
||||||
|
MaxConcurrentBatches: 3,
|
||||||
|
MaxPacketsPerSOCKSPerBatch: 2,
|
||||||
|
MuxRotateEveryBatches: 1,
|
||||||
|
MuxBurstThresholdBytes: 4096,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg, nil)
|
||||||
|
if got := client.effectiveConcurrentBatches(1024); got != 1 {
|
||||||
|
t.Fatalf("expected low-load concurrency of 1, got %d", got)
|
||||||
|
}
|
||||||
|
if got := client.effectiveConcurrentBatches(4096); got != 3 {
|
||||||
|
t.Fatalf("expected burst concurrency of 3, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+120
-69
@@ -16,79 +16,87 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AESEncryptionKey string
|
AESEncryptionKey string
|
||||||
RelayURL string
|
RelayURL string
|
||||||
HTTPUserAgentsFile string
|
HTTPUserAgentsFile string
|
||||||
HTTPHeaderProfile string
|
HTTPHeaderProfile string
|
||||||
HTTPRandomizeHeaders bool
|
HTTPRandomizeHeaders bool
|
||||||
HTTPPaddingHeader string
|
HTTPPaddingHeader string
|
||||||
HTTPPaddingMinBytes int
|
HTTPPaddingMinBytes int
|
||||||
HTTPPaddingMaxBytes int
|
HTTPPaddingMaxBytes int
|
||||||
HTTPReferer string
|
HTTPReferer string
|
||||||
HTTPAcceptLanguage string
|
HTTPAcceptLanguage string
|
||||||
HTTPTimingJitterMS int
|
HTTPTimingJitterMS int
|
||||||
HTTPBatchRandomize bool
|
HTTPBatchRandomize bool
|
||||||
HTTPBatchPacketsJitter int
|
HTTPBatchPacketsJitter int
|
||||||
HTTPBatchBytesJitter int
|
HTTPBatchBytesJitter int
|
||||||
ServerHost string
|
ServerHost string
|
||||||
ServerPort int
|
ServerPort int
|
||||||
SOCKSHost string
|
SOCKSHost string
|
||||||
SOCKSPort int
|
SOCKSPort int
|
||||||
SOCKSAuth bool
|
SOCKSAuth bool
|
||||||
SOCKSUsername string
|
SOCKSUsername string
|
||||||
SOCKSPassword string
|
SOCKSPassword string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
MaxChunkSize int
|
MaxChunkSize int
|
||||||
MaxPacketsPerBatch int
|
MaxPacketsPerBatch int
|
||||||
MaxBatchBytes int
|
MaxBatchBytes int
|
||||||
WorkerCount int
|
WorkerCount int
|
||||||
HTTPRequestTimeoutMS int
|
MaxConcurrentBatches int
|
||||||
WorkerPollIntervalMS int
|
MaxPacketsPerSOCKSPerBatch int
|
||||||
IdlePollIntervalMS int
|
MuxRotateEveryBatches int
|
||||||
MaxQueueBytesPerSOCKS int
|
MuxBurstThresholdBytes int
|
||||||
AckTimeoutMS int
|
HTTPRequestTimeoutMS int
|
||||||
MaxRetryCount int
|
WorkerPollIntervalMS int
|
||||||
ReorderTimeoutMS int
|
IdlePollIntervalMS int
|
||||||
MaxReorderBufferPackets int
|
MaxQueueBytesPerSOCKS int
|
||||||
SessionIdleTimeoutMS int
|
AckTimeoutMS int
|
||||||
SOCKSIdleTimeoutMS int
|
MaxRetryCount int
|
||||||
ReadBodyLimitBytes int
|
ReorderTimeoutMS int
|
||||||
MaxServerQueueBytes int
|
MaxReorderBufferPackets int
|
||||||
|
SessionIdleTimeoutMS int
|
||||||
|
SOCKSIdleTimeoutMS int
|
||||||
|
ReadBodyLimitBytes int
|
||||||
|
MaxServerQueueBytes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string) (Config, error) {
|
func Load(path string) (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
SOCKSHost: "127.0.0.1",
|
SOCKSHost: "127.0.0.1",
|
||||||
SOCKSPort: 1080,
|
SOCKSPort: 1080,
|
||||||
HTTPUserAgentsFile: "user-agents.txt",
|
HTTPUserAgentsFile: "user-agents.txt",
|
||||||
HTTPHeaderProfile: "browser",
|
HTTPHeaderProfile: "browser",
|
||||||
HTTPRandomizeHeaders: true,
|
HTTPRandomizeHeaders: true,
|
||||||
HTTPPaddingHeader: "X-Padding",
|
HTTPPaddingHeader: "X-Padding",
|
||||||
HTTPPaddingMinBytes: 16,
|
HTTPPaddingMinBytes: 16,
|
||||||
HTTPPaddingMaxBytes: 48,
|
HTTPPaddingMaxBytes: 48,
|
||||||
HTTPTimingJitterMS: 50,
|
HTTPTimingJitterMS: 50,
|
||||||
HTTPBatchRandomize: true,
|
HTTPBatchRandomize: true,
|
||||||
HTTPBatchPacketsJitter: 4,
|
HTTPBatchPacketsJitter: 4,
|
||||||
HTTPBatchBytesJitter: 32768,
|
HTTPBatchBytesJitter: 32768,
|
||||||
ServerHost: "127.0.0.1",
|
ServerHost: "127.0.0.1",
|
||||||
ServerPort: 28080,
|
ServerPort: 28080,
|
||||||
LogLevel: "INFO",
|
LogLevel: "INFO",
|
||||||
MaxChunkSize: 16 * 1024,
|
MaxChunkSize: 16 * 1024,
|
||||||
MaxPacketsPerBatch: 32,
|
MaxPacketsPerBatch: 32,
|
||||||
MaxBatchBytes: 256 * 1024,
|
MaxBatchBytes: 256 * 1024,
|
||||||
WorkerCount: 4,
|
WorkerCount: 4,
|
||||||
HTTPRequestTimeoutMS: 15000,
|
MaxConcurrentBatches: 4,
|
||||||
WorkerPollIntervalMS: 200,
|
MaxPacketsPerSOCKSPerBatch: 2,
|
||||||
IdlePollIntervalMS: 1000,
|
MuxRotateEveryBatches: 1,
|
||||||
MaxQueueBytesPerSOCKS: 1024 * 1024,
|
MuxBurstThresholdBytes: 128 * 1024,
|
||||||
AckTimeoutMS: 5000,
|
HTTPRequestTimeoutMS: 15000,
|
||||||
MaxRetryCount: 5,
|
WorkerPollIntervalMS: 200,
|
||||||
ReorderTimeoutMS: 5000,
|
IdlePollIntervalMS: 1000,
|
||||||
MaxReorderBufferPackets: 128,
|
MaxQueueBytesPerSOCKS: 1024 * 1024,
|
||||||
SessionIdleTimeoutMS: 5 * 60 * 1000,
|
AckTimeoutMS: 5000,
|
||||||
SOCKSIdleTimeoutMS: 2 * 60 * 1000,
|
MaxRetryCount: 5,
|
||||||
ReadBodyLimitBytes: 2 * 1024 * 1024,
|
ReorderTimeoutMS: 5000,
|
||||||
MaxServerQueueBytes: 2 * 1024 * 1024,
|
MaxReorderBufferPackets: 128,
|
||||||
|
SessionIdleTimeoutMS: 5 * 60 * 1000,
|
||||||
|
SOCKSIdleTimeoutMS: 2 * 60 * 1000,
|
||||||
|
ReadBodyLimitBytes: 2 * 1024 * 1024,
|
||||||
|
MaxServerQueueBytes: 2 * 1024 * 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
@@ -235,6 +243,34 @@ func Load(path string) (Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.WorkerCount = count
|
cfg.WorkerCount = count
|
||||||
|
case "MAX_CONCURRENT_BATCHES":
|
||||||
|
count, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse MAX_CONCURRENT_BATCHES: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.MaxConcurrentBatches = count
|
||||||
|
case "MAX_PACKETS_PER_SOCKS_PER_BATCH":
|
||||||
|
count, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse MAX_PACKETS_PER_SOCKS_PER_BATCH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.MaxPacketsPerSOCKSPerBatch = count
|
||||||
|
case "MUX_ROTATE_EVERY_BATCHES":
|
||||||
|
count, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse MUX_ROTATE_EVERY_BATCHES: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.MuxRotateEveryBatches = count
|
||||||
|
case "MUX_BURST_THRESHOLD_BYTES":
|
||||||
|
size, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse MUX_BURST_THRESHOLD_BYTES: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.MuxBurstThresholdBytes = size
|
||||||
case "HTTP_REQUEST_TIMEOUT_MS":
|
case "HTTP_REQUEST_TIMEOUT_MS":
|
||||||
timeout, err := strconv.Atoi(value)
|
timeout, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -348,6 +384,21 @@ func (c Config) ValidateClient() error {
|
|||||||
if c.HTTPRequestTimeoutMS < 1 {
|
if c.HTTPRequestTimeoutMS < 1 {
|
||||||
return fmt.Errorf("invalid HTTP_REQUEST_TIMEOUT_MS: %d", c.HTTPRequestTimeoutMS)
|
return fmt.Errorf("invalid HTTP_REQUEST_TIMEOUT_MS: %d", c.HTTPRequestTimeoutMS)
|
||||||
}
|
}
|
||||||
|
if c.MaxConcurrentBatches < 1 {
|
||||||
|
return fmt.Errorf("invalid MAX_CONCURRENT_BATCHES: %d", c.MaxConcurrentBatches)
|
||||||
|
}
|
||||||
|
if c.MaxConcurrentBatches > c.WorkerCount {
|
||||||
|
return fmt.Errorf("MAX_CONCURRENT_BATCHES must be <= WORKER_COUNT")
|
||||||
|
}
|
||||||
|
if c.MaxPacketsPerSOCKSPerBatch < 1 {
|
||||||
|
return fmt.Errorf("invalid MAX_PACKETS_PER_SOCKS_PER_BATCH: %d", c.MaxPacketsPerSOCKSPerBatch)
|
||||||
|
}
|
||||||
|
if c.MuxRotateEveryBatches < 1 {
|
||||||
|
return fmt.Errorf("invalid MUX_ROTATE_EVERY_BATCHES: %d", c.MuxRotateEveryBatches)
|
||||||
|
}
|
||||||
|
if c.MuxBurstThresholdBytes < c.MaxChunkSize {
|
||||||
|
return fmt.Errorf("MUX_BURST_THRESHOLD_BYTES must be >= MAX_CHUNK_SIZE")
|
||||||
|
}
|
||||||
|
|
||||||
if c.WorkerPollIntervalMS < 1 {
|
if c.WorkerPollIntervalMS < 1 {
|
||||||
return fmt.Errorf("invalid WORKER_POLL_INTERVAL_MS: %d", c.WorkerPollIntervalMS)
|
return fmt.Errorf("invalid WORKER_POLL_INTERVAL_MS: %d", c.WorkerPollIntervalMS)
|
||||||
|
|||||||
Reference in New Issue
Block a user