supabase / supabase-py

Python Client for Supabase. Query Postgres from Flask, Django, FastAPI. Python user authentication, security policies, edge functions, file storage, and realtime data streaming. Good first issue.
https://supabase.com/docs/reference/python
MIT License
1.73k stars 205 forks source link

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

Open sigridjineth opened 1 month ago

sigridjineth commented 1 month ago

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.

DevyRuxpin commented 1 month ago

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 commented 1 month ago

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