@@ -4704,6 +4704,153 @@ func mustEncode(buf []byte, err error) []byte {
47044704 return buf
47054705}
47064706
4707+ // https://github.com/jackc/pgx/issues/2584
4708+ //
4709+ // When a context is canceled while the server is mid-stream, asyncClose must drain the socket
4710+ // before closing it. Closing a TCP socket with unread data in the kernel receive buffer causes
4711+ // the OS to send RST instead of FIN, which surfaces on the server / proxy as "connection reset by
4712+ // peer". This test floods the client with DataRows, cancels the context, and asserts the mock
4713+ // server sees Terminate followed by a clean EOF rather than ECONNRESET.
4714+ func TestAsyncCloseDrainsBeforeClose (t * testing.T ) {
4715+ t .Parallel ()
4716+
4717+ ln , err := net .Listen ("tcp" , "127.0.0.1:" )
4718+ require .NoError (t , err )
4719+ defer ln .Close ()
4720+
4721+ type serverResult struct {
4722+ gotTerminate bool
4723+ readErr error
4724+ }
4725+ serverDone := make (chan serverResult , 1 )
4726+
4727+ go func () {
4728+ var res serverResult
4729+ defer func () { serverDone <- res }()
4730+
4731+ conn , err := ln .Accept ()
4732+ if err != nil {
4733+ res .readErr = err
4734+ return
4735+ }
4736+ defer conn .Close ()
4737+ conn .SetDeadline (time .Now ().Add (30 * time .Second ))
4738+
4739+ // asyncClose dials a second connection to deliver CancelRequest and then blocks on a Read
4740+ // until the server closes (libpq does the same). Accept it, consume the request, and close
4741+ // promptly so asyncClose proceeds to Terminate. Spawned after the primary Accept() so there
4742+ // is no race over which goroutine gets the first connection.
4743+ go func () {
4744+ c , err := ln .Accept ()
4745+ if err != nil {
4746+ return
4747+ }
4748+ defer c .Close ()
4749+ c .SetDeadline (time .Now ().Add (5 * time .Second ))
4750+ io .ReadFull (c , make ([]byte , 16 )) // len + code + pid + 4-byte secret
4751+ }()
4752+
4753+ backend := pgproto3 .NewBackend (conn , conn )
4754+
4755+ if _ , err := backend .ReceiveStartupMessage (); err != nil {
4756+ res .readErr = err
4757+ return
4758+ }
4759+ backend .Send (& pgproto3.AuthenticationOk {})
4760+ backend .Send (& pgproto3.BackendKeyData {ProcessID : 0 , SecretKey : []byte {0 , 0 , 0 , 0 }})
4761+ backend .Send (& pgproto3.ReadyForQuery {TxStatus : 'I' })
4762+ if err := backend .Flush (); err != nil {
4763+ res .readErr = err
4764+ return
4765+ }
4766+
4767+ // Read the simple Query message.
4768+ if _ , err := backend .Receive (); err != nil {
4769+ res .readErr = err
4770+ return
4771+ }
4772+
4773+ backend .Send (& pgproto3.RowDescription {Fields : []pgproto3.FieldDescription {{Name : []byte ("n" )}}})
4774+ if err := backend .Flush (); err != nil {
4775+ res .readErr = err
4776+ return
4777+ }
4778+
4779+ // Flood DataRows. Use a payload large enough that the client's kernel receive buffer holds
4780+ // unread data at the moment of cancellation; without the drain in asyncClose, the client's
4781+ // Close() would emit RST and the server's subsequent Receive() would see ECONNRESET. Stop
4782+ // when the client closes its write side or the socket buffers are full enough that we have
4783+ // blocked past the cancel point; either way fall through to read Terminate.
4784+ row := & pgproto3.DataRow {Values : [][]byte {bytes .Repeat ([]byte ("x" ), 1024 )}}
4785+ conn .SetWriteDeadline (time .Now ().Add (2 * time .Second ))
4786+ for i := 0 ; i < 16 * 1024 ; i ++ {
4787+ backend .Send (row )
4788+ if err := backend .Flush (); err != nil {
4789+ break
4790+ }
4791+ }
4792+ conn .SetWriteDeadline (time.Time {})
4793+
4794+ // The client's asyncClose sends Terminate and then drains until we close. Read until we see
4795+ // Terminate, then half-close our write side so the client's drain observes EOF and proceeds
4796+ // to Close() its end.
4797+ conn .SetReadDeadline (time .Now ().Add (20 * time .Second ))
4798+ for {
4799+ msg , err := backend .Receive ()
4800+ if err != nil {
4801+ res .readErr = err
4802+ return
4803+ }
4804+ if _ , ok := msg .(* pgproto3.Terminate ); ok {
4805+ res .gotTerminate = true
4806+ break
4807+ }
4808+ }
4809+
4810+ // Half-close: send FIN to the client so its io.Copy(io.Discard, ...) returns io.EOF, while
4811+ // keeping our read side open to observe how the client closes. A clean client close yields
4812+ // io.EOF here; an abortive close (RST due to unread data) yields ECONNRESET.
4813+ conn .(* net.TCPConn ).CloseWrite ()
4814+ buf := make ([]byte , 1 )
4815+ _ , res .readErr = conn .Read (buf )
4816+ }()
4817+
4818+ host , port , _ := strings .Cut (ln .Addr ().String (), ":" )
4819+ connStr := fmt .Sprintf ("sslmode=disable host=%s port=%s" , host , port )
4820+
4821+ conn , err := pgconn .Connect (context .Background (), connStr )
4822+ require .NoError (t , err )
4823+
4824+ queryCtx , queryCancel := context .WithCancel (context .Background ())
4825+ mrr := conn .Exec (queryCtx , "select n from generate_series(1,1000000) n" )
4826+ require .True (t , mrr .NextResult ())
4827+ rr := mrr .ResultReader ()
4828+
4829+ // Read a handful of rows so the server is definitely streaming, then cancel.
4830+ for i := 0 ; i < 50 && rr .NextRow (); i ++ {
4831+ }
4832+ queryCancel ()
4833+ for rr .NextRow () {
4834+ }
4835+ rr .Close ()
4836+ mrr .Close ()
4837+
4838+ // asyncClose runs in a background goroutine; wait for it (and the drain) to finish.
4839+ select {
4840+ case <- conn .CleanupDone ():
4841+ case <- time .After (30 * time .Second ):
4842+ t .Fatal ("timed out waiting for client cleanup" )
4843+ }
4844+
4845+ select {
4846+ case res := <- serverDone :
4847+ require .True (t , res .gotTerminate , "server never received Terminate message" )
4848+ require .ErrorIs (t , res .readErr , io .EOF , "server saw %v, expected clean EOF (FIN); RST indicates the client closed with unread data" , res .readErr )
4849+ case <- time .After (30 * time .Second ):
4850+ t .Fatal ("timed out waiting for server" )
4851+ }
4852+ }
4853+
47074854func TestDeadlineContextWatcherHandler (t * testing.T ) {
47084855 t .Parallel ()
47094856
0 commit comments