Add mux-aware batching limits and burst concurrency control

This commit is contained in:
Amin.MasterkinG
2026-04-21 10:26:42 +03:30
parent 136ddef09a
commit 236ae711c3
5 changed files with 349 additions and 91 deletions
+35
View File
@@ -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.
+1
View File
@@ -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
} }
+116 -15
View File
@@ -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
+77 -7
View File
@@ -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
View File
@@ -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)