Skip to content

prevent duplicate reloading#130

Open
clairton wants to merge 1 commit intomainfrom
fix/duplicate-reloading
Open

prevent duplicate reloading#130
clairton wants to merge 1 commit intomainfrom
fix/duplicate-reloading

Conversation

@clairton
Copy link
Owner

@clairton clairton commented Feb 16, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved connection state detection for more reliable session handling.
    • Added protection against concurrent reloads to prevent conflicts during session refresh.
    • Refined status transition handling including a new "reloading" state to avoid race conditions.
  • Chores

    • Version bumped to 2.8.1-alpha

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Adds a new 'reloading' session status, updates status checks to use unified getStatus() calls, introduces a concurrent-reload guard and status transitions, and bumps package version to 2.8.1-alpha.

Changes

Cohort / File(s) Summary
Session Status System
src/services/session_store.ts
Added 'reloading' to sessionStatus union and introduced isStatusReloading(phone: string) to check that status.
Reload logic & guard
src/services/reload_baileys.ts
Added early-return guard when status is 'reloading'; set status to 'reloading' before reload; consolidated status checks to use getStatus() and disconnect existing client when in active states.
Auto-connect adjustments
src/services/auto_connect.ts
Replaced predicate checks with getStatus() + includes check; log now reports currentStatus when updating session to offline before auto-connect.
Redis-backed store behavior
src/services/session_store_redis.ts
Include 'reloading' in statuses that trigger clearConnectCount within setStatus.
Package metadata
package.json
Bumped package version from 2.8.0 to 2.8.1-alpha.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ReloadService
    participant SessionStore
    participant BaileysClient

    Client->>ReloadService: request reload(phone)
    ReloadService->>SessionStore: getStatus(phone)
    SessionStore-->>ReloadService: currentStatus
    alt currentStatus == "reloading"
        ReloadService->>ReloadService: log "already reloading" and return
    else
        ReloadService->>SessionStore: setStatus(phone, "reloading")
        ReloadService->>BaileysClient: if active (online/connecting/standby) -> disconnect()
        ReloadService->>BaileysClient: initialize/reconnect client
        BaileysClient-->>ReloadService: ready/connected
        ReloadService->>SessionStore: setStatus(phone, "online" | appropriate)
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through statuses with a careful spin,

I added "reloading" so we don't begin again,
Guards in place, logs alight,
Version bumped — a tiny bite,
Now I twitch my nose and grin.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'prevent duplicate reloading' directly aligns with the main change: adding a 'reloading' status and guard logic to prevent concurrent reload operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/duplicate-reloading

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/reload_baileys.ts (1)

23-58: ⚠️ Potential issue | 🔴 Critical

If an error is thrown after setting 'reloading', the session is permanently stuck.

There is no try/finally (or equivalent) to reset the status away from 'reloading' on failure. Any exception thrown between line 36 and line 50 (e.g., getClient fails, disconnect fails, super.run fails) will leave the session stuck in 'reloading' forever, causing the guard on line 32 to block all future reload attempts for that phone.

🐛 Proposed fix: wrap the reload body in try/finally
     await sessionStore.setStatus(phone, 'reloading')
+    try {
       const currentClient = await this.getClient({
         phone,
         listener: this.listener,
         getConfig: this.getConfig,
         onNewLogin: this.onNewLogin,
       })
       const currentStatus = await sessionStore.getStatus(phone)
       if (['online', 'standby', 'connecting'].includes(currentStatus)) {
         logger.warn('Reload disconnect session %s!', phone)
         await currentClient?.disconnect()
       }
       await super.run(phone)
       await sessionStore.setStatus(phone, 'online') // to clear standby
       await sessionStore.setStatus(phone, 'disconnected')
       await this.getClient({
         phone,
         listener: this.listener,
         getConfig: this.getConfig,
         onNewLogin: this.onNewLogin,
       })
       logger.info('Reloaded session %s!', phone)
+    } catch (error) {
+      logger.error(error, 'Error reloading session %s', phone)
+      await sessionStore.setStatus(phone, 'disconnected')
+      throw error
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/reload_baileys.ts` around lines 23 - 58, The reload flow in
run(...) sets sessionStore.setStatus(phone, 'reloading') but never guarantees it
will be cleared on error; wrap the critical reload sequence (calls to getClient,
sessionStore.getStatus checks, currentClient.disconnect, super.run and the
subsequent client creation) in a try/finally so that
sessionStore.setStatus(phone, ...) is always reset (either back to the prior
status or to a safe state like 'disconnected' or 'standby') in the finally
block; update references in the finally to use the same sessionStore and phone
variables and ensure any awaited cleanup (e.g., clearing 'reloading' or
restoring previous status) occurs there so an exception does not permanently
leave the session in 'reloading'.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@src/services/reload_baileys.ts`:
- Around line 32-36: The TOCTOU occurs because run(phone) calls
sessionStore.isStatusReloading and then sessionStore.setStatus in separate async
Redis trips (SessionStoreRedis), allowing races; change the logic to perform an
atomic check-and-set instead of two calls — either add/use a new atomic
sessionStore method (e.g., setStatusIfNotReloading or acquireReloadLock(phone))
that uses Redis SET key value NX (or a Lua script) to set the "reloading" marker
only if absent (optionally with TTL) and return success/failure, and replace the
isStatusReloading+setStatus pair in run(phone) with that single atomic call;
alternatively, if single-process, implement a per-phone in-memory lock/semaphore
and use that in run(phone) to serialize concurrent reloads.
- Around line 49-50: Replace the fragile side-effect hack of calling
sessionStore.setStatus(phone, 'online') to clear the connect count: remove that
call and invoke the explicit API sessionStore.clearConnectCount(phone) (or the
appropriate clearConnectCount method on SessionStore) before or instead of
setting sessionStore.setStatus(phone, 'disconnected'); keep the final
sessionStore.setStatus(phone, 'disconnected') but use clearConnectCount(phone)
to express intent and avoid triggering online-status side effects.
- Around line 23-58: The reload flow in run(...) sets
sessionStore.setStatus(phone, 'reloading') but never guarantees it will be
cleared on error; wrap the critical reload sequence (calls to getClient,
sessionStore.getStatus checks, currentClient.disconnect, super.run and the
subsequent client creation) in a try/finally so that
sessionStore.setStatus(phone, ...) is always reset (either back to the prior
status or to a safe state like 'disconnected' or 'standby') in the finally
block; update references in the finally to use the same sessionStore and phone
variables and ensure any awaited cleanup (e.g., clearing 'reloading' or
restoring previous status) occurs there so an exception does not permanently
leave the session in 'reloading'.
🧹 Nitpick comments (1)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@src/services/reload_baileys.ts`:
- Around line 49-50: Replace the fragile side-effect hack of calling
sessionStore.setStatus(phone, 'online') to clear the connect count: remove that
call and invoke the explicit API sessionStore.clearConnectCount(phone) (or the
appropriate clearConnectCount method on SessionStore) before or instead of
setting sessionStore.setStatus(phone, 'disconnected'); keep the final
sessionStore.setStatus(phone, 'disconnected') but use clearConnectCount(phone)
to express intent and avoid triggering online-status side effects.
src/services/reload_baileys.ts (1)

49-50: Setting status to 'online' only to immediately override it with 'disconnected' is a fragile side-effect hack.

This relies on the implicit behavior that setStatus('online') clears the connect count in the Redis store. A dedicated clearConnectCount call (already available on SessionStore) would express the intent directly and wouldn't risk triggering other side effects tied to the 'online' status (e.g., listeners, logging).

♻️ Suggested change
-    await sessionStore.setStatus(phone, 'online') // to clear standby
-    await sessionStore.setStatus(phone, 'disconnected')
+    await sessionStore.clearConnectCount(phone)
+    await sessionStore.setStatus(phone, 'disconnected')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/reload_baileys.ts` around lines 49 - 50, Replace the fragile
side-effect hack of calling sessionStore.setStatus(phone, 'online') to clear the
connect count: remove that call and invoke the explicit API
sessionStore.clearConnectCount(phone) (or the appropriate clearConnectCount
method on SessionStore) before or instead of setting
sessionStore.setStatus(phone, 'disconnected'); keep the final
sessionStore.setStatus(phone, 'disconnected') but use clearConnectCount(phone)
to express intent and avoid triggering online-status side effects.

Comment on lines 32 to 36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TOCTOU race between the reloading check and the status set.

With the Redis-backed SessionStoreRedis, the isStatusReloading (line 32) and setStatus (line 36) are two separate async Redis round-trips. Two concurrent run(phone) calls can both observe isStatusReloading === false before either sets the status, defeating the guard. Consider using an atomic Redis operation (e.g., SET key value NX or a Lua script) to make the check-and-set atomic, or use a per-phone in-memory lock/semaphore as a simpler alternative if single-process.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/reload_baileys.ts` around lines 32 - 36, The TOCTOU occurs
because run(phone) calls sessionStore.isStatusReloading and then
sessionStore.setStatus in separate async Redis trips (SessionStoreRedis),
allowing races; change the logic to perform an atomic check-and-set instead of
two calls — either add/use a new atomic sessionStore method (e.g.,
setStatusIfNotReloading or acquireReloadLock(phone)) that uses Redis SET key
value NX (or a Lua script) to set the "reloading" marker only if absent
(optionally with TTL) and return success/failure, and replace the
isStatusReloading+setStatus pair in run(phone) with that single atomic call;
alternatively, if single-process, implement a per-phone in-memory lock/semaphore
and use that in run(phone) to serialize concurrent reloads.

@clairton clairton force-pushed the fix/duplicate-reloading branch from a85abb1 to f858074 Compare February 16, 2026 22:37
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/services/reload_baileys.ts`:
- Around line 30-37: The reload flow currently sets
sessionStore.setStatus(phone, 'reloading') then proceeds and may throw, leaving
the status stuck; update the logic in the reload function (the block that calls
config.getStore, sessionStore.getStatus, sessionStore.setStatus and then calls
getClient, disconnect, super.run) to wrap the remainder of the reload body in a
try/finally so that sessionStore.setStatus(phone, <previousStatus or null/''>)
is always called in the finally block to clear/reset the status even if
getClient, disconnect, super.run (or any other operation) throws; preserve
retrieval of the prior status from sessionStore.getStatus(phone) before setting
'reloading' and restore that value (or a cleared state) in the finally.

Comment on lines +30 to +37
const store = await config.getStore(phone, config)
const { sessionStore } = store
const currentStatus = await sessionStore.getStatus(phone)
if (currentStatus == 'reloading') {
logger.info('Already reloading session %s!', phone)
return
}
await sessionStore.setStatus(phone, 'reloading')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Status stuck at 'reloading' if run() throws after line 37.

If any operation after setStatus(phone, 'reloading') throws (e.g., getClient, disconnect, super.run), the status is never reset and all future reloads for this phone will be permanently blocked by the early-return guard on line 33. Wrap the reload body in a try/finally to ensure the status is cleared on failure.

Proposed fix
     await sessionStore.setStatus(phone, 'reloading')
+    try {
     const currentClient = await this.getClient({
       phone,
       listener: this.listener,
       getConfig: this.getConfig,
       onNewLogin: this.onNewLogin,
     })
     if (['online', 'standby', 'connecting'].includes(currentStatus)) {
       logger.warn('Reload disconnect session %s!', phone)
       await currentClient?.disconnect()
     }
     await super.run(phone)
     await sessionStore.setStatus(phone, 'online') // to clear standby
     await sessionStore.setStatus(phone, 'disconnected')
     await this.getClient({
       phone,
       listener: this.listener,
       getConfig: this.getConfig,
       onNewLogin: this.onNewLogin,
     })
     logger.info('Reloaded session %s!', phone)
+    } catch (e) {
+      logger.error(e, 'Error reloading session %s', phone)
+      await sessionStore.setStatus(phone, 'offline')
+      throw e
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const store = await config.getStore(phone, config)
const { sessionStore } = store
const currentStatus = await sessionStore.getStatus(phone)
if (currentStatus == 'reloading') {
logger.info('Already reloading session %s!', phone)
return
}
await sessionStore.setStatus(phone, 'reloading')
const store = await config.getStore(phone, config)
const { sessionStore } = store
const currentStatus = await sessionStore.getStatus(phone)
if (currentStatus == 'reloading') {
logger.info('Already reloading session %s!', phone)
return
}
await sessionStore.setStatus(phone, 'reloading')
try {
const currentClient = await this.getClient({
phone,
listener: this.listener,
getConfig: this.getConfig,
onNewLogin: this.onNewLogin,
})
if (['online', 'standby', 'connecting'].includes(currentStatus)) {
logger.warn('Reload disconnect session %s!', phone)
await currentClient?.disconnect()
}
await super.run(phone)
await sessionStore.setStatus(phone, 'online') // to clear standby
await sessionStore.setStatus(phone, 'disconnected')
await this.getClient({
phone,
listener: this.listener,
getConfig: this.getConfig,
onNewLogin: this.onNewLogin,
})
logger.info('Reloaded session %s!', phone)
} catch (e) {
logger.error(e, 'Error reloading session %s', phone)
await sessionStore.setStatus(phone, 'offline')
throw e
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/reload_baileys.ts` around lines 30 - 37, The reload flow
currently sets sessionStore.setStatus(phone, 'reloading') then proceeds and may
throw, leaving the status stuck; update the logic in the reload function (the
block that calls config.getStore, sessionStore.getStatus, sessionStore.setStatus
and then calls getClient, disconnect, super.run) to wrap the remainder of the
reload body in a try/finally so that sessionStore.setStatus(phone,
<previousStatus or null/''>) is always called in the finally block to
clear/reset the status even if getClient, disconnect, super.run (or any other
operation) throws; preserve retrieval of the prior status from
sessionStore.getStatus(phone) before setting 'reloading' and restore that value
(or a cleared state) in the finally.

@clairton clairton force-pushed the fix/duplicate-reloading branch from f858074 to 0af8f80 Compare February 17, 2026 17:54
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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/services/reload_baileys.ts`:
- Around line 37-57: The reload sequence can leave the session stuck at
'reloading' if any of sessionStore.setStatus, this.getClient,
currentClient.disconnect, super.run, or the subsequent getClient calls throw;
wrap the core reload logic in a try/finally so you always clear/reset status:
set sessionStore.setStatus(phone,'reloading') before the try, perform
getClient/disconnect/super.run/second getClient inside the try, and in the
finally ensure you call sessionStore.setStatus(phone,'online') (or original
state) and then sessionStore.setStatus(phone,'disconnected') as necessary so the
existing early-return that checks status will not permanently block future
reloads. Ensure references to currentClient?.disconnect and this.getClient
remain unchanged when moving into the try block.
- Around line 32-37: The current guard in run(phone) uses two separate async
calls sessionStore.getStatus(phone) and sessionStore.setStatus(phone,
'reloading'), which allows a TOCTOU race when run(phone) is invoked
concurrently; replace this with an atomic check-and-set using the sessionStore's
Redis backend (e.g., use Redis SET key value NX with an expiration or a small
Lua script that performs GET and SET atomically) so only the first caller
succeeds in setting 'reloading' and others see that and exit; modify run(phone)
to call a new atomic method on sessionStore (e.g., trySetStatusIfNotExists /
setStatusIfCurrent) or add an atomic Lua helper and use it instead of
getStatus+setStatus.

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