supabase-py icon indicating copy to clipboard operation
supabase-py copied to clipboard

Supabase Client Requires Explicit `sign_out()` to Terminate Properly

Open sigridjineth opened this issue 1 year ago • 7 comments

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 avatar Sep 16 '24 07:09 sigridjineth

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 avatar Sep 18 '24 23:09 DevyRuxpin

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

DevyRuxpin avatar Sep 18 '24 23:09 DevyRuxpin

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

Diya910 avatar Dec 06 '24 21:12 Diya910

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

silentworks avatar Dec 07 '24 02:12 silentworks

Hi! :) Is there any update on this?

I'm having an issue because after a couple of minutes/hours the Python client connection dies (complains about JWT, and apparently it is not auto-refreshing). As a quick fix, I'm creating new connections every once in a while (short span) and my server memory is rapidly increasing. :/

Thank you! 🙏

miguel-arrf avatar Jan 07 '25 17:01 miguel-arrf

@miguel-arrf you need to provide code when mentioning issue to do with code. Provide a reproducible example repo. I have not experienced what you are mentioning here nor am I able to reproduce it.

silentworks avatar Jan 07 '25 20:01 silentworks

I created an issue (#1034) detailing the same problem I'm facing as @miguel-arrf.

ritikd2 avatar Jan 23 '25 16:01 ritikd2