This commit is contained in:
2026-07-19 16:05:37 -03:00
parent 5f43698e2b
commit 37861cda22
5 changed files with 284 additions and 47 deletions
+8 -8
View File
@@ -72,11 +72,11 @@ Contas SSH e clientes VLESS/VMess do modo nativo podem usar `data_quota_bytes` c
O runtime nativo também possui limites globais para impedir crescimento sem controle de sockets, goroutines e sessões HTTP:
- `max_concurrent_connections`: conexões de transporte TCP/TLS/WebSocket/XHTTP; padrão `4096`;
- `max_concurrent_xhttp_requests`: handlers XHTTP simultâneos; padrão `8192`;
- `xhttp_max_sessions`: sessões XHTTP ativas; padrão `4096`.
- `max_concurrent_connections`: conexões de transporte TCP/TLS/WebSocket/XHTTP; padrão `32768`;
- `max_concurrent_xhttp_requests`: mantido apenas para compatibilidade de configuração; o limite de requisições web fica desativado (`-1`) no XHTTP;
- `xhttp_max_sessions`: sessões XHTTP ativas; padrão `32768`.
Esses campos ficam em **Configurações → Xray → Native Xray scale tuning**. `0` seleciona o padrão seguro e `-1` desativa o respectivo contador de admissão, o que não é recomendado em listeners públicos; conexões HTTP/2 continuam com um limite de 256 streams simultâneos por conexão. Cada transporte Mux aceita no máximo 64 sessões filhas, com limite global padrão de 8192. Sockets WebSocket incompletos têm timeout de handshake, conexões HTTP ociosas têm timeout, e parar/reiniciar o Xray nativo fecha conexões e sessões existentes. Atualizações de tráfego e de conexões ativas são agregadas e persistidas em lote a cada cinco segundos, sem criar uma goroutine ou consulta PostgreSQL por conexão. Entradas pendentes de usuários removidos são descartadas para manter os mapas de retry limitados ao conjunto atual de contas.
Esses campos ficam em **Configurações → Xray → Native Xray scale tuning**. O XHTTP é tratado como transporte VPN: rajadas de packet-up usam backpressure cancelável e buffers de bytes limitados, sem respostas `429` nem semântica de “too many requests”. Ao atingir o teto de transporte, novos sockets permanecem no backlog do kernel em vez de serem aceitos e resetados. Conexões HTTP/2 mantêm um limite de fluxo de 1024 streams simultâneos por conexão. Cada transporte Mux aceita no máximo 64 sessões filhas, com limite global padrão de 32768. Sockets WebSocket incompletos têm timeout de handshake, conexões HTTP ociosas têm timeout, e parar/reiniciar o Xray nativo fecha conexões e sessões existentes. Atualizações de tráfego e de conexões ativas são agregadas e persistidas em lote a cada cinco segundos, sem criar uma goroutine ou consulta PostgreSQL por conexão. Entradas pendentes de usuários removidos são descartadas para manter os mapas de retry limitados ao conjunto atual de contas.
### Requisitos
@@ -613,11 +613,11 @@ SSH accounts and native-mode VLESS/VMess clients can use `data_quota_bytes` with
The native runtime also has global ceilings that prevent unbounded socket, goroutine, and HTTP-session growth:
- `max_concurrent_connections`: TCP/TLS/WebSocket/XHTTP transport connections; default `4096`;
- `max_concurrent_xhttp_requests`: simultaneous XHTTP handlers; default `8192`;
- `xhttp_max_sessions`: active XHTTP sessions; default `4096`.
- `max_concurrent_connections`: TCP/TLS/WebSocket/XHTTP transport connections; default `32768`;
- `max_concurrent_xhttp_requests`: retained for configuration compatibility; the web-request cap is disabled (`-1`) for XHTTP;
- `xhttp_max_sessions`: active XHTTP sessions; default `32768`.
These fields are available under **Settings → Xray → Native Xray scale tuning**. `0` selects the safe default and `-1` disables the corresponding admission counter, which is not recommended on public listeners; HTTP/2 connections still retain a 256-stream concurrent guard. Each Mux transport accepts at most 64 child sessions, with a default global ceiling of 8192. Incomplete WebSocket handshakes time out, idle HTTP connections time out, and stopping/restarting native Xray closes existing transports and XHTTP sessions. Traffic and active-connection changes are aggregated and written in five-second batches rather than creating a PostgreSQL query or goroutine for every connection. Pending retry entries for deleted clients are removed so retry maps stay bounded by the current account set.
These fields are available under **Settings → Xray → Native Xray scale tuning**. XHTTP is treated as VPN transport traffic: packet-up bursts use cancelable backpressure and bounded byte buffers, with no `429` or “too many requests” behavior. At the transport ceiling, new sockets remain in the kernel backlog instead of being accepted and reset. HTTP/2 connections retain a 1024-stream flow-control guard per connection. Each Mux transport accepts at most 64 child sessions, with a default global ceiling of 32768. Incomplete WebSocket handshakes time out, idle HTTP connections time out, and stopping/restarting native Xray closes existing transports and XHTTP sessions. Traffic and active-connection changes are aggregated and written in five-second batches rather than creating a PostgreSQL query or goroutine for every connection. Pending retry entries for deleted clients are removed so retry maps stay bounded by the current account set.
### Requirements
+101
View File
@@ -5,6 +5,7 @@ import (
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
@@ -211,6 +212,40 @@ func TestNativeProtocolGuardsRemainFinite(t *testing.T) {
}
}
func TestXHTTPHandlerDoesNotApplyWebRequestCeiling(t *testing.T) {
oldLimit := nativeTuneMaxXHTTPRequests.Load()
oldActive := nativeXHTTPRequests.Load()
nativeTuneMaxXHTTPRequests.Store(1)
nativeXHTTPRequests.Store(1)
defer func() {
nativeTuneMaxXHTTPRequests.Store(oldLimit)
nativeXHTTPRequests.Store(oldActive)
}()
ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest(http.MethodOptions, "/", nil)
rec := httptest.NewRecorder()
ib.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("XHTTP OPTIONS at legacy request ceiling = %d, want 200", rec.Code)
}
}
func TestLegacyXHTTPTuningMigratesToVPNDefaults(t *testing.T) {
got := normalizeNativeXrayTuning(&XrayNativeTuning{
MuxGlobalSessions: 8192,
MaxConcurrentConnections: 4096,
MaxConcurrentXHTTPRequests: 8192,
XHTTPMaxSessions: 4096,
})
if got.MuxGlobalSessions != defaultNativeMuxGlobalSessions ||
got.MaxConcurrentConnections != defaultNativeMaxConnections ||
got.MaxConcurrentXHTTPRequests != defaultNativeMaxXHTTPRequests ||
got.XHTTPMaxSessions != defaultNativeXHTTPMaxSessions {
t.Fatalf("legacy tuning was not migrated: %+v", got)
}
}
func TestXHTTPMetadataLengthIsBoundedBeforeSessionAllocation(t *testing.T) {
ib := &nativeInbound{transport: "xhttp", path: "/"}
req := httptest.NewRequest("GET", "/"+strings.Repeat("a", nativeXHTTPMaxSessionIDBytes+1), nil)
@@ -288,6 +323,72 @@ func TestXHTTPUploadQueueEnforcesPerSessionByteBudget(t *testing.T) {
}
}
func TestXHTTPUploadQueueBackpressuresInsteadOfRejectingBurst(t *testing.T) {
before := nativeXHTTPBufferedBytes.Load()
q := newNativeXHTTPUploadQueue(2, 4)
defer q.close()
first, ok := acquireNativeXHTTPMemory(4)
if !ok {
t.Fatal("failed to reserve first XHTTP payload")
}
if err := q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("one!"), Seq: 0}, first); err != nil {
first.release()
t.Fatalf("first queue push failed: %v", err)
}
first.release()
second, ok := acquireNativeXHTTPMemory(4)
if !ok {
t.Fatal("failed to reserve second XHTTP payload")
}
done := make(chan error, 1)
go func() {
done <- q.push(context.Background(), nativeXHTTPPacket{Payload: []byte("two!"), Seq: 1}, second)
}()
select {
case err := <-done:
second.release()
t.Fatalf("second burst packet did not backpressure: %v", err)
case <-time.After(25 * time.Millisecond):
}
buf := make([]byte, 4)
if n, err := q.Read(buf); err != nil || n != 4 || string(buf) != "one!" {
second.release()
t.Fatalf("first queue read = (%d, %v, %q)", n, err, string(buf))
}
select {
case err := <-done:
if err != nil {
second.release()
t.Fatalf("backpressured packet failed after space released: %v", err)
}
second.release()
case <-time.After(time.Second):
second.release()
t.Fatal("backpressured packet did not resume")
}
q.close()
if got := nativeXHTTPBufferedBytes.Load(); got != before {
t.Fatalf("backpressure test leaked %d buffered bytes (baseline %d)", got, before)
}
}
func TestXHTTPBodyReservationUsesActualContentLength(t *testing.T) {
ib := &nativeInbound{xhttpMaxEachPostBytes: 1_000_000}
req := httptest.NewRequest(http.MethodPost, "/session/0", strings.NewReader("small"))
if got := ib.xhttpUploadReservationBytes(req); got != 5 {
t.Fatalf("body reservation = %d, want actual payload length 5", got)
}
req.ContentLength = -1
if got := ib.xhttpUploadReservationBytes(req); got != 1_000_000 {
t.Fatalf("chunked body reservation = %d, want configured maximum", got)
}
}
func TestNativeQuotaResetWaitsForInFlightTraffic(t *testing.T) {
const uuid = "22222222-2222-2222-2222-222222222222"
state := &xrayNativeQuotaState{usedBytes: 123, generation: 1}
+41 -5
View File
@@ -189,7 +189,14 @@ func wrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
_ = c.Close()
return nil, false
}
return registerTrackedNativeTransportConn(c, release)
}
// registerTrackedNativeTransportConn finishes registration when the caller has
// already reserved a transport slot. Keeping reservation and Accept separate is
// what lets the production listener apply kernel/socket backpressure instead of
// accepting and immediately resetting connections at capacity.
func registerTrackedNativeTransportConn(c net.Conn, release func()) (net.Conn, bool) {
counted := &nativeCountedConn{Conn: c, release: release}
counted.onClose = func() {
nativeTransportRegistry.Lock()
@@ -208,6 +215,25 @@ func wrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
return counted, true
}
// waitWrapTrackedNativeTransportConn is used by raw native accept loops. It
// holds at most one already-accepted socket while capacity is busy, leaving the
// rest in the kernel backlog instead of creating origin-side resets/502s.
func waitWrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
if c == nil {
return nil, false
}
configureNativeTransportSocket(c)
for nativeTransportAccepting.Load() {
release, ok := acquireNativeTransportConnection()
if ok {
return registerTrackedNativeTransportConn(c, release)
}
time.Sleep(nativeOverloadBackoff)
}
_ = c.Close()
return nil, false
}
func beginNativeTransportAccepting() {
nativeTransportAccepting.Store(true)
}
@@ -229,23 +255,33 @@ func closeAllNativeTransportConnections() {
}
// nativeLimitedListener applies the same pre-authentication ceiling to XHTTP
// listeners. net/http receives only sockets that own a slot; rejected sockets
// are closed before it can allocate a per-connection goroutine or perform TLS.
// listeners. net/http receives only sockets that own a slot; when capacity is
// busy, new sockets remain in the kernel backlog until a slot becomes available.
type nativeLimitedListener struct {
net.Listener
}
func (l nativeLimitedListener) Accept() (net.Conn, error) {
for {
// Reserve before accepting. When the transport is at capacity, connections
// remain queued by the kernel rather than being accepted and reset, which is
// the behavior CDNs commonly report as an origin 502.
release, ok := acquireNativeTransportConnection()
if !ok {
time.Sleep(nativeOverloadBackoff)
continue
}
c, err := l.Listener.Accept()
if err != nil {
release()
return nil, err
}
if counted, ok := wrapTrackedNativeTransportConn(c); ok {
configureNativeTransportSocket(c)
if counted, ok := registerTrackedNativeTransportConn(c, release); ok {
return counted, nil
}
// At the ceiling, a hot accept/close loop can itself consume a CPU core.
// A short fixed backoff also lets the kernel backlog absorb brief spikes.
// Shutdown may race Accept. Registration closes the socket and releases the
// slot; the next Accept observes the listener close.
time.Sleep(nativeOverloadBackoff)
}
}
+20 -5
View File
@@ -17,17 +17,20 @@ type XrayNativeTuning struct {
const (
defaultNativeRuntimeGOMAXPROCS = 0
defaultNativeMuxGlobalSessions = 8192
defaultNativeMaxConnections = 4096
defaultNativeMaxXHTTPRequests = 8192
defaultNativeMuxGlobalSessions = 32768
defaultNativeMaxConnections = 32768
// XHTTP packet handlers are governed by HTTP/2 flow control and bounded byte
// queues, not a website-style request ceiling. A negative configured value is
// normalized to the internal unlimited representation.
defaultNativeMaxXHTTPRequests = -1
fixedNativeMuxMaxSessions = 64
fixedNativeMuxUDPIdleMS = 120000
fixedNativeMuxUDPReadBuffer = 256 * 1024
fixedNativeMuxUDPWriteBuffer = 256 * 1024
defaultNativeXHTTPMaxSessions = 4096
defaultNativeHTTP2MaxStreams = 256
defaultNativeXHTTPMaxSessions = 32768
defaultNativeHTTP2MaxStreams = 1024
// Packet-up posts are also protected by byte budgets in xray_xhttp.go. Keep
// the default reorder queue modest so thousands of unauthenticated sessions
// cannot consume large amounts of memory merely by allocating empty channel
@@ -60,6 +63,18 @@ func normalizeNativeXrayTuning(t *XrayNativeTuning) XrayNativeTuning {
t = &XrayNativeTuning{}
}
out := *t
// Migrate the two profiles written by older panel builds. Those defaults were
// sized like a web service (4K/8K sessions and a global request cap) and cause
// valid high-volume XHTTP VPN traffic to be rejected after an upgrade unless
// the persisted values are translated here.
legacySafe := out.MaxConcurrentConnections == 4096 && out.MaxConcurrentXHTTPRequests == 8192 && out.XHTTPMaxSessions == 4096
legacy2K := out.MaxConcurrentConnections == 8192 && out.MaxConcurrentXHTTPRequests == 16384 && out.XHTTPMaxSessions == 8192
if legacySafe || legacy2K {
out.MuxGlobalSessions = defaultNativeMuxGlobalSessions
out.MaxConcurrentConnections = defaultNativeMaxConnections
out.MaxConcurrentXHTTPRequests = defaultNativeMaxXHTTPRequests
out.XHTTPMaxSessions = defaultNativeXHTTPMaxSessions
}
if out.RuntimeGOMAXPROCS < 0 {
out.RuntimeGOMAXPROCS = defaultNativeRuntimeGOMAXPROCS
}
+114 -29
View File
@@ -37,6 +37,10 @@ var (
nativeXHTTPBufferedBytes atomic.Int64
nativeXHTTPBufferRejected atomic.Int64
errNativeXHTTPUploadBufferFull = errors.New("xhttp upload buffer limit reached")
nativeXHTTPMemoryWait = struct {
sync.Mutex
changed chan struct{}
}{changed: make(chan struct{})}
)
const (
@@ -295,13 +299,12 @@ func (ib *nativeInbound) xhttpServerMaxHeaderBytes() int {
// byte stream to the VLESS/VMess handlers as a net.Conn.
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer xrayRecover(fmt.Sprintf("native xray XHTTP request inbound=%q method=%s path=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.RemoteAddr))
releaseRequest, ok := acquireNativeXHTTPRequest()
if !ok {
w.Header().Set("Retry-After", "1")
http.Error(w, "native XHTTP request limit reached", http.StatusTooManyRequests)
return
}
defer releaseRequest()
// XHTTP is a VPN transport, not a web API. A single connected user keeps a
// long-lived download handler and can generate many short packet-up handlers.
// Rejecting handlers at an application request ceiling turns normal tunnel
// bursts into 429s and, through CDNs/reverse proxies, intermittent 502s.
// HTTP/2 flow control plus the bounded, cancelable upload queues below provide
// backpressure without applying website rate-limit semantics to tunnel traffic.
if !ib.isXHTTP() {
logNativePreAuthRejection("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
xhttpBadRequest(w)
@@ -558,15 +561,13 @@ func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *n
return s
}
if max := ib.xhttpMaxActiveSessions(); max > 0 && len(ib.xhttpSessions) >= max {
w.Header().Set("Retry-After", "1")
http.Error(w, "native XHTTP session limit reached", http.StatusTooManyRequests)
http.Error(w, "native XHTTP session capacity reached", http.StatusServiceUnavailable)
logNativePreAuthRejection("native xray: xhttp session rejected inbound=%q active=%d limit=%d", ib.tag, len(ib.xhttpSessions), max)
return nil
}
releaseSlot, ok := acquireNativeXHTTPSession()
if !ok {
w.Header().Set("Retry-After", "1")
http.Error(w, "native XHTTP global session limit reached", http.StatusTooManyRequests)
http.Error(w, "native XHTTP global session capacity reached", http.StatusServiceUnavailable)
return nil
}
s := &nativeXHTTPSession{
@@ -662,10 +663,14 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
http.Error(w, "bad xhttp sequence", http.StatusBadRequest)
return
}
memory, ok := acquireNativeXHTTPMemory(ib.xhttpMaxPostBytes())
if !ok {
w.Header().Set("Retry-After", "1")
http.Error(w, errNativeXHTTPUploadBufferFull.Error(), http.StatusTooManyRequests)
// Reserve the expected payload rather than the configured maximum. Normal
// XHTTP body uploads have a Content-Length, so small packets no longer each
// consume a full 1 MB reservation. Unknown/chunked or metadata-carried uploads
// still reserve the maximum before decoding to preserve the hard memory bound.
memory, err := acquireNativeXHTTPMemoryContext(r.Context(), ib.xhttpUploadReservationBytes(r))
if err != nil {
// If the client/CDN canceled while waiting for backpressure, there is no
// useful HTTP error to send. Returning also releases every reservation.
return
}
defer memory.release()
@@ -680,9 +685,14 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
if errors.Is(err, io.ErrClosedPipe) {
// A packet can race the stream-down request closing. Acknowledge the late
// upload instead of leaking an origin 500/502 into the reconnect path.
w.WriteHeader(http.StatusOK)
return
}
if errors.Is(err, errNativeXHTTPUploadBufferFull) {
w.Header().Set("Retry-After", "1")
http.Error(w, err.Error(), http.StatusTooManyRequests)
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
return
}
xrayTracef("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
@@ -797,6 +807,22 @@ func (ib *nativeInbound) xhttpMaxPostBytes() int64 {
return 1_000_000
}
// xhttpUploadReservationBytes returns a safe pre-read reservation. Body-mode
// clients normally send Content-Length, which lets thousands of small packets
// share the global budget. Header/cookie/auto and chunked bodies reserve the
// configured maximum because their decoded size is not known until parsed.
func (ib *nativeInbound) xhttpUploadReservationBytes(r *http.Request) int64 {
maxBytes := ib.xhttpMaxPostBytes()
placement := firstNonEmpty(ib.xhttpUplinkDataPlacement, xhttpPlacementBody)
if placement == xhttpPlacementBody && r.ContentLength >= 0 {
if r.ContentLength > maxBytes {
return maxBytes
}
return r.ContentLength
}
return maxBytes
}
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
defer xrayRecover(fmt.Sprintf("native xray XHTTP stream-one inbound=%q remote=%s", ib.tag, r.RemoteAddr))
xrayTracef("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
@@ -1081,6 +1107,40 @@ func acquireNativeXHTTPMemory(n int64) (*nativeXHTTPMemoryLease, bool) {
}
}
// acquireNativeXHTTPMemoryContext applies process-wide memory backpressure.
// Unlike the old fail-fast admission path, a legitimate tunnel burst waits for
// queued bytes to be consumed and remains cancelable if its HTTP request ends.
func acquireNativeXHTTPMemoryContext(ctx context.Context, n int64) (*nativeXHTTPMemoryLease, error) {
if n <= 0 {
return &nativeXHTTPMemoryLease{}, nil
}
if n > nativeXHTTPMaxBufferedGlobalBytes {
return nil, errNativeXHTTPUploadBufferFull
}
for {
current := nativeXHTTPBufferedBytes.Load()
if current <= nativeXHTTPMaxBufferedGlobalBytes-n && nativeXHTTPBufferedBytes.CompareAndSwap(current, current+n) {
return &nativeXHTTPMemoryLease{bytes: n}, nil
}
nativeXHTTPMemoryWait.Lock()
// Recheck while holding the generation lock so a release cannot happen
// between the failed check and subscribing to the notification channel.
current = nativeXHTTPBufferedBytes.Load()
if current <= nativeXHTTPMaxBufferedGlobalBytes-n {
nativeXHTTPMemoryWait.Unlock()
continue
}
changed := nativeXHTTPMemoryWait.changed
nativeXHTTPMemoryWait.Unlock()
select {
case <-changed:
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
func releaseNativeXHTTPMemory(n int64) {
if n <= 0 {
return
@@ -1092,6 +1152,10 @@ func releaseNativeXHTTPMemory(n int64) {
next = 0
}
if nativeXHTTPBufferedBytes.CompareAndSwap(current, next) {
nativeXHTTPMemoryWait.Lock()
close(nativeXHTTPMemoryWait.changed)
nativeXHTTPMemoryWait.changed = make(chan struct{})
nativeXHTTPMemoryWait.Unlock()
return
}
}
@@ -1145,6 +1209,7 @@ type nativeXHTTPUploadQueue struct {
readDeadline time.Time
bufferedBytes int64
closedFlag bool
spaceChanged chan struct{}
closed chan struct{}
closeOnce sync.Once
@@ -1165,6 +1230,7 @@ func newNativeXHTTPUploadQueue(maxPackets int, maxBytes int64) *nativeXHTTPUploa
maxPackets: maxPackets,
maxBytes: maxBytes,
closed: make(chan struct{}),
spaceChanged: make(chan struct{}),
}
}
@@ -1178,21 +1244,38 @@ func (q *nativeXHTTPUploadQueue) beginPush() bool {
return true
}
func (q *nativeXHTTPUploadQueue) adoptPayloadMemory(memory *nativeXHTTPMemoryLease, n int64) bool {
func (q *nativeXHTTPUploadQueue) adoptPayloadMemory(ctx context.Context, memory *nativeXHTTPMemoryLease, n int64) error {
if n <= 0 {
return true
return nil
}
if memory == nil || memory.bytes != n {
return false
return errNativeXHTTPUploadBufferFull
}
q.mu.Lock()
defer q.mu.Unlock()
if q.closedFlag || q.bufferedBytes > q.maxBytes-n {
return false
if n > q.maxBytes {
return errNativeXHTTPUploadBufferFull
}
for {
q.mu.Lock()
if q.closedFlag {
q.mu.Unlock()
return io.ErrClosedPipe
}
if q.bufferedBytes <= q.maxBytes-n {
q.bufferedBytes += n
memory.bytes = 0
q.mu.Unlock()
return nil
}
changed := q.spaceChanged
q.mu.Unlock()
select {
case <-changed:
case <-q.closed:
return io.ErrClosedPipe
case <-ctx.Done():
return ctx.Err()
}
}
q.bufferedBytes += n
memory.bytes = 0
return true
}
func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
@@ -1205,6 +1288,8 @@ func (q *nativeXHTTPUploadQueue) releasePayloadMemory(n int64) {
release = q.bufferedBytes
}
q.bufferedBytes -= release
close(q.spaceChanged)
q.spaceChanged = make(chan struct{})
q.mu.Unlock()
releaseNativeXHTTPMemory(release)
}
@@ -1234,8 +1319,8 @@ func (q *nativeXHTTPUploadQueue) push(ctx context.Context, p nativeXHTTPPacket,
}()
}
payloadBytes := int64(len(p.Payload))
if !q.adoptPayloadMemory(memory, payloadBytes) {
return errNativeXHTTPUploadBufferFull
if err := q.adoptPayloadMemory(ctx, memory, payloadBytes); err != nil {
return err
}
transferred := payloadBytes > 0
if transferred {