[Transfer Engine] Bound TCP connection pools with FIFO backpressure#2974
[Transfer Engine] Bound TCP connection pools with FIFO backpressure#2974jacklin78911-collab wants to merge 2 commits into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
he-yufeng
left a comment
There was a problem hiding this comment.
Reviewed the connection-pool concurrency design here (I read it against #2930 rather than building the TCP transport locally, so this is a design-level review, not a runtime one).
The core shape is sound. The cap counting connections.size() + pending_connects.size() is the right call, since counting only established sockets would let many in-flight connects blow past the cap before any of them finish. FIFO waiters with driveWaitersLocked gives fair backpressure. The part I looked at hardest is callback re-entrancy, and it holds up: every acquisition callback is invoked outside state->mutex (the reject_for_shutdown path returns and calls back after the lock scope, and the normal path collects PoolActions under the lock and runs them via enqueuePoolActions after releasing it), so a callback that re-enters the pool cannot self-deadlock. Rejecting new acquisitions once shutting_down and failing queued waiters when the runtime is gone are the right deterministic-shutdown behaviors.
The one property I'd most want the added test to pin down (since it is exactly what an unbounded->bounded pool tends to get subtly wrong): under a shutdown that races in-flight connects, every entry in both pending_connects and waiters has its callback invoked exactly once, with none leaked (callback dropped) or double-invoked. A stress variant of tcp_write_visibility_test that spawns N over-cap acquisitions and calls shutdownConnectionPool() mid-connect, asserting the completion count equals N, would lock that in against future refactors.
Nice fix for #2930.
|
Thanks for working on this issue. Bounding the number of TCP connections is a valuable first step, especially for preventing FD exhaustion and excessive One concern is that the current implementation may move part of the resource pressure rather than fully eliminate it. A large transfer can still generate many acquisition callbacks, which then accumulate in It may be worth considering whether the connection lifecycle can be modeled differently. Since the number of destination peers is generally bounded, we could maintain a small set of long-lived connection lanes for each peer and let them consume work from a bounded per-peer transfer queue:
This would apply backpressure when transfer work enters the system, instead of creating one connection-acquisition callback for every slice. It would also make queue depth, waiting time, cancellation, and peer health easier to manage and observe. Overall, this PR is a useful step toward controlling connection growth. I’d just like us to ensure that the pressure is genuinely bounded rather than shifted from sockets to waiter state. |
|
@alogfans The current implementation bounds sockets, including in-flight connects, but shifts the remaining unbounded state to A bounded per-peer transfer queue consumed by a small fixed set of long-lived connection lanes looks like the cleaner end-state. Before changing the branch, could you confirm the preferred scope for this Draft PR?
If you prefer the second direction, I will first post a short design note covering the queued work-item granularity, queue-full behavior, transfer failure semantics, reconnect handling, and shutdown invariants before changing the implementation. I will also carry @he-yufeng's suggested shutdown-race stress test into either design, since the exactly-once completion property remains important. |
|
@he-yufeng Thank you for the careful review, especially for validating the re-entrancy reasoning around The exactly-once completion guarantee during a racing shutdown is the key invariant I want to preserve. With the direction discussed above—a bounded per-peer transfer queue consumed by a fixed set of long-lived lanes—I’ll adapt your suggested test to the revised design: submit N tasks beyond the queue/lane capacity, initiate shutdown while some tasks are connecting and others remain queued, and assert that the total completion count is exactly N, with no dropped or duplicate callbacks. Although the abstraction changes from acquisition callbacks to queued transfer tasks, the invariant remains the same. I’ll include both the deterministic case and a stress variant in the revised test suite. |
Summary
This PR addresses the bounded-pool and backpressure part of #2930.
The existing TCP connection pool can create an unbounded number of
connections to the same peer when no idle connection is available. This
change introduces:
The cap counts both established and in-progress connections:
Scope
This PR implements Phase A of #2930:
capacity is released;
This PR intentionally does not implement:
Design
Each peer pool contains:
PendingConnecttokens for in-progress resolve/connect attempts.When a pooled connection is requested:
a pending-connect slot is reserved and an asynchronous resolve/connect is
started.
per-peer FIFO waiter queue.
the oldest queued waiter.
Each pending-connect token records its peer, attempt ID, callback, resolver,
socket, and lifecycle stage. Resolve/connect handlers validate token identity,
stage, and shutdown state before completing the attempt.
Acquisition callbacks are never invoked while holding the connection-pool
mutex.
Waiter follow-up work is dispatched through an iterative trampoline to avoid
deep synchronous callback re-entry when completing an acquisition immediately
releases capacity or drives another waiter.
Shutdown behavior
During shutdown, the pool first marks itself as shutting down and detaches:
These objects are moved out while holding the pool mutex. The mutex is then
released before any observer or acquisition callback is invoked.
The worker shutdown ordering preserves the baseline stop-then-join pattern:
After the worker exits, pending resolvers are cancelled, sockets are closed,
and detached acquisition callbacks are completed with failure outside the
pool mutex.
This strengthens acquisition-path shutdown guarantees without introducing a
new drain guarantee for transfers that have already started.
Backpressure limitation
This phase assumes that a connection occupying a slot eventually returns or
surfaces an error.
If an established transfer makes no progress and does not produce an I/O
error, it may continue occupying a capped slot, and queued acquisitions may
continue waiting behind the cap.
Progress deadlines and stale-connection detection are intentionally left for
a follow-up phase of #2930.
Configuration
The per-peer limit is configured through:
The default value is
64.The value is parsed only when the TCP connection pool is enabled. Valid values
are from
1to65535; invalid values fall back to the default.The default is intended as an initial configurable guardrail rather than a
claim that
64is optimal for every workload. Raising the value providesbehavior closer to the previous effectively unbounded pool.
Feedback on the initial default is welcome.
Test-only instrumentation
The deterministic pending-connect shutdown test uses narrowly scoped test
hooks.
The hook registry, setters, and invocation points are compiled only when
BUILD_UNIT_TESTS=ONthrough:A production-check build with
BUILD_UNIT_TESTS=OFFcontained no hookdefinition or hook symbol.
The shutdown detachment observer is also invoked only after releasing the pool
mutex.
Tests
The test suite includes coverage for:
cap=1;Validation results:
normal test suite:
11/11passed;ASan/UBSan/LeakSanitizer:
11/11passed with no sanitizer diagnostics;TSan:
PendingConnectShutdownDetachesOwnershipAndCompletesOnce: passed;QueuedWaitersAndActiveTransfersDoNotMakeDestructionHang: passed;environment.
A separate TSan run of
CapOneReusesConnectionForQueuedValidWritesexposed a pre-existing racebetween:
and:
The write side performs an atomic counter update while the read side reads the
same counter non-atomically. Neither access site is modified by this PR, so
that issue is intentionally kept outside this patch.
Correctness boundaries
The ordinary network, cancellation, shutdown, and callback-exception paths are
designed to transfer and complete acquisition callbacks exactly once.
Allocation failure through
std::bad_allocis outside the statedexactly-once correctness contract of this PR.
Lifetime safety continues to rely on the existing Transfer Engine contract
that submission does not race destruction and that destruction is not
initiated from the TCP worker thread.
Suggested review order
The patch is easiest to review in this order:
tcp_transport.hPeerPoolPendingConnecttcp_transport.cpptcp_write_visibility_test.cppThe patch is intentionally test-heavy. Of the
+1437/-164total, roughly1000 touched lines are in
tcp_transport.h/.cpp(including replacement ofthe previous synchronous pool path); the remainder is tests and build wiring.
The main changed files are:
Feedback requested
This is currently a Draft PR. In particular, feedback would be helpful on:
phases;
64is appropriate;ready for review.