Skip to content

fix duplicate tab issue#4607

Merged
adhami3310 merged 15 commits into
mainfrom
lendemor/fix_duplicate_tab_issue
Aug 4, 2025
Merged

fix duplicate tab issue#4607
adhami3310 merged 15 commits into
mainfrom
lendemor/fix_duplicate_tab_issue

Conversation

@Lendemor
Copy link
Copy Markdown
Contributor

@Lendemor Lendemor commented Jan 8, 2025

fix #4605

Comment thread reflex/app.py Outdated
Comment thread reflex/app.py Outdated
@masenf
Copy link
Copy Markdown
Collaborator

masenf commented Jan 21, 2025

Revisiting this PR, is there ever a case where we wouldn't get on_disconnect to unlink the sid/token, resulting in a refresh/reconnect leading to loss of token and state for a client?

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 20, 2025

CodSpeed Performance Report

Merging #4607 will not alter performance

Comparing lendemor/fix_duplicate_tab_issue (989a48c) with main (687d20f)

Summary

✅ 8 untouched benchmarks

@Lendemor Lendemor marked this pull request as ready for review July 31, 2025 21:50
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Greptile Summary

This PR implements a comprehensive fix for duplicate browser tab interference during background events (issue #4605). The core problem was that when users duplicated tabs using Ctrl+D or similar methods, both tabs shared the same client_token from sessionStorage, causing background tasks from one tab to interfere with the other's state updates.

The solution introduces a new TokenManager system with the following key components:

  1. TokenManager Architecture: Abstract base class with LocalTokenManager (single-worker) and RedisTokenManager (distributed) implementations that detect when the same token is used by multiple Socket.IO session IDs, indicating duplicate tabs.

  2. WebSocket Connection Enhancement: Modified the client-side state.js to send the client token as a query parameter during WebSocket connection establishment, enabling server-side duplicate detection at connection time rather than during event processing.

  3. Duplicate Detection & Resolution: When a duplicate token is detected, the server automatically generates a new UUID4 token and emits a 'new_token' event to the client, which updates both the global token variable and sessionStorage.

  4. Event Flow Restructuring: Moved token-to-session-ID linking from on_event to on_connect in the EventNamespace, ensuring duplicate detection happens immediately upon connection rather than during background task execution.

  5. Testing Infrastructure: Added comprehensive unit tests covering both LocalTokenManager and RedisTokenManager implementations, including error handling, Redis fallback mechanisms, and cleanup scenarios.

  6. Testing Framework Integration: Enhanced the AppHarness testing class to reset the TokenManager instance when the backend state manager is reset, preventing event loop affinity issues during tests.

This architectural change maintains backward compatibility while providing robust duplicate tab isolation for both single-worker and distributed Reflex deployments.

Confidence score: 4/5

  • This is a well-architected solution that addresses the root cause of tab interference issues with proper error handling and fallback mechanisms.
  • The comprehensive test coverage and multi-environment support (local vs Redis) demonstrates thorough consideration of edge cases.
  • Files requiring more attention: reflex/app.py for the core EventNamespace changes and reflex/utils/token_manager.py for the new token management logic, as these are critical components that handle the duplicate detection and token generation flow.

5 files reviewed, 2 comments

Edit Code Review Bot Settings | Greptile

Comment thread reflex/utils/token_manager.py Outdated
Comment thread reflex/app.py Outdated
Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

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

functionality works great in my testing; a few comments from an API continuity perspective, since people actually do use this functionality and need it for background tasks and perhaps other cleanup steps

Comment thread reflex/app.py

def on_connect(self, sid: str, environ: dict):
# Use TokenManager for distributed duplicate tab prevention
self._token_manager = TokenManager.create()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

consider putting the TokenManager on App as a real API into this data.

down the road we could even expose helper iterators on this new TokenManager to make cancelling background tasks easier, like session_is_connected and token_is_connected which would save the passed sid/token and yield True as long as these values were still being tracked.

Like so
@rx.event(background=True)
async def thing_checker(self):
    for _task_active in app.token_manager.session_is_connected(
        sid=self.router_data.session.session_id
    ):
        async with self:
            if await self._check_thing():
                await self._update_thing()
        await asyncio.sleep(CHECK_INTERVAL)

lot of possibilities for an asyncio.Event API for getting notified when a session or client disconnects, which people have asked for

Comment thread reflex/app.py
The token to SID mapping dict.
"""
# For backward compatibility, expose the underlying dict
return self._token_manager.token_to_sid
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should raise a deprecation here and below

@adhami3310 adhami3310 merged commit 71a54bf into main Aug 4, 2025
41 checks passed
@adhami3310 adhami3310 deleted the lendemor/fix_duplicate_tab_issue branch August 4, 2025 23:31
@hr-alebel
Copy link
Copy Markdown

Revisiting this PR, is there ever a case where we wouldn't get on_disconnect to unlink the sid/token, resulting in a refresh/reconnect leading to loss of token and state for a client?

Sry to ressurrect this PR.

Yes there is a case where a client reconnects without firing "on_disconnect". Specifically when the Backend Process crashes. If the app is used by multiple users, one causing a crash (or restart by external controls, see containers in K8s/docker) will make the tokes/sessions stored in Redis persistent but still linked, meaning after the container restarts it causes a token duplicate, which in turn forces to create a new token (without hydration mind you) causing an inconsitent state between UI and Backend breaking the app.

Tested by:

  1. having an app with for example selectors and a button to get the state from Backend
  2. select a certain combination that differs from the default state
  3. check state in Redis -> selection is persisted
  4. restart the container running the backend
  5. when the backend is back observe that in the Websocket Messages there is a new_token command
  6. press button to get state -> default state from Backend is shown

This issue is reproducible 100% on any Version after 0.8.5 (last version before this PR got merged)

@benedikt-bartscher
Copy link
Copy Markdown
Contributor

@hr-alebel may I suggest opening an issue for this which references the old issue/PR?

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.

Duplicated tabs interfere with each other during background events

5 participants