Skip to content

Conversation

@gogbog
Copy link
Contributor

@gogbog gogbog commented Dec 2, 2025

Description

This PR addresses an issue with how database connections are managed when using multiple Postgres retrievers.
Currently, each retriever creates its own dedicated connection, which leads to hundreds of open connections over time.

It resolves the problem by introducing a shared pgxpool implemented using a singleton pattern. All retrievers now reuse the same connection pool instead of creating new individual connections.

How to test:

  1. Configure multiple Postgres retrievers. (100+)
  2. Verify that only a few connections to the database are created. (the pool may create multiple)
  3. Confirm that feature flag evaluations still work as expected.
  4. Monitor Postgres to ensure connection counts stay stable.

Breaking changes:

None expected. Retrievers now use a pooled connection, but the change is internal and transparent to users.

Closes issue(s)

Resolve #4392

Checklist

  • I have implemented reconnecting logic
  • I have tested this code
  • I have added unit test to cover this code
  • I have updated the documentation (README.md and /website/docs)
  • I have followed the contributing guide

@netlify
Copy link

netlify bot commented Dec 2, 2025

Deploy Preview for go-feature-flag-doc-preview ready!

Name Link
🔨 Latest commit a0bd5ce
🔍 Latest deploy log https://app.netlify.com/projects/go-feature-flag-doc-preview/deploys/693d4cf2aa91500008d884f4
😎 Deploy Preview https://deploy-preview-4393--go-feature-flag-doc-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 94.87179% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.70%. Comparing base (7d96999) to head (a0bd5ce).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
retriever/postgresqlretriever/postgres.go 92.30% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4393      +/-   ##
==========================================
+ Coverage   85.61%   85.70%   +0.08%     
==========================================
  Files         147      148       +1     
  Lines        6131     6155      +24     
==========================================
+ Hits         5249     5275      +26     
+ Misses        655      654       -1     
+ Partials      227      226       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@thomaspoignant
Copy link
Owner

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a shared connection pool for PostgreSQL retrievers, which is a significant improvement to prevent exhausting database connections. The approach of using a singleton with reference counting is sound. However, the current implementation has a critical flaw where it assumes all retrievers use the same database URI, causing them to share a single connection pool incorrectly if they are configured with different URIs. My review includes suggestions to make the pool manager URI-aware, ensuring a separate pool is managed for each unique database connection string. I've also suggested a small improvement to the Shutdown logic for better robustness. Additionally, I've noted that the checklist items for implementing reconnecting logic and adding unit tests are unchecked. It would be beneficial to add tests for the new pooling logic, especially covering multiple URIs and database disconnection scenarios, to ensure its reliability.

Comment on lines 1 to 49
package postgresqlretriever

import (
"context"
"sync"

"github.com/jackc/pgx/v5/pgxpool"
)

var (
pool *pgxpool.Pool
mu sync.Mutex
refCount int
)

func GetPool(ctx context.Context, uri string) (*pgxpool.Pool, error) {
mu.Lock()
defer mu.Unlock()

if pool == nil {
p, err := pgxpool.New(ctx, uri)
if err != nil {
return nil, err
}
if err := p.Ping(ctx); err != nil {
p.Close()
return nil, err
}

pool = p
}

refCount++
return pool, nil
}

func ReleasePool() {
mu.Lock()
defer mu.Unlock()

refCount--
if refCount <= 0 {
if pool != nil {
pool.Close()
pool = nil
}
refCount = 0
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current singleton implementation for the connection pool doesn't support multiple database URIs. The global pool variable will be initialized once with the URI from the first retriever, and all subsequent retrievers, regardless of their configured URI, will receive this same pool. This can lead to retrievers connecting to the wrong database.

To fix this, the pool management should be URI-aware. I suggest using maps to store a connection pool and a reference count for each unique URI. This ensures that each database has its own dedicated pool, which is shared only by retrievers connecting to that same database.

package postgresqlretriever

import (
	"context"
	"sync"

	"github.com/jackc/pgx/v5/pgxpool"
)

var (
	pools     = make(map[string]*pgxpool.Pool)
	refCounts = make(map[string]int)
	mu        sync.Mutex
)

// GetPool retrieves a shared connection pool for the given URI.
// It creates a new pool if one doesn't exist for the URI, otherwise, it returns the existing one.
// It uses reference counting to manage the lifecycle of the pool.
func GetPool(ctx context.Context, uri string) (*pgxpool.Pool, error) {
	mu.Lock()
	defer mu.Unlock()

	// If a pool for this URI already exists, increment its ref count and return it.
	if pool, ok := pools[uri]; ok {
		refCounts[uri]++
		return pool, nil
	}

	// Otherwise, create a new pool for this URI.
	p, err := pgxpool.New(ctx, uri)
	if err != nil {
		return nil, err
	}
	if err := p.Ping(ctx); err != nil {
		p.Close()
		return nil, err
	}

	pools[uri] = p
	refCounts[uri] = 1
	return p, nil
}

// ReleasePool decrements the reference count for a pool.
// If the reference count drops to zero, the pool is closed and removed.
func ReleasePool(uri string) {
	mu.Lock()
	defer mu.Unlock()

	// Only proceed if the URI is being tracked.
	if _, ok := refCounts[uri]; !ok {
		return
	}

	refCounts[uri]--
	if refCounts[uri] <= 0 {
		if pool, ok := pools[uri]; ok {
			pool.Close()
			delete(pools, uri)
		}
		delete(refCounts, uri)
	}
}

Comment on lines 70 to 73
func (r *Retriever) Shutdown(ctx context.Context) error {
if r.conn == nil {
return nil
}
return r.conn.Close(ctx)
ReleasePool()
return nil
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To support the URI-aware pool manager, the Shutdown function should pass the retriever's URI to ReleasePool. This ensures that the reference count for the correct connection pool is decremented.

I've also added a nil check for r.pool to make the shutdown process more robust, preventing a call to ReleasePool if the retriever's initialization failed before the pool was assigned.

Suggested change
func (r *Retriever) Shutdown(ctx context.Context) error {
if r.conn == nil {
return nil
}
return r.conn.Close(ctx)
ReleasePool()
return nil
}
func (r *Retriever) Shutdown(ctx context.Context) error {
if r.pool != nil {
ReleasePool(r.URI)
}
return nil
}

@thomaspoignant
Copy link
Owner

thomaspoignant commented Dec 3, 2025

@gogbog thanks for starting this, Gemini said exactly what I was about to say (probably in a better way).
Your implementation is great but it will not work if you are using 2 DB (which can be the case for isolated environment for example).

I also was asking myself if we want to offer a way to opt-out from using the pool and use direct connection?

@gogbog
Copy link
Contributor Author

gogbog commented Dec 3, 2025

@thomaspoignant I totally missed that use-case. We can still make the solution support multiple pools depending on how many databases the user needs to connect to - for example:

var (
    pools    = map[string]*pgxpool.Pool{}
    refCount = map[string]int{}
    mu       sync.Mutex
)

For the opting-out from using the pool and using a direct connection instead, we can definitely add support for it, but what would be the benefits? From what I know, the pool - even if you only ever use a single connection - is not a bad choice because it still manages reconnections, backoff, lifecycle, and error recovery for free, whereas a direct connection gives you none of that.

@thomaspoignant
Copy link
Owner

Your proposal with a Map is great, this is what I had in mind too.

For the optout, forget it, you are right it will always be better to have a pool.

@gogbog
Copy link
Contributor Author

gogbog commented Dec 8, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the PostgreSQL retriever to utilize a shared connection pool (pgxpool.Pool) instead of individual pgx.Conn connections. A new file, postgres.go, was introduced to manage a global map of connection pools with reference counting, allowing multiple retrievers to share pools based on their URI. The retriever.go file was updated to integrate this new pooling mechanism for initialization, shutdown, and query execution. Additionally, the code was modernized by replacing interface{} with any for query arguments and map types. Review comments noted the use of global variables for connection pool management as a common design choice, suggested adding a context.Context to the ReleasePool function for API consistency, and praised the modernization to any types.

Comment on lines +15 to +17
var (
mu sync.Mutex
poolMap = make(map[string]*poolEntry)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of global variables (mu and poolMap) to implement the singleton pattern for the connection pool is a common approach for shared resources. However, global state can sometimes make testing more complex and introduce implicit dependencies. While acceptable for this specific use case, it's a design choice that should be considered carefully in larger architectures.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(change) Refactor PostgreSQL retriever to use shared pgxpool.Pool

2 participants