Skip to content

Commit 9ef7e5a

Browse files
StellarisWHei-z
authored andcommitted
feat: add pooled connection reuse health check (#426)
Co-authored-by: StellarisW <stellarisw@users.noreply.github.com>
1 parent c5cc5c2 commit 9ef7e5a

2 files changed

Lines changed: 601 additions & 0 deletions

File tree

‎connection_reuse_linux.go‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2022 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build linux
16+
17+
package netpoll
18+
19+
import (
20+
"runtime"
21+
"sync/atomic"
22+
"syscall"
23+
"time"
24+
)
25+
26+
// IsHealthyForReuse reports whether a dialed TCP connection has no pending
27+
// inbound data and has not been closed by its peer. ownerWaitTimeout bounds
28+
// only the wait to acquire the receive owner's FDOperator; the socket probe
29+
// itself is non-blocking.
30+
//
31+
// The check is synchronized with the netpoll receive owner. In particular, it
32+
// keeps the FDOperator ownership across both the LinkBuffer check and the
33+
// non-consuming socket probe, so a poller cannot move bytes from the kernel
34+
// receive queue into the LinkBuffer between those observations.
35+
//
36+
// It is intended for an idle client connection that is exclusively owned by a
37+
// connection pool. It does not reserve the connection for a later write and
38+
// therefore cannot prevent a peer from closing it after this method returns.
39+
func (c *TCPConnection) IsHealthyForReuse(ownerWaitTimeout time.Duration) bool {
40+
// Reject invalid, already-closing, or concurrently flushing connections
41+
// before touching their receive operator. A non-positive budget cannot
42+
// safely establish ownership and therefore fails closed.
43+
if c == nil || ownerWaitTimeout <= 0 || !c.IsActive() || !c.lock(flushing) {
44+
return false
45+
}
46+
// Keep close and flush transitions serialized for the entire observation.
47+
defer c.unlock(flushing)
48+
49+
// Close stops the flushing lock before it can free and recycle the operator.
50+
// Recheck after acquiring it so a concurrent close cannot hand us a reused
51+
// operator.
52+
if !c.IsActive() || c.operator == nil {
53+
return false
54+
}
55+
56+
// Try the uncontended owner transition first so the healthy idle fast path
57+
// avoids both a clock read and a scheduler yield.
58+
op := c.operator
59+
if !op.do() {
60+
// A poller currently owns the operator. Wait only within the caller's
61+
// budget, while continuously rejecting lifecycle transitions that make
62+
// this operator ineligible for reuse.
63+
deadline := time.Now().Add(ownerWaitTimeout)
64+
for {
65+
// Stop waiting as soon as the connection closes or the operator starts
66+
// its detach/recycle lifecycle.
67+
if !c.IsActive() || op.isUnused() || atomic.LoadInt32(&op.detached) != 0 {
68+
return false
69+
}
70+
// Check the absolute deadline before each ownership attempt so repeated
71+
// contention cannot extend the caller-provided budget.
72+
if !time.Now().Before(deadline) {
73+
return false
74+
}
75+
if op.do() {
76+
// Ownership may be acquired exactly as the deadline expires. Release
77+
// it explicitly before failing so the poller is never left blocked.
78+
if !time.Now().Before(deadline) {
79+
op.done()
80+
return false
81+
}
82+
break
83+
}
84+
// Yield instead of busy-spinning while the poller finishes its current
85+
// read/InputAck critical section.
86+
runtime.Gosched()
87+
}
88+
}
89+
// Hold receive ownership through both buffer inspection and socket probing;
90+
// every return below must hand it back to the poller.
91+
defer op.done()
92+
93+
// appendHup detaches the operator and calls OnHup asynchronously. A detached
94+
// operator is no longer eligible even before OnHup publishes closing state.
95+
if atomic.LoadInt32(&op.detached) != 0 {
96+
return false
97+
}
98+
99+
// Buffered application data means the connection is not an idle reusable
100+
// transport, even when the kernel receive queue is currently empty.
101+
if !c.IsActive() || c.inputBuffer.Len() != 0 {
102+
return false
103+
}
104+
105+
// Peek one byte directly from the socket. MSG_PEEK preserves application
106+
// data and MSG_DONTWAIT guarantees this probe never waits for network I/O.
107+
var buffer [1]byte
108+
_, _, err := syscall.Recvfrom(c.fd, buffer[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT)
109+
return c.isHealthyForReuseAfterPeek(op, err)
110+
}
111+
112+
// isHealthyForReuseAfterPeek classifies the single nonblocking socket probe
113+
// and revalidates connection state at the final reuse decision point.
114+
func (c *TCPConnection) isHealthyForReuseAfterPeek(op *FDOperator, err error) bool {
115+
if err != syscall.EAGAIN { // EWOULDBLOCK is the same errno on Linux.
116+
// Data, FIN, EINTR, and socket errors all fail closed. In particular,
117+
// EINTR is not retried so this nonblocking probe remains bounded.
118+
return false
119+
}
120+
121+
// The operator is still owned, so the LinkBuffer cannot be between readv
122+
// and InputAck while this decision is made. A concurrent user close can
123+
// detach the operator, so reject it at the decision point.
124+
return c.IsActive() && c.inputBuffer.Len() == 0 && atomic.LoadInt32(&op.detached) == 0
125+
}

0 commit comments

Comments
 (0)