Search code examples
pythonsocket.iopassenger

How to use Flask-SocketIO with Phusion Passenger?


Phusion Passenger needs to launch the webserver using its own function, so a custom function to launch a webserver cannot be used, while SocketIO seems to require to use one. Is there is any way to bypass this?

Sample code:

from flask import Flask
from flask_socketio import SocketIO

app = Flask('')
socket = SocketIO(app)

@app.route('/')
def home():
    return open("index.html", "r").read()

@socket.on('echo')
def echo(json):
    return json

# Phusion Passenger already does this, so we shouldnt run that next line or else we will never run the program
#app.run(host='0.0.0.0',port=80)

Solution

  • After hours of work on this, I finally got it working. The problem is that Passenger uses parallelism for web requests, which Flask-SocketIO's session system do not support due to storage of data being messy when parallelized. However, creating our own session system and injecting it to Flask-SocketIO makes it work. I am using Redis for storing sessions, but a MySQL or multithreaded SQLite could also work with some adaptations:

    from flask import Flask
    from flask_socketio import SocketIO
    
    app = Flask('')
    #socketio = SocketIO(app) #redefined later
    
    
    # special config for passenger
    from flask import g
    import json, redis, uuid
    
    socketio = SocketIO(app, transports=['polling'], async_mode="threading")
    SESSION_KEY_PREFIX = "yourprojectname_session:"
    redis_socket_path = "sock.sock"
    redis_password = "password"
    redis_store = redis.StrictRedis(
      
        unix_socket_path=redis_socket_path,
        password=redis_password,
        decode_responses=True
    )
    redis_store.ping()
    
    class CustomSession:
        def __init__(self, sid=None):
    
            self.sid = sid or str(uuid.uuid4())
            self.data = {}
    
        def get(self, key, default=None):
            return self.data.get(key, default)
    
        def set(self, key, value):
            self.data[key] = value
            redis_store.set(f"{SESSION_KEY_PREFIX}{self.sid}", self.data)
    
        def load(self):
            data = redis_store.get(f"{SESSION_KEY_PREFIX}{self.sid}")
            if data:
                self.data = json.loads(data)
    
        def save(self):
            redis_store.setex(
                f"{SESSION_KEY_PREFIX}{self.sid}",
                86400 * 9999,  # 9999 days expiry
                json.dumps(self.data)
            )
    
    @socketio.on('connect')
    def handle_connect():
        session_id = session.get('sid')
        if not session_id:
            session_id = str(uuid.uuid4())
            session['sid'] = session_id
        custom_session = CustomSession(session_id)
        custom_session.load()
        g.custom_session = custom_session
    
    @socketio.on('disconnect')
    def handle_disconnect():
        custom_session = getattr(g, 'custom_session', None)
        if custom_session:
            custom_session.save()
    
    # end of the phusion passenger patch/config
    
    @app.route('/')
    def home():
        return open("index.html", "r").read()
    
    @socketio.on('echo')
    def echo(json):
        return json
    
    # Phusion Passenger already does this, so we shouldnt run that next line or else we will never run the program
    #app.run(host='0.0.0.0',port=80)
    

    I also noticed that since Phusion Passenger relies on other software like Apache or Nginx, it is very unlikely to support websocket, so I recommend (as I did) to use long polling by default, and use the "threading" mode to make sure Flask-SocketIO is aware it will run in parallelized.

    Feel free to use my code under no license at all.