pythongunicornflask-socketioeventlet

Flask-socketIO with Gunicorn on the cloud not working


I've build a test app (testing for RESTfull Api) to process some POST requests from another app. When both apps are run localy from PyCharm everything works ok - POST request activates a function which a frontend socketIO is monitoring, and thus triggering a Frontend element update.

My main goal is, however, to have some clients that run localy which will be sending POST requests to a server app in the cloud (Koyeb).

The platform needs to be run using Gunicorn WSGI server, and how ever I try to make it run, the socketIO part just wont work. POST requests are getting through and activate the required function, but socketIO doesn't work.

app.py code:

import eventlet
eventlet.monkey_patch()

from flask import Flask, render_template, request, redirect, send_file, url_for, jsonify
from flask_sqlalchemy import SQLAlchemy
import secrets, os, requests, json
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
from flask_bcrypt import Bcrypt
import feedparser
from flask_socketio import SocketIO, emit

app = Flask(__name__)
socketio = SocketIO(app, async_mode='eventlet')


#
#other code
#

@app.route("/endpoint", methods=['GET', 'POST'])
def endpoint():
    if request.method == 'POST':
        #ApiDb.__table__.drop(db.engine)
        #db.session.query(ApiDb).delete()

        # dbVisits2 = db.session.query(ApiDb).filter_by(id=1).first()
        try:
            dbVisits = ApiDb.query.filter_by(id=1).first()
            dbVisits.nrVisits += 1
        except:
            db.session.add(ApiDb(id=1, nrVisits=1))
        db.session.commit()

        #trigger_update()
        handle_trigger_update()
        return {'odgovor': 'OK'}


@socketio.on('trigger_update')
def handle_trigger_update():
    dbVisits = ApiDb.query.filter_by(id=1).first()
    total = dbVisits.nrVisits
    socketio.emit('update', {'total': total})

@app.route('/trigger_backend')
def trigger_backend():
    handle_trigger_update()  # Poziv backend funkcije direktno
    return 'Backend function triggered successfully'


if __name__=='__main__':
    #socketio.run(app, debug=True, allow_unsafe_werkzeug=True)
    #socketio.run(app)
    #socketio.run(app, port=8000)
    wsgi.server(eventlet.listen(('', 8000)), app)

html code:

<!DOCTYPE html>
<html lang="hr">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>{% block title %} Naslov {% endblock %}</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/w3css.css') }}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/fontawesome/css/all.min.css') }}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
    </head>
    <body>


    <div class="outside-container">
        <div class="form-container" style="width: clamp(600px, 30vw, 1000px);">
            <h3>Comms</h3>

            <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.1/socket.io.js" integrity="sha512-Y5MDU6RaF5h5HE5BgqJlKkV12kbkbIgWHutcT+XHHNOUzr+HHjWZGC02sqEguuPglmFms3cc08WH2PhQ5rF8Cw==" crossorigin="anonymous"></script>

            <script>
                document.addEventListener('DOMContentLoaded', () => {
                    const socket = io.connect('http://' + document.domain + ':' + location.port, {
                      transports: ['websocket']
                    });

                    console.log('sckIO start');
                    socket.on('update', function(data) {
                        console.log('sckIO update');
                        document.getElementById('output3').innerText = data.total;
                    });

                    function triggerBackend() {
                        fetch('/trigger_backend')
                            .then(response => response.text())
                            .then(data => console.log(data))  // Log the response from the server
                            .catch(error => console.error('Error:', error));
                    }

                    triggerBackend();
                });

            </script>

            <div style="display: flex; justify-content: center;">
                Number of visits - periodicaly: <strong><div style="margin-left: 15px;" id="output"></div></strong>
            </div>
            <div style="display: flex; justify-content: center;">
                Number of visits - onStart: <strong><div style="margin-left: 15px;" id="output2"></div></strong>
            </div>
            <div style="display: flex; justify-content: center;">
                Number of visits - direct from userComm load: <strong><div style="margin-left: 15px;">{{ total }}</div></strong>
            </div>
            <div style="display: flex; justify-content: center;">
                Number of visits - SocketIO: <strong><div style="margin-left: 15px;" id="output3"></div></strong>
            </div>

        </div>
    </div>
    
    

    </body>
</html>

Koyeb buildpack run command override: (Procfile) gunicorn --worker-class eventlet -w 1 app:app

Anyone have some idea? It doesnt matter if it uses eventlet or gunicorn, I just want the socketIO to work


Solution

  • After days of searching the net, testing and experimenting, here is the solution: (the trick was to configure socketIO connect properly in your html)

    app.py:

    from flask import Flask, render_template, request, redirect, url_for
    from flask_socketio import SocketIO, send, emit
    app = Flask(__name__)
    socketio = SocketIO(app)
    #socketio = SocketIO(app, cors_allowed_origins='*', engineio_logger=True, logger=True) # for debugging
    #
    # other code
    #
    @app.route("/userComm", methods=['GET', 'POST'])
    def userComm():
        return render_template("userComm.html")
    
    @app.route("/endpoint", methods=['GET', 'POST'])
    def endpoint():
        if request.method == 'POST':
            dbVisits = ApiDb.query.filter_by(id=1).first()
            visits = dbVisits.nrVisits
            socketio.emit("update_visits", visits, namespace='/userComm')
            return {'response': 'OK'}
    
    @socketio.on('connect', namespace='/userComm') # not neccesary
    def test_connect():
        print('SocketIO client on /userComm connected')
    
    @socketio.on('disconnect', namespace='/userComm') # not neccesary
    def test_disconnect():
        print('SocketIO client on /userComm disconnected')
    
    if __name__=='__main__':
        #socketio.run(app, debug=True, host='127.0.0.1', port=5000) #for testing as a local app
        socketio.run(app, debug=True, host='0.0.0.0', port=8000)
    

    userComm.html (as an extend to your basefile):

                <h3>Comms</h3>
                <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.1/socket.io.js" integrity="sha512-Y5MDU6RaF5h5HE5BgqJlKkV12kbkbIgWHutcT+XHHNOUzr+HHjWZGC02sqEguuPglmFms3cc08WH2PhQ5rF8Cw==" crossorigin="anonymous"></script>
                <script>
                    socket = io('/userComm');
                    socket.on("update_visits", (visits)=>{
                        console.log(visits);
                        document.getElementById("output").innerHTML=visits;
                    });
                </script>
                <div style="display: flex; justify-content: center;">
                    Number of visits - SocketIO: <strong><div style="margin-left: 15px;" id="output"></div></strong>
                </div>
    

    Procfile: web: gunicorn --worker-class eventlet -b 0.0.0.0:8000 -w 1 app:app