Illogical behaviour of python web framework (issue investigation story)
MikeVV
Posted on February 20, 2023
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)
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)
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())
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)
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))
db.py
from peewee import *
....
def init_db():
....
return db
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)
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))
db.py
from peewee import *
....
def init_db():
....
return db
db = init_db()
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?
Posted on February 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.