Skip to content

fix(whatsmeow): create the sqlstore container once instead of on every StartClient - #194

Open
EcoosUP wants to merge 1 commit into
evolution-foundation:mainfrom
EcoosUP:fix/shared-sqlstore-container
Open

fix(whatsmeow): create the sqlstore container once instead of on every StartClient#194
EcoosUP wants to merge 1 commit into
evolution-foundation:mainfrom
EcoosUP:fix/shared-sqlstore-container

Conversation

@EcoosUP

@EcoosUP EcoosUP commented Sep 10, 2026

Copy link
Copy Markdown

The problem

StartClient calls sqlstore.New on every invocation and never closes the previous container. There is no container.Close() anywhere in the file.

Each sqlstore.New opens its own *sql.DB. A *sql.DB that is never closed keeps its TCP connections open for the lifetime of the process — the garbage collector does not reclaim them.

This is harmless while instances stay connected. It becomes fatal when one instance loops, and a device that was logged out from the phone does exactly that:

reconnect → no session → QR → nobody scans → max QR count reached
   → forced logout → Disconnected event → reconnect → ...

Every turn of that loop leaks a whole connection pool.

Measured in production

A 500-connection Postgres, nine instances, one of them logged out from the phone at 21:06:

reconnects in 50 minutes 110
QR codes generated 1135
connections leaked per reconnect ~4.5
time to exhaust the server 44 minutes

At 21:50 the pool was gone, and from that point on every other instance that dropped its websocket failed to come back:

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

Three unrelated numbers went silent for 14 hours because of one dead instance. All 500 connections were sitting idle when we found them — nothing was using them.

The fix

The DSN is identical for every instance (w.config.PostgresAuthDB, or the same sqlite path), so there was never a reason for a container per instance. This creates it once and shares it.

After the change, same deployment: 10 StartClient calls, 4 connections in use.

Two notes on the implementation

The error is not memoized. A sync.Once would let a database that is briefly unreachable at the first attempt poison the process for its entire lifetime. A Mutex guarding a nil check retries until it succeeds, and only the success is stored.

var err error was removed from the top of StartClient because the new := declares it. No behaviour change.

Testing

Built against main (go build ./..., clean) and running in production on two separate Evolution GO processes.

Happy to split this differently or adjust naming if you prefer.

Summary by Sourcery

Reuse a single whatsmeow SQL store container across client starts to prevent connection exhaustion during reconnect loops.

Bug Fixes:

  • Prevent WhatsApp client restarts from leaking database connection pools by reusing a shared SQL store container.

Enhancements:

  • Allow shared-container initialization to retry after transient database connection failures instead of permanently caching the initial error.

…y StartClient

StartClient called sqlstore.New on every invocation and never closed the
previous container. Each sqlstore.New opens its own *sql.DB, and a *sql.DB that
is never closed keeps its TCP connections open forever.

This is harmless while instances stay up. It becomes fatal when one instance
loops: a device logged out from the phone reconnects, finds no session, emits a
QR, nobody scans it, the max QR count forces a logout, that emits Disconnected,
and the cycle restarts. Every turn leaked a whole connection pool.

Measured on a 500-connection Postgres: a single logged-out instance produced 110
reconnects in 50 minutes and exhausted the server. After that, every OTHER
instance that dropped its websocket failed to return with

    Failed to create container: pq: sorry, too many clients already

One dead instance took every other number down with it.

The DSN is identical for all instances, so a single container serves them all.
After the change: 10 StartClient calls, 4 connections in use.

The error is deliberately not memoized (Mutex, not sync.Once) so a database that
is briefly unreachable at startup does not poison the whole process.
@sourcery-ai

sourcery-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes connection-pool exhaustion by creating the whatsmeow SQL store container once and sharing it across StartClient calls, while allowing transient initialization failures to be retried.

Sequence diagram for shared whatsmeow container initialization

sequenceDiagram
    participant StartClient
    participant sharedContainer
    participant sqlstore
    participant Database

    StartClient->>sharedContainer: sharedContainer(w)
    sharedContainer->>sharedContainer: Lock sharedContainerMu
    alt sharedStore != nil
        sharedContainer-->>StartClient: sharedStore
    else sharedStore is nil
        sharedContainer->>sqlstore: New(context.Background(), driver, dsn, dbLog)
        alt initialization succeeds
            sqlstore->>Database: Open connection pool
            Database-->>sqlstore: Pool ready
            sharedContainer->>sharedContainer: sharedStore = container
            sharedContainer-->>StartClient: container
        else initialization fails
            sqlstore-->>sharedContainer: error
            sharedContainer-->>StartClient: error
            StartClient->>StartClient: Retry on a later call
        end
    end
    sharedContainer->>sharedContainer: Unlock sharedContainerMu
Loading

File-Level Changes

Change Details Files
Introduces a lazily initialized, process-wide whatsmeow SQL store container and reuses it across client starts.
  • Adds a mutex-protected shared container accessor.
  • Preserves PostgreSQL and SQLite DSN selection and debug logging during first initialization.
  • Retries initialization after failures by storing the container only after successful creation.
  • Replaces per-StartClient container creation with the shared accessor and removes the redundant error declaration.
pkg/whatsmeow/service/whatsmeow.go

Possibly linked issues

  • #Postgres connection leak: The PR directly fixes the reported leak by sharing one sqlstore container instead of creating one per reconnect.
  • #unknown: The PR directly fixes the issue by caching and reusing the sqlstore container instead of creating pools per StartClient call.
  • #PostgreSQL connection leak: a new sqlstore pool is created on instance reconnect: The PR directly fixes the issue by sharing one sqlstore container across client starts and reconnects.

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 reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. The first StartClient call fixes the process-wide database container, so if service instances use different SQLite paths or database credentials, later clients could read or write session data in the wrong database. Reverting stops further misrouting, but records written through the wrong container would remain and require cleanup or recovery.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

felipersd8 added a commit to felipersd8/evolution-go that referenced this pull request Sep 14, 2026
…y StartClient

StartClient opened a new sqlstore container — its own *sql.DB pool — on
every call and never closed the previous one. An instance that is not
paired restarts its QR loop every couple of minutes, and every restart
leaked a pool. In hours the Postgres hit max_connections and every
instance start failed with "pq: sorry, too many clients already", so no
QR code could be generated until the container was restarted.

The DSN is the same for every instance, so one shared container serves
them all. Patch from upstream PR evolution-foundation#194
(issues evolution-foundation#106, evolution-foundation#109, evolution-foundation#175, evolution-foundation#186), applied on top of 0.7.2 while upstream
has not released a fix.
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.

2 participants