https://github.com/jackc/pgx/issues/2470 When the server sends multiple FATAL errors in a single batch (as PgBouncer can do when terminating idle-in-transaction connections), Pipeline.Close() must not panic with "close of closed channel" on cleanupDone. The first FATAL triggers OnPgError which close
(t *testing.T)
| 4603 | // This test sends all server responses in a single TCP write to guarantee both FATAL errors |
| 4604 | // are in the chunkReader buffer simultaneously. |
| 4605 | func TestPipelineCloseDoesNotPanicOnMultipleFatalErrors(t *testing.T) { |
| 4606 | t.Parallel() |
| 4607 | |
| 4608 | ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) |
| 4609 | defer cancel() |
| 4610 | |
| 4611 | ln, err := net.Listen("tcp", "127.0.0.1:") |
| 4612 | require.NoError(t, err) |
| 4613 | defer ln.Close() |
| 4614 | |
| 4615 | serverErrChan := make(chan error, 1) |
| 4616 | go func() { |
| 4617 | defer close(serverErrChan) |
| 4618 | |
| 4619 | conn, err := ln.Accept() |
| 4620 | if err != nil { |
| 4621 | serverErrChan <- err |
| 4622 | return |
| 4623 | } |
| 4624 | defer conn.Close() |
| 4625 | |
| 4626 | err = conn.SetDeadline(time.Now().Add(59 * time.Second)) |
| 4627 | if err != nil { |
| 4628 | serverErrChan <- err |
| 4629 | return |
| 4630 | } |
| 4631 | |
| 4632 | backend := pgproto3.NewBackend(conn, conn) |
| 4633 | |
| 4634 | // Handle startup |
| 4635 | _, err = backend.ReceiveStartupMessage() |
| 4636 | if err != nil { |
| 4637 | serverErrChan <- err |
| 4638 | return |
| 4639 | } |
| 4640 | backend.Send(&pgproto3.AuthenticationOk{}) |
| 4641 | backend.Send(&pgproto3.BackendKeyData{ProcessID: 0, SecretKey: []byte{0, 0, 0, 0}}) |
| 4642 | backend.Send(&pgproto3.ReadyForQuery{TxStatus: 'I'}) |
| 4643 | err = backend.Flush() |
| 4644 | if err != nil { |
| 4645 | serverErrChan <- err |
| 4646 | return |
| 4647 | } |
| 4648 | |
| 4649 | // Read all client pipeline messages (Parse, Describe, Parse, Describe, Sync) |
| 4650 | for i := 0; i < 5; i++ { |
| 4651 | _, err = backend.Receive() |
| 4652 | if err != nil { |
| 4653 | serverErrChan <- err |
| 4654 | return |
| 4655 | } |
| 4656 | } |
| 4657 | |
| 4658 | // Send ALL responses in a single write so they all end up in the chunkReader buffer. |
| 4659 | // This simulates PgBouncer sending a FATAL and then the real PostgreSQL also sending |
| 4660 | // a FATAL, both arriving in the same TCP segment. |
| 4661 | backend.Send(&pgproto3.ParseComplete{}) |
| 4662 | backend.Send(&pgproto3.ParameterDescription{}) |
nothing calls this directly
no test coverage detected