Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supabase Client Requires Explicit sign_out() to Terminate Properly #926

Open
sigridjineth opened this issue Sep 16, 2024 · 4 comments
Open
Assignees
Labels
bug Something isn't working

Comments

@sigridjineth
Copy link

sigridjineth commented Sep 16, 2024

Summary

The Supabase client currently requires an explicit call to client.auth.sign_out() for processes to terminate correctly. Without this, background WebSocket connections and other resources may remain active, leading to incomplete shutdowns and potential resource leaks.

Problem Explanation:

The current behavior of the Supabase client involves establishing WebSocket connections and listening for authentication events. These processes, especially those involving real-time functionality, do not automatically terminate upon the program’s end. Explicitly calling client.auth.sign_out() is necessary to clean up these resources and ensure proper process termination.

# From SyncClient class in SyncClient.py
class SyncClient:
    def __init__(self, ...):
        # ...
        self.realtime = self._init_realtime_client(
            realtime_url=self.realtime_url,
            supabase_key=self.supabase_key,
            options=options.realtime if options else None,
        )
        # ...

    @staticmethod
    def _init_realtime_client(
        realtime_url: str, supabase_key: str, options: Optional[Dict[str, Any]]
    ) -> SyncRealtimeClient:
        """Private method for creating an instance of the realtime-py client."""
        return SyncRealtimeClient(
            realtime_url, token=supabase_key, params=options or {}
        )

    def _listen_to_auth_events(
        self, event: AuthChangeEvent, session: Union[Session, None]
    ):
        # ...
        self.realtime.set_auth(access_token)

# From SyncRealtimeClient in realtime-py
class SyncRealtimeClient:
    def __init__(self, ...):
        # ...
        self._endpointWebSocket = None
        # ...

    def connect(self):
        # ...
        self._endpointWebSocket = websocket.WebSocketApp(
            # ...
        )
        # ...

    def set_auth(self, token):
        # ...
        self.connect()  # This might create a new WebSocket connection

# From GoTrueClient in gotrue-py
class SyncGoTrueClient:
    def sign_out(self, options: SignOutOptions = {"scope": "global"}) -> None:
        # ...
        self._remove_session()
        self._notify_all_subscribers("SIGNED_OUT", None)

Key points:

  1. Real-time Connections: The WebSocket connections created by SyncRealtimeClient continue running in the background and need to be manually terminated.
  2. Authentication Events: Sign-out triggers an event that helps reset real-time client authentication, which won't occur unless sign_out() is called.
  3. Resource Management: The sign_out() function ensures proper cleanup of sessions and network connections, preventing potential memory leaks or resource hogging.
  4. Daemon Threads: Real-time connections might be running as daemon threads, which do not automatically terminate, leading to hanging processes unless explicitly stopped with sign_out().

Given this behavior, the necessity of an explicit client.auth.sign_out() call should be clearly documented and potentially re-evaluated for a more intuitive shutdown process.

@sigridjineth sigridjineth added the bug Something isn't working label Sep 16, 2024
@sigridjineth sigridjineth changed the title Supabase Client Requires Explicit sign_out() to Terminate Properly Supabase Client Requires Explicit sign_out() to Terminate Properly Sep 16, 2024
@DevyRuxpin
Copy link

from typing import Optional, Dict, Any, Union
import websocket

From SyncClient class in SyncClient.py

class SyncClient:
"""
SyncClient class for managing real-time connections and authentication.

...

Methods:
    sign_out():
        Sign out and clean up resources. This method must be called to
        terminate WebSocket connections and prevent resource leaks.
"""

def __init__(self, realtime_url: str, supabase_key: str, options: Optional[Dict[str, Any]] = None):
    self.realtime_url = realtime_url
    self.supabase_key = supabase_key
    self.options = options
    self.realtime = self._init_realtime_client(
        realtime_url=self.realtime_url,
        supabase_key=self.supabase_key,
        options=options.realtime if options else None,
    )

@staticmethod
def _init_realtime_client(
    realtime_url: str, supabase_key: str, options: Optional[Dict[str, Any]]
) -> 'SyncRealtimeClient':
    """Private method for creating an instance of the realtime-py client."""
    return SyncRealtimeClient(
        realtime_url, token=supabase_key, params=options or {}
    )

def _listen_to_auth_events(
    self, event: 'AuthChangeEvent', session: Union['Session', None]
):
    # ...
    access_token = session.access_token if session else None
    self.realtime.set_auth(access_token)

def sign_out(self):
    """Sign out and clean up resources."""
    # Terminate WebSocket connections
    if self.realtime:
        self.realtime.disconnect()
    # Perform other cleanup tasks
    # ...

def __enter__(self):
    """Enter the runtime context related to this object."""
    return self

def __exit__(self, exc_type, exc_value, traceback):
    """Exit the runtime context related to this object."""
    self.sign_out()

From SyncRealtimeClient in realtime-py

class SyncRealtimeClient:
def init(self, realtime_url: str, token: str, params: Optional[Dict[str, Any]] = None):
self.realtime_url = realtime_url
self.token = token
self.params = params
self._endpointWebSocket = None

def connect(self):
    # ...
    self._endpointWebSocket = websocket.WebSocketApp(
        self.realtime_url,
        header={"Authorization": f"Bearer {self.token}"},
        on_message=self.on_message,
        on_error=self.on_error,
        on_close=self.on_close,
    )
    self._endpointWebSocket.run_forever()

def disconnect(self):
    """Disconnect the WebSocket connection."""
    if self._endpointWebSocket:
        self._endpointWebSocket.close()
        self._endpointWebSocket = None

def set_auth(self, access_token: Optional[str]):
    """Set the authentication token for the WebSocket connection."""
    self.token = access_token
    if self._endpointWebSocket:
        self.disconnect()
        self.connect()

def on_message(self, ws, message):
    # Handle incoming messages
    pass

def on_error(self, ws, error):
    # Handle errors
    pass

def on_close(self, ws, close_status_code, close_msg):
    # Handle connection close
    pass

Example usage with context manager

if name == "main":
with SyncClient(realtime_url="wss://example.com/socket", supabase_key="your-supabase-key") as client:
# Perform operations with the client
# ...
pass
# The sign_out() method will be called automatically when exiting the context

@DevyRuxpin
Copy link

Full Disclosure, I'm currently learning. Hope the code I provided above helps. Feel free to comment with any advice etc.

@silentworks silentworks self-assigned this Sep 29, 2024
@Diya910
Copy link

Diya910 commented Dec 6, 2024

Yes I am willing to submit a PR! Please assign me this issue.

@silentworks
Copy link
Contributor

@Diya910 no need to assign, you can just submit a PR once your PR is ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants