diff --git a/pgxpool/pool.go b/pgxpool/pool.go index 3234c162..b7c949e5 100644 --- a/pgxpool/pool.go +++ b/pgxpool/pool.go @@ -70,6 +70,16 @@ func (cr *connResource) getPoolRows(c *Conn, r pgx.Rows) *poolRows { return pr } +// detachedCtx wraps a context and will never be canceled, regardless of if +// the wrapped one is cancelled. The Err() method will never return any errors. +type detachedCtx struct { + context.Context +} + +func (detachedCtx) Done() <-chan struct{} { return nil } +func (detachedCtx) Deadline() (time.Time, bool) { return time.Time{}, false } +func (detachedCtx) Err() error { return nil } + // Pool allows for connection reuse. type Pool struct { p *puddle.Pool @@ -195,6 +205,14 @@ func ConnectConfig(ctx context.Context, config *Config) (*Pool, error) { p.p = puddle.NewPool( func(ctx context.Context) (interface{}, error) { + // we ignore cancellation on the original context because its either from + // the health check or its from a query and we don't want to cancel creating + // a connection just because the original query was cancelled since that + // could end up stampeding the server + // this will keep any Values in the original context and will just ignore + // cancellation + // see https://github.com/jackc/pgx/issues/1259 + ctx = detachedCtx{ctx} connConfig := p.config.ConnConfig if p.beforeConnect != nil { diff --git a/pgxpool/pool_test.go b/pgxpool/pool_test.go index 1742f55d..4e712016 100644 --- a/pgxpool/pool_test.go +++ b/pgxpool/pool_test.go @@ -73,6 +73,33 @@ func TestLazyConnect(t *testing.T) { assert.Equal(t, context.Canceled, err) } +func TestConstructorIgnoresContext(t *testing.T) { + t.Parallel() + + config, err := pgxpool.ParseConfig(os.Getenv("PGX_TEST_DATABASE")) + assert.NoError(t, err) + config.LazyConnect = true + var cancel func() + config.BeforeConnect = func(context.Context, *pgx.ConnConfig) error { + // cancel the query's context before we actually Dial to ensure the Dial's + // context isn't cancelled + cancel() + return nil + } + + pool, err := pgxpool.ConnectConfig(context.Background(), config) + require.NoError(t, err) + + assert.EqualValues(t, 0, pool.Stat().TotalConns()) + + var ctx context.Context + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + _, err = pool.Exec(ctx, "SELECT 1") + assert.ErrorIs(t, err, context.Canceled) + assert.EqualValues(t, 1, pool.Stat().TotalConns()) +} + func TestConnectConfigRequiresConnConfigFromParseConfig(t *testing.T) { t.Parallel()