Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,11 @@ func (c *Conn) Prepare(ctx context.Context, name, sql string) (sd *pgconn.Statem
if err != nil {
var pErr *pgconn.PrepareError
if errors.As(err, &pErr) {
c.failedDescribeStatement = psKey
// The statement may have been created on the server under psName. Store psName (not psKey) so the
// deferred Deallocate sends the Close for the name the server actually knows. With psKey, the digest
// name==sql path would deallocate the SQL text, leak the statement, and permanently poison this
// connection with 42P05 on any retry of the same sql. See https://github.com/jackc/pgx/issues/2640.
c.failedDescribeStatement = psName
}
return nil, err
}
Expand Down
72 changes: 72 additions & 0 deletions conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package pgx_test
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"io"
"net"
"os"
Expand Down Expand Up @@ -532,6 +534,76 @@ func TestPrepareHandlesTimeoutBetweenParseAndDescribe(t *testing.T) {
require.NotNil(t, psd)
}

// https://github.com/jackc/pgx/issues/2640
// When name == sql the statement is created on the server under a digest name (stmt_<sha256>). The deferred cleanup of
// a failed prepare must deallocate that server-side name; dealing the SQL text instead leaks the statement and every
// retry of the same sql on this connection fails with 42P05 duplicate_prepared_statement.
func TestPrepareHandlesTimeoutBetweenParseAndDescribeWhenNameEqualsSQL(t *testing.T) {
// Not parallel because it is a timing sensitive test.

config, err := pgx.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
require.NoError(t, err)

var faultyConn *faultyconn.Conn
config.AfterNetConnect = func(ctx context.Context, config *pgconn.Config, conn net.Conn) (net.Conn, error) {
faultyConn = faultyconn.New(conn)
return faultyConn, nil
}

ctx := context.Background()
conn, err := pgx.ConnectConfig(ctx, config)
require.NoError(t, err)
defer closeConn(t, conn)
require.NotNil(t, faultyConn)

pgxtest.SkipCockroachDB(t, conn, "Induced error does not occur on CockroachDB")

_, err = conn.Exec(ctx, "set statement_timeout = '100ms'")
require.NoError(t, err)

faultyConn.HandleFrontendMessage = func(backendWriter io.Writer, msg pgproto3.FrontendMessage) error {
if _, ok := msg.(*pgproto3.Describe); ok {
time.Sleep(200 * time.Millisecond)
}
buf, err := msg.Encode(nil)
if err != nil {
return err
}
_, err = backendWriter.Write(buf)
return err
}

sql := "select $1::varchar"
digest := sha256.Sum256([]byte(sql))
psName := "stmt_" + hex.EncodeToString(digest[0:24])

psd, err := conn.Prepare(ctx, sql, sql)
var pgErr *pgconn.PgError
require.ErrorAs(t, err, &pgErr)
require.Equal(t, "57014", pgErr.Code)
require.Nil(t, psd)

faultyConn.HandleFrontendMessage = nil

_, err = conn.Exec(ctx, "set statement_timeout = default")
require.NoError(t, err)

var existsOnServer bool
err = conn.QueryRow(
ctx,
"select exists(select 1 from pg_prepared_statements where name = '"+psName+"')",
// Avoid using the prepared statement cache or it will clear the broken statement before we can check for its
// existence.
pgx.QueryExecModeExec,
).Scan(&existsOnServer)
require.NoError(t, err)
require.True(t, existsOnServer)

psd, err = conn.Prepare(ctx, sql, sql)
require.NoError(t, err)
require.NotNil(t, psd)
}

func TestPrepareBadSQLFailure(t *testing.T) {
t.Parallel()

Expand Down