Skip to content

fix(whatsmeow): reuse shared sqlstore.Container instead of recreating it on every reconnect - #131

Open
wellpelomundo wants to merge 1 commit into
evolution-foundation:developfrom
wellpelomundo:fix/postgres-connection-leak-reconnect
Open

fix(whatsmeow): reuse shared sqlstore.Container instead of recreating it on every reconnect#131
wellpelomundo wants to merge 1 commit into
evolution-foundation:developfrom
wellpelomundo:fix/postgres-connection-leak-reconnect

Conversation

@wellpelomundo

@wellpelomundo wellpelomundo commented Jul 24, 2026

Copy link
Copy Markdown

Problem

StartClient() calls sqlstore.New(...) fresh on every reconnection
attempt — both the internal auto-reconnect retry (Client disconnected on attempt 1/2, 2/2 in the logs) and any external POST /instance/connect.
Each call opens a brand new Postgres connection pool via sql.Open. The
previous container is never closed (no container.Close() call anywhere
in the file), so every reconnect cycle leaves a whole pool (up to 25
connections, per the limits sqlstore.New already sets internally)
orphaned against Postgres.

Under frequent reconnection this exhausts max_connections even with a
high limit configured:

Failed to create container: failed to upgrade database: failed to check if version table is up to date: pq: sorry, too many clients already

Restarting only the app container doesn't clear it, since the leaked
connections live on the database side — only restarting Postgres itself
recovers, until it happens again.

Observed in production

Self-hosted, Postgres 16. This exact exhaustion pattern recurred 3 times
over two weeks. max_connections was raised 60→200 after the first
occurrence, which only delayed the recurrence, not fixed it — consistent
with a genuine per-reconnect leak rather than a capacity issue. Support
recommended idle_session_timeout + periodic restarts as an operational
mitigation (applied, helps), but doesn't address the root cause.

Fix

Cache a single shared *sqlstore.Container in a new containerPointer
map, created lazily on first use and reused by every subsequent
StartClient() call instead of recreating it. One container per process
is correct here (not one per instance) since every instance shares the
same PostgresAuthDB DSN.

Uses the same map-as-reference-type pattern already established by
clientPointer in this file, so the cache works correctly even though
StartClient (like most methods on whatsmeowService) uses a value
receiver — a plain (non-map) field would silently fail to persist across
calls. Guarded by a *sync.Mutex (pointer, not an embedded value —
go vet correctly flags an embedded sync.Mutex here as "passes lock by
value" given the value-receiver pattern throughout this file).

Testing

  • go build ./pkg/whatsmeow/... — clean
  • go vet ./pkg/whatsmeow/... — clean (no warnings, including the mutex-by-value check)
  • Not able to run the full binary/integration build locally due to an
    unrelated CGO failure in github.com/chai2010/webp on my machine, so
    only the package containing the change was verified to compile. Happy
    to run any additional checks the maintainers want, or adjust the
    approach if there's a reason the container needs to be recreated per
    attempt that isn't obvious from this file alone.

🤖 Generated with Claude Code

Summary by Sourcery

Reuse a single shared sqlstore.Container across all Whatsmeow instances and reconnect attempts to prevent database connection pool leaks on client restarts.

Enhancements:

  • Add a shared sqlstore.Container cache keyed by a constant and guarded by a mutex in whatsmeowService to avoid recreating containers on every StartClient call.
  • Initialize the new container cache and mutex in NewWhatsmeowService so all instances share the same underlying database container.
  • Tidy button click event payload construction without changing its structure or semantics.

… it on every reconnect

StartClient() called sqlstore.New(...) fresh on every reconnection attempt
(both the internal auto-reconnect "attempt 1/2, 2/2" retry and any
external POST /instance/connect), opening a brand new Postgres connection
pool each time via sql.Open. The previous container was never closed
(no container.Close() call anywhere in the file), so every reconnect
cycle left a whole pool (up to 25 connections, per the limits sqlstore.New
already sets) orphaned against Postgres. Under frequent reconnection this
exhausts max_connections even with a high limit configured, and restarting
only the app (not Postgres) doesn't clear it since the leaked connections
live on the database side.

Cache a single shared *sqlstore.Container (same PostgresAuthDB/DSN for
every instance, so one container per process is correct, not one per
instance) in a new containerPointer map, created lazily on first use and
reused by every subsequent StartClient() call. Uses the same
map-as-reference-type pattern already established by clientPointer, so it
works correctly even though StartClient (like most methods here) uses a
value receiver. Guarded by a *sync.Mutex (pointer, not embedded value —
go vet correctly flags an embedded sync.Mutex here as "passes lock by
value" given the value-receiver pattern).

Verified: `go build ./pkg/whatsmeow/...` and `go vet ./pkg/whatsmeow/...`
both clean. Not able to run the full binary/integration build locally
(unrelated CGO failure in github.com/chai2010/webp on this machine), so
only the package containing the change was verified to compile — happy to
run any additional checks the maintainers want.

Observed in production (self-hosted, Postgres 16): this same
`too many clients already` exhaustion pattern recurred 3 times over two
weeks (max_connections raised 60->200 after the first occurrence, which
only delayed recurrence). Support recommended idle_session_timeout +
periodic restarts as an operational mitigation, which we've applied, but
this fix addresses the actual leak at its source.
@sourcery-ai

sourcery-ai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Reuses a single shared sqlstore.Container across all whatsmeow instances and reconnect attempts to prevent leaking database connection pools, by adding a lazily initialized, mutex-guarded container cache on the service struct and refactoring StartClient to obtain the container from this cache instead of creating a new one each time, plus a minor JSON payload formatting cleanup.

Sequence diagram for StartClient container reuse and lazy initialization

sequenceDiagram
    participant whatsmeowService
    participant containerPointer
    participant sqlstore
    participant Postgres

    whatsmeowService->>containerPointer: read [sharedContainerKey]
    alt container exists
        containerPointer-->>whatsmeowService: *sqlstore.Container
    else container missing
        whatsmeowService->>whatsmeowService: containerInitMu.Lock()
        whatsmeowService->>containerPointer: read [sharedContainerKey] (double check)
        alt still missing
            alt config.WaDebug != ""
                whatsmeowService->>sqlstore: waLog.Stdout("Database", config.WaDebug, true)
            end
            alt config.PostgresAuthDB != ""
                whatsmeowService->>sqlstore: sqlstore.New(context.Background(), "postgres", config.PostgresAuthDB, dbLog)
                sqlstore->>Postgres: sql.Open("postgres", PostgresAuthDB)
            else sqlite fallback
                whatsmeowService->>sqlstore: sqlstore.New(context.Background(), "sqlite", dsn, dbLog)
            end
            whatsmeowService->>containerPointer: set [sharedContainerKey] = *sqlstore.Container
        end
        whatsmeowService->>whatsmeowService: containerInitMu.Unlock()
    end

    whatsmeowService-->>whatsmeowService: use shared *sqlstore.Container for client start
Loading

File-Level Changes

Change Details Files
Introduce a shared sqlstore.Container cache and its synchronization primitives on whatsmeowService.
  • Add a sharedContainerKey constant used as the sole key for the shared container cache.
  • Extend whatsmeowService with a containerPointer map keyed by string to *sqlstore.Container to hold the shared container instance.
  • Add a *sync.Mutex field (containerInitMu) to coordinate one-time container initialization across concurrent StartClient calls.
  • Initialize the new containerPointer map and containerInitMu in NewWhatsmeowService.
pkg/whatsmeow/service/whatsmeow.go
Refactor StartClient to reuse a lazily initialized shared sqlstore.Container instead of creating a new one on each call.
  • Replace the per-call construction of a new sqlstore.Container with lookup in containerPointer using sharedContainerKey.
  • Guard first-time container creation with containerInitMu, including double-checked locking to avoid duplicate creation under contention.
  • Unify logger setup so dbLog is only constructed when WaDebug is set and reused for either postgres or sqlite container creation.
  • On successful container creation, store it in containerPointer for reuse; preserve existing error handling flow based on the shared err variable.
pkg/whatsmeow/service/whatsmeow.go
Minor cleanup to button click event payload formatting.
  • Realign and reformat the buttonClickMap data fields without changing semantics or structure of the JSON payload.
pkg/whatsmeow/service/whatsmeow.go

Possibly linked issues

  • #(not provided): PR implements a shared sqlstore.Container reused across reconnects, fixing the Postgres connection leak described in the issue.
  • #Postgres connection leak: each (re)connect on a logged-out instance leaks an unclosed sqlstore pool (evogo_auth/evogo_users): The PR reuses one sqlstore.Container instead of recreating per reconnect, directly fixing the reported Postgres pool leak.
  • #: PR reuses a shared sqlstore.Container instead of recreating it per reconnect, fixing the Postgres connection leak reported.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • Access to containerPointer in StartClient is currently racy (read outside the mutex, write inside); consider wrapping both the initial read and subsequent write in the same lock or using sync.Once/atomic pointer to avoid data races under concurrent reconnects.
  • Since you only ever use a single sharedContainerKey, a map may be unnecessary; replacing containerPointer with a single *sqlstore.Container field (initialized once with proper synchronization) would simplify the implementation and make the intent clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Access to containerPointer in StartClient is currently racy (read outside the mutex, write inside); consider wrapping both the initial read and subsequent write in the same lock or using sync.Once/atomic pointer to avoid data races under concurrent reconnects.
- Since you only ever use a single sharedContainerKey, a map may be unnecessary; replacing containerPointer with a single *sqlstore.Container field (initialized once with proper synchronization) would simplify the implementation and make the intent clearer.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

1 participant