Illogical behaviour of python web framework (issue investigation story)

mikevv

MikeVV

Posted on February 20, 2023

Illogical behaviour of python web framework (issue investigation story)

TLDR: Strange issues with Sanic framework on my new pet project were fixed in ugly way and I do not like it but do not see the other way and asking for help/advice.


Recently I have tried a new python ASGI web framework for my new pet project and faced an weirdest issue on my memory.

Our hero framework here is Sanic - new, modern async web framework with great slogan "Build fast. Run fast." on the site.

Let's move to the story itself.

I have created an web application backbone with couple endpoints and it works at the beginning.

code like this:

from sanic import Sanic


app = Sanic("pet project N7355")


@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))


@app.route("/system/health")
async def health_handler(request):
    return json(dict(status="ok"))


@app.route("/system/stat")
async def stat_handler(request):
    return json(dict())


app.run(host='0.0.0.0', port=8765, debug=debug)
Enter fullscreen mode Exit fullscreen mode

But after I split that code to modules and moved initialisation of endpoints to separate function I have started to see an error about Sanic cannot start as there is no routes defined.

new code:
main.py

from sanic import Sanic
from routes import init_routes


app = Sanic("pet project N7355")
init_routes(app)

app.run(host='0.0.0.0', port=8765, debug=debug)
Enter fullscreen mode Exit fullscreen mode

routes.py


def init_routes(app):
    @app.route("/")
    async def index_handler(request):
        return json(dict(name="pet project N7355", version="0.1.0"))


    @app.route("/system/health")
    async def health_handler(request):
        return json(dict(status="ok"))


    @app.route("/system/stat")
    async def stat_handler(request):
        return json(dict())
Enter fullscreen mode Exit fullscreen mode

Similar approach for defining a routes was used in my previous project with Flask and worked perfectly fine but not now.
I have tried to run application in single thread mode as suggested in error message but there was no such error in this more.

I thought what a strange issue and fixed that by workaround (dirty hack in fact) - moved "/" method from function in module to main file.

My next step was about connecting to database.

I have used PeeWee ORM (not an mainstream choice but it looks simple and easy) and as there is no plugins to link Sanic with PeeWee I used approach listed on Sanic site - put a connection to application context app.ctx.db = db and faced another error - "cannot use not initialised database".

code with db connection:
main.py

from sanic import Sanic
from routes import init_routes
from db import init_db


app = Sanic("pet project N7355")
app.ctx.db = init_db()

# workaround to fix routes issue in multithread mode
@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))


init_routes(app)

app.run(host='0.0.0.0', port=8765, debug=debug)
Enter fullscreen mode Exit fullscreen mode

routes.py


def init_routes(app):
    @app.route("/system/health")
    async def health_handler(request):
        return json(dict(status="ok"))


    @app.route("/system/stat")
    async def stat_handler(request):
        app.ctx.db.connect()
        msg_count = Message.select().count()
        app.ctx.db.close()
        return json(dict(msg_count=msg_count))
Enter fullscreen mode Exit fullscreen mode

db.py

from peewee import *

....

def init_db():
  ....
  return db
Enter fullscreen mode Exit fullscreen mode

At first I did not realised that these issues are about the same but after some debugging with prints I have found that Sanic in multithreaded mode besides main instance of app creates two additional instances (I suppose I instance per thread) and these new instances in fact processing requests, and do not have routes and db defined.

After finding that behaviour I understand how to fix the issues - move all initialisation from "runtime" level to "module import" level

fixed code:
main.py

from sanic import Sanic
from routes import app
from db import db

app.ctx.db = db

app.run(host='0.0.0.0', port=8765, debug=debug)
Enter fullscreen mode Exit fullscreen mode

routes.py


app = Sanic("pet project N7355")

@app.route("/")
async def index_handler(request):
    return json(dict(name="pet project N7355", version="0.1.0"))

@app.route("/system/health")
async def health_handler(request):
    return json(dict(status="ok"))

@app.route("/system/stat")
async def stat_handler(request):
    app.ctx.db.connect()
    msg_count = Message.select().count()
    app.ctx.db.close()
    return json(dict(msg_count=msg_count))
Enter fullscreen mode Exit fullscreen mode

db.py

from peewee import *

....

def init_db():
  ....
  return db

db = init_db()
Enter fullscreen mode Exit fullscreen mode

from one hand this solution works but from other it looks ugly for me as with that code structure it hard to test modules code in isolation.

I completely understand that I used not the best approach with routes and better rewrite those with blueprints that available in Sanic, but still, for DB I have used approach listed on the Sanic site and is does not work for me because that Sanic's specifics.

As a result I have a open questions:

  • [rhetorical] Why it made is such way where only one way to make things right exists and this specifics is not listed in documentation?
  • [practical] Maybe I did something wrong and things that I see as logical is not in fact logical?

Maybe someone faced something like this and know how to do that properly?

💖 💪 🙅 🚩
mikevv
MikeVV

Posted on February 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related