Skip to content

fix(reload): prevent duplicate/destructive reload during transitions#129

Open
ViperTecCorporation wants to merge 4 commits intoclairton:mainfrom
ViperTecCorporation:fix/reload-transition-guard
Open

fix(reload): prevent duplicate/destructive reload during transitions#129
ViperTecCorporation wants to merge 4 commits intoclairton:mainfrom
ViperTecCorporation:fix/reload-transition-guard

Conversation

@ViperTecCorporation
Copy link
Contributor

@ViperTecCorporation ViperTecCorporation commented Feb 16, 2026

fix(reload): prevenir reload duplicado/destrutivo durante transições de conexão

Problema

  • Eventos de reload em sequência podiam executar múltiplos reloads para o mesmo número.
  • A sessão podia ser desconectada enquanto ainda estava em estado transitório (connecting).
  • O fluxo forçava status online -> disconnected durante o reload, gerando instabilidade.
  • O publish simultâneo em broker + bridge aumentava chance de reload duplicado em ambiente distribuído.

Solução

  • ReloadBaileys:

    • adiciona lock por telefone (inFlightByPhone) para bloquear execução concorrente.
    • adiciona debounce por telefone (RELOAD_BAILEYS_DEBOUNCE_MS, default 15000ms).
    • antes de desconectar, verifica estados transitórios:
      • isStatusConnecting(phone)
      • isStatusRestartRequired(phone)
    • se estiver em transição, pula reload destrutivo com log de proteção.
    • remove transição forçada de status (online -> disconnected) durante o reload.
    • garante liberação do lock em bloco finally.
  • ReloadAmqp:

    • torna publish no broker opcional via RELOAD_PUBLISH_BROKER (default false).
    • mantém publish no bridge para o servidor alvo.
  • defaults:

    • adiciona RELOAD_PUBLISH_BROKER.
    • adiciona RELOAD_BAILEYS_DEBOUNCE_MS.

Resultado

  • Reduz reload em cascata e reconexões desnecessárias.
  • Evita desconexão de sessão em estados transitórios.
  • Melhora previsibilidade/segurança do reload em cenários com múltiplas instâncias.

Summary by CodeRabbit

  • Configuration

    • Added environment variables to control reload broker publishing and debouncing behavior.
  • New Features

    • Added forced logout capability during deregistration.
  • Improvements

    • Implemented debouncing to prevent duplicate reload requests per phone.
    • Enhanced status checking with finer-grained pre-checks before reload operations.
    • Made broker publish conditional based on configuration settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Warning

Rate limit exceeded

@ViperTecCorporation has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds two environment-driven defaults, makes AMQP broker publish conditional, implements per-phone debouncing and in-flight tracking for Baileys reloads, and introduces an optional forced logout flag propagated through logout interfaces, AMQP, job, and controller layers.

Changes

Cohort / File(s) Summary
Configuration Constants
src/defaults.ts
Add RELOAD_PUBLISH_BROKER: boolean and RELOAD_BAILEYS_DEBOUNCE_MS: number exported from defaults, sourced from env vars.
Reload AMQP
src/services/reload_amqp.ts
Import RELOAD_PUBLISH_BROKER and conditionally publish to broker based on this flag; bridge publish remains unconditional.
Reload Baileys
src/services/reload_baileys.ts
Add per-phone inFlightByPhone & lastRunAtByPhone tracking, debounce using RELOAD_BAILEYS_DEBOUNCE_MS, transitional-state checks via sessionStore, and ensure in-flight cleanup with try/finally.
Logout API & Types
src/services/logout.ts
Add LogoutOptions type with optional force?: boolean; change Logout.run signature to accept optional options?: LogoutOptions.
Logout Job & Controller
src/jobs/logout.ts, src/controllers/registration_controller.ts
Propagate optional force through Logout job consumer and call logout with { force }; registration deregister now calls logout with { force: true }.
Logout AMQP & Baileys Implementations
src/services/logout_amqp.ts, src/services/logout_baileys.ts
Update signatures to accept options?: LogoutOptions; AMQP payload now includes force; Baileys logout honors options.force, conditionally invokes client logout and continues cleanup with error handling.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Debounce as Debounce/Tracking
    participant SessionStore
    participant BaileysSvc as Baileys Service
    participant CurrentClient

    Client->>Debounce: Reload request (phone)
    Debounce->>Debounce: Check inFlight / lastRunAt (debounce)
    alt Recently in-flight or within debounce window
        Debounce-->>Client: Skip reload
    else Proceed
        Debounce->>Debounce: Mark in-flight
        Debounce->>SessionStore: Query isConnecting / isRestartRequired
        alt Transitional state
            Debounce-->>Client: Skip (transitional)
            Debounce->>Debounce: Clear in-flight
        else Safe to proceed
            Debounce->>SessionStore: Query isOnline / isStandBy
            SessionStore-->>Debounce: Status
            Debounce->>CurrentClient: Disconnect if needed
            BaileysSvc->>BaileysSvc: Perform reload
            Debounce->>Debounce: Clear in-flight (finally)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I nibbled configs beneath the moonlight glow,
Debounced each hop so reloads move slow,
Broker listens only when the flag is true,
Forced goodbyes sent when the job must do,
A rabbit cheers—no duplicate show! 🥕

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing duplicate/destructive reload during connection transitions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/services/reload_baileys.ts`:
- Around line 35-71: Indent the entire body of the try block one level deeper to
improve readability: update the block that starts with try { ... } in the
ReloadBaileys.run flow (including calls to this.getConfig, super.run,
this.getClient, await config.getStore, usage of
sessionStore.isStatusConnecting/isStatusRestartRequired/isStatusOnline/isStatusStandBy,
currentClient.disconnect, the second this.getClient call, logger.info and the
finally ReloadBaileys.inFlightByPhone.delete(phone)) so all statements inside
try are consistently nested under try; ensure closing braces and the finally
remain aligned correctly with the try.
- Around line 10-11: The Map lastRunAtByPhone grows without bounds; add pruning
to avoid stale entries by implementing a cleanup policy: either cap size with an
LRU eviction or periodically remove entries older than a configurable TTL.
Concretely, add a method (e.g., pruneLastRunAtEntries) on the same class that
iterates lastRunAtByPhone and deletes keys with timestamp < Date.now() - TTL,
and invoke it regularly (setInterval) or call it opportunistically from the code
paths that update lastRunAtByPhone (where inFlightByPhone and lastRunAtByPhone
are used) so stale phone entries are removed; make TTL and interval configurable
constants and reference lastRunAtByPhone and inFlightByPhone in the
implementation.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/services/reload_baileys.ts`:
- Around line 35-71: Indent the entire body of the try block one level deeper to
improve readability: update the block that starts with try { ... } in the
ReloadBaileys.run flow (including calls to this.getConfig, super.run,
this.getClient, await config.getStore, usage of
sessionStore.isStatusConnecting/isStatusRestartRequired/isStatusOnline/isStatusStandBy,
currentClient.disconnect, the second this.getClient call, logger.info and the
finally ReloadBaileys.inFlightByPhone.delete(phone)) so all statements inside
try are consistently nested under try; ensure closing braces and the finally
remain aligned correctly with the try.
- Around line 10-11: The Map lastRunAtByPhone grows without bounds; add pruning
to avoid stale entries by implementing a cleanup policy: either cap size with an
LRU eviction or periodically remove entries older than a configurable TTL.
Concretely, add a method (e.g., pruneLastRunAtEntries) on the same class that
iterates lastRunAtByPhone and deletes keys with timestamp < Date.now() - TTL,
and invoke it regularly (setInterval) or call it opportunistically from the code
paths that update lastRunAtByPhone (where inFlightByPhone and lastRunAtByPhone
are used) so stale phone entries are removed; make TTL and interval configurable
constants and reference lastRunAtByPhone and inFlightByPhone in the
implementation.
src/services/reload_baileys.ts (2)

35-71: Indentation inside try block is inconsistent.

Lines 36–69 (the body of the try) are at the same indent level as try itself. This harms readability. Consider indenting the body one level deeper.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/services/reload_baileys.ts` around lines 35 - 71, Indent the entire body
of the try block one level deeper to improve readability: update the block that
starts with try { ... } in the ReloadBaileys.run flow (including calls to
this.getConfig, super.run, this.getClient, await config.getStore, usage of
sessionStore.isStatusConnecting/isStatusRestartRequired/isStatusOnline/isStatusStandBy,
currentClient.disconnect, the second this.getClient call, logger.info and the
finally ReloadBaileys.inFlightByPhone.delete(phone)) so all statements inside
try are consistently nested under try; ensure closing braces and the finally
remain aligned correctly with the try.

10-11: lastRunAtByPhone entries are never pruned.

The Map grows by one entry per unique phone and is never cleaned up. For long-running instances handling a large number of phones this could accumulate stale entries. Not urgent given the small per-entry footprint, but worth a periodic cleanup or a bounded Map/LRU if the phone population is large.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/services/reload_baileys.ts` around lines 10 - 11, The Map
lastRunAtByPhone grows without bounds; add pruning to avoid stale entries by
implementing a cleanup policy: either cap size with an LRU eviction or
periodically remove entries older than a configurable TTL. Concretely, add a
method (e.g., pruneLastRunAtEntries) on the same class that iterates
lastRunAtByPhone and deletes keys with timestamp < Date.now() - TTL, and invoke
it regularly (setInterval) or call it opportunistically from the code paths that
update lastRunAtByPhone (where inFlightByPhone and lastRunAtByPhone are used) so
stale phone entries are removed; make TTL and interval configurable constants
and reference lastRunAtByPhone and inFlightByPhone in the implementation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@src/controllers/registration_controller.ts`:
- Around line 42-44: The deregister endpoint currently calls
this.logout.run(phone, { force: true }) without error handling so any exception
becomes an unhandled rejection; wrap that call in a try/catch (mirroring the
register handler) inside the deregister function in registration_controller.ts,
catch errors from this.logout.run, log the error (use the controller's logger)
and return an appropriate HTTP error response (e.g., res.status(500).json({
error: 'Failed to deregister' }) or pass the error to next(err)) so failures in
socket disconnect/cleanup are handled gracefully.

In `@src/services/logout_baileys.ts`:
- Line 42: The log incorrectly always says "forced logout" regardless of why the
socket logout path ran; update the logger.warn call in logout_baileys.ts (the
code that uses logger.warn(error, 'Ignore error on forced logout for %s',
phone)) to conditionally choose the message based on the force flag (or
shouldTrySocketLogout) — e.g., log "Ignore error on forced logout for %s" when
force is true, otherwise log "Ignore error on socket/logout for online session
%s" (or similar) so the message accurately reflects the cause and include phone
and error as before.
- Around line 30-43: The logout flow currently calls this.getClient(...) when no
cached client exists, which may create a new connection just to logout; change
the logic so you only attempt a socket logout if a cached client exists (e.g.,
check clients.has(phone) or clients.get(phone) first) and skip calling
this.getClient(...) when there's no cached client; still run the session cleanup
afterwards (sessionStore methods) and keep using client?.logout() when a cached
client is found; update the shouldTrySocketLogout branch to guard the
this.getClient(...) call accordingly.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@src/services/logout_baileys.ts`:
- Line 42: The log incorrectly always says "forced logout" regardless of why the
socket logout path ran; update the logger.warn call in logout_baileys.ts (the
code that uses logger.warn(error, 'Ignore error on forced logout for %s',
phone)) to conditionally choose the message based on the force flag (or
shouldTrySocketLogout) — e.g., log "Ignore error on forced logout for %s" when
force is true, otherwise log "Ignore error on socket/logout for online session
%s" (or similar) so the message accurately reflects the cause and include phone
and error as before.
- Around line 30-43: The logout flow currently calls this.getClient(...) when no
cached client exists, which may create a new connection just to logout; change
the logic so you only attempt a socket logout if a cached client exists (e.g.,
check clients.has(phone) or clients.get(phone) first) and skip calling
this.getClient(...) when there's no cached client; still run the session cleanup
afterwards (sessionStore methods) and keep using client?.logout() when a cached
client is found; update the shouldTrySocketLogout branch to guard the
this.getClient(...) call accordingly.
src/services/logout_baileys.ts (2)

42-42: Log message says "forced logout" but this path also runs for non-forced online sessions.

When shouldTrySocketLogout is true because the session is online (not because force is true), the warning still reads "forced logout." Consider adjusting the message for accuracy.

Proposed fix
-        logger.warn(error, 'Ignore error on forced logout for %s', phone)
+        logger.warn(error, 'Ignore error on logout for %s (force=%s)', phone, force)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/logout_baileys.ts` at line 42, The log incorrectly always says
"forced logout" regardless of why the socket logout path ran; update the
logger.warn call in logout_baileys.ts (the code that uses logger.warn(error,
'Ignore error on forced logout for %s', phone)) to conditionally choose the
message based on the force flag (or shouldTrySocketLogout) — e.g., log "Ignore
error on forced logout for %s" when force is true, otherwise log "Ignore error
on socket/logout for online session %s" (or similar) so the message accurately
reflects the cause and include phone and error as before.

30-43: Skip socket logout attempt when no cached client exists to avoid unnecessary connection.

In lines 34-39, if a client doesn't exist in cache, calling this.getClient() instantiates a new client and triggers connect() (which establishes a WhatsApp WebSocket connection), only to immediately logout. This wastes resources for forced logouts where the connection doesn't already exist. Since the session cleanup at lines 45-51 always runs regardless, the socket logout is optional.

Consider skipping the logout attempt when there's no existing cached client:

Proposed fix
     if (shouldTrySocketLogout) {
       try {
-        const client = clients.get(phone) || (await this.getClient({
-          phone,
-          listener: this.listener,
-          getConfig: this.getConfig,
-          onNewLogin: this.onNewLogin,
-        }))
+        const client = clients.get(phone)
+        if (!client) {
+          logger.debug('No existing client for %s, skipping socket logout', phone)
+        }
         await client?.logout()
       } catch (error) {
         logger.warn(error, 'Ignore error on forced logout for %s', phone)
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/logout_baileys.ts` around lines 30 - 43, The logout flow
currently calls this.getClient(...) when no cached client exists, which may
create a new connection just to logout; change the logic so you only attempt a
socket logout if a cached client exists (e.g., check clients.has(phone) or
clients.get(phone) first) and skip calling this.getClient(...) when there's no
cached client; still run the session cleanup afterwards (sessionStore methods)
and keep using client?.logout() when a cached client is found; update the
shouldTrySocketLogout branch to guard the this.getClient(...) call accordingly.

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

Comments