Skip to content

fix: stabilize WhatsApp connections and restore paired sessions on startup - #154

Open
member3541 wants to merge 3 commits into
evolution-foundation:mainfrom
member3541:fix/connection-stability-and-startup-recovery
Open

fix: stabilize WhatsApp connections and restore paired sessions on startup#154
member3541 wants to merge 3 commits into
evolution-foundation:mainfrom
member3541:fix/connection-stability-and-startup-recovery

Conversation

@member3541

@member3541 member3541 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • Stabilize WhatsApp instance lifecycle: serialize start/restart, close sqlstore containers properly, and use reconnect backoff instead of aggressive restarts.
  • Prevent QR polling (GET /instance/qr) from restarting an already authenticated session (returns HTTP 409).
  • Restore paired instances after redeploy: default CONNECT_ON_STARTUP to rue and select instances by persisted jid instead of the transient live connected flag.

Test plan

  • Connect an instance, then call GET /instance/qr and confirm HTTP 409 without disconnect.
  • Force a transient disconnect and confirm reconnect with backoff without growing PostgreSQL connections.
  • Redeploy with CONNECT_ON_STARTUP=true and confirm paired instances restore automatically when auth store still has the device.
  • Confirm missing/expired auth sessions are skipped and require a new QR.

Summary by Sourcery

Stabilize WhatsApp instance lifecycle, introduce controlled reconnect behavior, and ensure paired sessions are restored cleanly on startup without disrupting authenticated clients.

New Features:

  • Support automatic startup of previously paired WhatsApp instances based on persisted JID instead of transient connection state.
  • Introduce backoff-based reconnection of existing WhatsApp clients on transient disconnects, avoiding creation of new sqlstore containers each time.

Bug Fixes:

  • Prevent QR polling from restarting already authenticated sessions by returning HTTP 409 when the client is logged in.
  • Serialize instance start and restart operations to avoid concurrent client lifecycles and resource races.
  • Ensure sqlstore containers are properly closed during controlled client shutdowns and restarts, preventing leaked database connections.
  • Avoid reconnect loops when the stored WhatsApp device is missing, requiring a new pairing instead.

Enhancements:

  • Refine instance shutdown flow to route stop signals through a dedicated lifecycle channel, coordinate worker goroutines, and emit a single Disconnected event without auto-restart loops.
  • Track runtime client and MyClient pointers via synchronized accessors to make runtime queries and updates safe under concurrent operations.
  • Replace aggressive instance restart-on-disconnect behavior with reuse of the existing client and store whenever possible.
  • Update presence and QR code handling to honor stop signals and cleanup, reducing stale background workers.
  • Adjust reconnect and QR flows in the instance service to use the new controlled restart logic instead of ad-hoc client recreation.

Build:

  • Default CONNECT_ON_STARTUP configuration to true so paired WhatsApp instances are restored automatically after redeploys.

Chores:

  • Add repository helpers to list all paired instances by persisted JID for startup restoration, decoupled from live Connected state.

…artup

Prevent QR polling from restarting logged-in sessions, serialize instance lifecycle to avoid connection leaks, and reconnect paired instances after redeploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sourcery-ai

sourcery-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Stabilizes WhatsApp instance lifecycle by serializing start/restart operations, introducing a controlled reconnect-with-backoff path that reuses sqlstore containers, preventing QR polling from restarting logged-in sessions, and restoring paired instances on startup based on persisted JIDs instead of transient connection state.

Sequence diagram for QR polling without restarting logged-in sessions

sequenceDiagram
    actor User
    participant InstanceHandler as instanceHandler
    participant InstanceService as instanceService
    participant WhatsmeowService as whatsmeowService

    User->>InstanceHandler: Qr
    InstanceHandler->>InstanceService: GetQr(instance)
    InstanceService->>InstanceService: client = clientPointer[instance.Id]
    alt client logged in
        InstanceService->>InstanceHandler: return ErrSessionAlreadyLoggedIn
        InstanceHandler->>User: HTTP 409 (connected=true)
    else client nil or disconnected and not logged in
        alt client is nil
            InstanceService->>WhatsmeowService: StartInstance(instance.Id)
        else client exists but disconnected
            InstanceService->>WhatsmeowService: ReconnectClient(instance.Id)
        end
        WhatsmeowService-->>InstanceService: client ready for QR
        InstanceService->>InstanceHandler: QrcodeStruct
        InstanceHandler->>User: HTTP 200 QR code
    end
Loading

File-Level Changes

Change Details Files
Introduce a synchronized runtime lifecycle to serialize instance start/restart and safely manage runtime pointers, stop signals, and sqlstore containers.
  • Add runtimeLifecycle struct with mutex-protected maps for starting instances and per-instance restart locks
  • Provide helper methods to reserve/release start, check starting state, and atomically get/set/clear client/myClient/killChannel pointers
  • Initialize lifecycle in NewWhatsmeowService and update various callers to use runtimePointers instead of direct map access
pkg/whatsmeow/service/whatsmeow.go
Refactor MyClient lifecycle management to support controlled shutdown, worker coordination, and reconnect-with-backoff using the existing WhatsApp client and sqlstore.
  • Extend MyClient with stopChannel, done channel, sync.Once guards, WaitGroup, lifecycle mutex, and flags for stopping/reconnecting
  • Add methods to markStopping/isStopping, beginReconnect/endReconnect, startWorker, requestStop, closeDone, and recoverConnection with exponential backoff while reusing WAClient and its Store
  • Ensure StartClient publishes runtime pointers atomically, manages the stop channel, closes sqlstore containers deterministically, waits for workers on shutdown, and clears caches via clearRuntimePointers
  • Change presence updates, QR code handling, and event-driven goroutines to respect mycli.done and workerWG, avoiding races and stale workers
pkg/whatsmeow/service/whatsmeow.go
Replace aggressive reconnect and restart loops with controlled reconnect behavior driven by events and guarded against terminal/logout states.
  • Update ReconnectClient to use per-instance restart locks, wait for any in-progress startup to finish, send stop signals via MyClient, and wait for StartClient cleanup before starting a fresh instance
  • Modify myEventHandler to treat LoggedOut as terminal (markStopping and requestStop), route Disconnected and StreamReplaced events through recoverConnection instead of spawning new clients, and avoid reconnect when a controlled shutdown is in progress
  • Change ad-hoc reconnect paths (e.g., for specific error IDs) to call recoverConnection instead of directly disconnecting/connecting
pkg/whatsmeow/service/whatsmeow.go
Make QR code polling and pairing safe with respect to authenticated sessions and disconnected clients, returning HTTP 409 when a session is already logged in.
  • Add ErrSessionAlreadyLoggedIn sentinel error in instance service
  • Update GetQr to avoid restarting logged-in sessions, only start/reconnect clients when they are absent or disconnected and not logged in, and propagate ErrSessionAlreadyLoggedIn when appropriate
  • Adjust Pair to use StartInstance or ReconnectClient depending on whether a client exists, maintaining the controlled lifecycle
  • Handle ErrSessionAlreadyLoggedIn in the QR HTTP handler by returning HTTP 409 with a connected flag
pkg/instance/service/instance_service.go
pkg/instance/handler/instance_handler.go
Restore paired instances automatically on startup using persisted JIDs instead of transient Connected flags, and stage startup requests through the lifecycle.
  • Extend InstanceRepository with GetAllPairedInstances and GetAllPairedInstancesByClientName, selecting rows where JID is non-empty
  • Refactor ConnectOnStartup to use the new paired-instance queries, call startInstance with autoStart=true, and stagger startups with a short sleep to avoid connection storms
  • Implement startInstance as a lifecycle-aware wrapper around StartInstance, adding AutoStart to ClientData and ensuring killChannel/stopChannel are coordinated
pkg/instance/repository/instance_repository.go
pkg/whatsmeow/service/whatsmeow.go
Adjust startup, connect, disconnect, logout, delete, and cache-clear flows in instance service to delegate lifecycle responsibilities to whatsmeowService and avoid unsafe map/channel manipulation.
  • Update Connect to distinguish between existing-but-disconnected clients and running instances, using StartInstance or ReconnectClient instead of constructing ClientData directly
  • Refactor Reconnect, Disconnect, Logout, Delete, ForceReconnect, and ClearInstanceCache to use signalStop, ClearInstanceCache, and ReconnectClient, avoiding direct killChannel closes/deletes and waiting for owners to clean up sqlstore and pointers
  • Guard cache clearing and stop signaling with startup-state checks and timeouts to avoid races with concurrent StartInstance
pkg/instance/service/instance_service.go
pkg/whatsmeow/service/whatsmeow.go
Tighten how automatic startup handles missing or unpaired sessions and improve error reporting when session stores are absent.
  • Add AutoStart flag to ClientData and use it in StartClient to skip automatic startup when the instance has no JID or when the deviceStore is missing, updating DisconnectReason accordingly
  • Ensure instances with missing/expired auth sessions are marked disconnected and require a new QR instead of attempting to auto-connect with a fresh store
pkg/whatsmeow/service/whatsmeow.go
Default CONNECT_ON_STARTUP to true so that paired WhatsApp instances are restored after redeploy when auth store data is present.
  • Change the environment default for CONNECT_ON_STARTUP from "false" to "true" in config loading
pkg/config/config.go

Possibly linked issues

  • #Postgres connection leak: each (re)connect on a logged-out instance leaks an unclosed sqlstore pool (evogo_auth/evogo_users): PR introduces lifecycle control and ensures sqlstore containers are closed, preventing Postgres leaks and stopping LoggedOut reconnect loops.
  • #(unknown): PR reuses existing clients, serializes restarts, and explicitly closes sqlstore containers, preventing idle Postgres connection leaks.

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 found 2 issues, and left some high level feedback:

  • The lifecycle coordination between stopChannel and done is quite subtle; consider adding a small helper or comment block documenting the expected ordering (requestStop → closeDone → workerWG.Wait) to make it clearer how goroutines should shut down and avoid future races or deadlocks.
  • In recoverConnection, the reconnect loop currently retries indefinitely while Store.ID is set; it might be worth adding a hard cap or observability hook (metric/counter) so operators can detect or bound long-lived reconnect loops in the presence of persistent network issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The lifecycle coordination between `stopChannel` and `done` is quite subtle; consider adding a small helper or comment block documenting the expected ordering (requestStop → closeDone → workerWG.Wait) to make it clearer how goroutines should shut down and avoid future races or deadlocks.
- In `recoverConnection`, the reconnect loop currently retries indefinitely while `Store.ID` is set; it might be worth adding a hard cap or observability hook (metric/counter) so operators can detect or bound long-lived reconnect loops in the presence of persistent network issues.

## Individual Comments

### Comment 1
<location path="pkg/instance/service/instance_service.go" line_range="167-168" />
<code_context>
 	return client, nil
 }

+func (i instances) signalStop(instanceID string) error {
+	stopChannel := i.killChannel[instanceID]
+	if stopChannel == nil {
+		return fmt.Errorf("instance stop channel not found")
</code_context>
<issue_to_address>
**issue (bug_risk):** signalStop relies on killChannel map that is no longer populated, breaking disconnect/logout paths

signalStop now uses i.killChannel, but newer flows (Connect, StartInstance, ClearInstanceCache, etc.) only manage stop channels in whatsmeowService (w.killChannel). As a result, Disconnect/Logout and other signalStop callers will frequently see "instance stop channel not found" for running instances. Please either route stop requests through whatsmeowService (e.g., a method using runtimePointers/killChannel there) or ensure i.killChannel is kept in sync with w.killChannel for all instance lifecycle operations.
</issue_to_address>

### Comment 2
<location path="pkg/instance/service/instance_service.go" line_range="719-725" />
<code_context>
+	if err := i.whatsmeowService.ReconnectClient(instanceId); err != nil {
</code_context>
<issue_to_address>
**issue (bug_risk):** ForceReconnect health check still uses local clientPointer, which may be stale after ReconnectClient

After calling whatsmeowService.ReconnectClient, the health check still uses i.clientPointer, but ReconnectClient manages state via whatsmeowService’s clientPointer/killChannel. If these differ from i.clientPointer, the check can falsely report a failed reconnect even when the new client is healthy. Consider either validating via whatsmeowService (e.g., runtimePointers) or making i.clientPointer the single, consistently updated source of truth in the same lifecycle path as whatsmeowService.
</issue_to_address>

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.

Comment thread pkg/instance/service/instance_service.go Outdated
Comment thread pkg/instance/service/instance_service.go Outdated
Route stop/health checks through whatsmeow runtime pointers, document shutdown ordering, and cap automatic reconnect attempts with periodic warning logs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@member3541

Copy link
Copy Markdown
Author

Addressed review feedback:

  • signalStop / ForceReconnect now go through whatsmeowService.RequestStop and ClientRuntimeState (lifecycle-owned maps).
  • Documented shutdown order (requestStopcloseDoneworkerWG.Wait) and extracted shutdownWorkers.
  • recoverConnection now has a hard attempt cap (120) plus periodic warn logs for long-lived reconnect loops.

Stop coordination now lives exclusively in whatsmeowService.RequestStop.

Co-authored-by: Cursor <cursoragent@cursor.com>
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