Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request middleware not running when registered in a method since 22.9.0 #2699

Closed
1 task done
semmypurewal opened this issue Mar 1, 2023 · 13 comments · Fixed by #2704
Closed
1 task done

request middleware not running when registered in a method since 22.9.0 #2699

semmypurewal opened this issue Mar 1, 2023 · 13 comments · Fixed by #2704
Assignees

Comments

@semmypurewal
Copy link

semmypurewal commented Mar 1, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Prior to 22.9.0, you could register request middleware in functions and listeners. Since 22.9.0 this has stopped working. I'm unclear on whether this is by design or not, but I wasn't able to find anything in the docs that explains the discrepancy.

The last working version I tested was 22.6.2.

Code snippet

from sanic import Sanic, response                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
app = Sanic("my-hello-world-app")                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
@app.middleware("request")                                                                                                                                                                                                                     
async def this_always_works(request):                                                                                                                                                                                                          
    print("this is request middleware")                                                                                                                                                                                                        
                                                                                                                                                                                                                                               
@app.before_server_start                                                                                                                                                                                                                       
async def before_server_start_listener(app, loop):                                                                                                                                                                                             
    def this_fails_in_22_9_0_and_after(request):                                                                                                                                                                                               
        print("this will not fire after in 22.9.0 and after")                                                                                                                                                                                  
    app.register_middleware(this_fails_in_22_9_0_and_after, "request")                                                                                                                                                                         
                                                                                                                                                                                                                                               
@app.route('/')                                                                                                                                                                                                                                
async def test(request):                                                                                                                                                                                                                       
    return response.json({'hello': 'world'})                                                                                                                                                                                                   
                                                                                                                                                                                                                                               
if __name__ == '__main__':                                                                                                                                                                                                                     
    app.register_middleware(lambda app: print("this will also not fire in 22.9.0 and after"), "request")                                                                                                                                       
    app.run(host="0.0.0.0", debug=True, auto_reload=True)                                                                                                                                                                                      

Expected Behavior

I expected all middleware to fire, but only the first one I set up fires since 22.9.0. I know middlware priority was introduced in 22.9, but I wouldn't expect that to have broken existing apps. I tried explicitly setting the priority and I still was not able to get the middleware to fire.

How do you run Sanic?

As a script (app.run or Sanic.serve)

Operating System

Linux

Sanic Version

22.9.0

Additional context

Thanks for Sanic!

@ahopkins ahopkins self-assigned this Mar 1, 2023
@ahopkins
Copy link
Member

ahopkins commented Mar 1, 2023

When I first looked at this earlier I was super shocked and quite disturbed to hear this.

  1. This is meant to be accomplished
  2. I do this very thing in a bunch of production applications

So I think we need to clarify a few things.

Middleware added in a listener will work and run

Your example code draws an incorrect conclusion here:

@app.before_server_start
async def before_server_start_listener(app, loop):
    def this_fails_in_22_9_0_and_after(request):
        print("this will not fire after in 22.9.0 and after")
    app.register_middleware(this_fails_in_22_9_0_and_after, "request")

This should work and will work. Or, rather, I cannot reproduce a scenario where it does not.

image

Adding to an app instance inside an if __name__ == "__main__" block is NOT supported

When you define this block...

if __name__ == '__main__':
    app.register_middleware(lambda app: print("this will also not fire in 22.9.0 and after"), "request")
    app.run(host="0.0.0.0", debug=True, auto_reload=True)

... what it means is ONLY run this code when the main Python interpreter process is running.

Sanic underwent a big (yet super powerful) shift in 22.9. Every worker gets its own dedicated process. This enables scaling, coordination, etc, etc.

But it also means that anything run in that block is not attached to your actual running Application instance.

So, what can you do?

Use a factory, or don't attach dynamically at run time in the if __name__ == "__main__" block

Going forward we are going to change a lot of the documents to really suggest that developers lean more heavily on the CLI. When you do, using the factory pattern becomes super easy and easier for such runtime changes.

def create_app():
    app = Sanic("MyApp")
    ... # do stuff
    return app
$ sanic path.to.server:create_app --factory

You can achieve this of course with if __name__ == "__main__" and app.run, and that will of course always be supported. And, sometimes the simple pattern I showed above is inadequate.

Therefore, we documented how to create dynamic runtime applications. Perhaps this method will work for you?

Use single_process mode

If you never intend to use multiple workers, perhaps you only want to run Sanic in single process mode:

if __name__ == "__main__":
    app.register_middleware(
        lambda app: print("this will also not fire in 22.9.0 and after"),
        "request",
    )
    app.run(..., single_process=True)

This does have a disadvantage though because now you do not have auto-reload for local development. That is itself running in a background process, therefore it is a tradeoff you need to decide upon.

Keep runtime global scope

One thing is to simply move your registration into the global scope.

app.register_middleware(lambda app: print("this will also not fire in 22.9.0 and after"), "request")

if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=True, auto_reload=True)

Now, that will trigger on both the main process and every single worker process to attach your middleware.

Run ONLY in the correct process

Since we ultimately want to run the register_middleware on the worker process, you could just be explicit about it:

if __name__ == "__mp_main__":
    app.register_middleware(
        lambda app: print("this will also not fire in 22.9.0 and after"),
        "request",
    )
elif __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, auto_reload=True)

There really is no reason to have the registration of middleware in the main app in most use cases. But, you could of course see how you could do both if you needed.


One final note:

This...

app.run(host="0.0.0.0", debug=True, auto_reload=True)

can be simplified...

app.run(host="0.0.0.0", dev=True)

@ahopkins ahopkins removed the bug label Mar 1, 2023
@semmypurewal
Copy link
Author

semmypurewal commented Mar 2, 2023

Thanks for the quick and thoughtful response!

It's really the other issue, attaching middleware in before_server_start, that's blocking us from upgrading. I noticed the main issue when I was experimenting and trying to resolve that issue in various ways. Since it was a similar behavior change between the versions, I assumed it was probably part of the same issue which is why I included it in the report.

I'm using Python 3.10 and running Sanic via poetry in Fedora Linux, but I originally noticed this problem in Docker on my Mac laptop. I haven't had a chance to try the minimal example below on my Mac today, but I will give it a try tomorrow and report back here.

Can you see anything in my setup that would make this reproduce consistently for me? More details are below.

My pyproject.toml file looks like this:

[tool.poetry]                                                                                                                                                                                                                                  
name = "sanic-bug"                                                                                                                                                                                                                             
version = "0.1.0"                                                                                                                                                                                                                              
description = ""                                                                                                                                                                                                                               
authors = ["Semmy Purewal <redacted@redacted.com>"]                                                                                                                                                                                           
readme = "README.md"                                                                                                                                                                                                                           
                                                                                                                                                                                                                                               
[tool.poetry.dependencies]                                                                                                                                                                                                                     
python = "^3.10"                                                                                                                                                                                                                               
sanic = "22.6.2"                                                                                                                                                                                                                               
                                                                                                                                                                                                                                               
                                                                                                                                                                                                                                               
[build-system]                                                                                                                                                                                                                                 
requires = ["poetry-core"]                                                                                                                                                                                                                     
build-backend = "poetry.core.masonry.api" 

And my server.py looks like this:

from sanic import Sanic, response                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
app = Sanic("my-hello-world-app")                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
@app.middleware("request")                                                                                                                                                                                                                     
async def this_always_works(request):                                                                                                                                                                                                          
    print("this is request middleware")                                                                                                                                                                                                        
                                                                                                                                                                                                                                               
@app.before_server_start                                                                                                                                                                                                                       
async def before_server_start_listener(app, loop):                                                                                                                                                                                             
    def this_fails_in_22_9_0_and_after(request):                                                                                                                                                                                               
        print("this will not fire after in 22.9.0 and after")                                                                                                                                                                                  
    app.register_middleware(this_fails_in_22_9_0_and_after, "request")                                                                                                                                                                         
                                                                                                                                                                                                                                               
@app.route('/')                                                                                                                                                                                                                                
async def test(request):                                                                                                                                                                                                                       
    return response.json({'hello': 'world'})                                                                                                                                                                                                   
                                                                                                                                                                                                                                               
if __name__ == '__main__':                                                                                                                                                                                                                     
    app.run(host="0.0.0.0", dev=True)           

Running poetry install, then poetry run python server.py, then hitting the endpoint, both middlewares run as we'd expect:

$ poetry run python server.py
[2023-03-01 20:13:34 -0500] [1401809] [INFO] 
  ┌──────────────────────────────────────────────────────────────────────────────────────┐
  │                                    Sanic v22.6.2                                     │
  │                           Goin' Fast @ http://0.0.0.0:8000                           │
  ├───────────────────────┬──────────────────────────────────────────────────────────────┤
  │                       │        mode: debug, single worker                            │
  │     ▄███ █████ ██     │      server: sanic, HTTP/1.1                                 │
  │    ██                 │      python: 3.10.9                                          │
  │     ▀███████ ███▄     │    platform: Linux-6.1.7-100.fc36.x86_64-x86_64-with-glibc2. │
  │                 ██    │              35                                              │
  │    ████ ████████▀     │ auto-reload: enabled                                         │
  │                       │    packages: sanic-routing==22.3.0                           │
  │ Build Fast. Run Fast. │                                                              │
  └───────────────────────┴──────────────────────────────────────────────────────────────┘

[2023-03-01 20:13:34 -0500] [1401809] [DEBUG] Dispatching signal: server.init.before
[2023-03-01 20:13:34 -0500] [1401809] [DEBUG] Dispatching signal: server.init.after
[2023-03-01 20:13:34 -0500] [1401809] [INFO] Starting worker [1401809]
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.begin
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.read_head
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.request
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.handle
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.routing.before
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.routing.after
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.middleware.before
this is request middleware
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.middleware.after
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.middleware.before
this will not fire after in 22.9.0 and after
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.middleware.after
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.response
[2023-03-01 20:13:38 -0500] - (sanic.access)[INFO][127.0.0.1:39236]: GET http://localhost:8000/  200 17
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.send
[2023-03-01 20:13:38 -0500] [1401809] [DEBUG] Dispatching signal: http.lifecycle.begin
[2023-03-01 20:13:43 -0500] [1401809] [DEBUG] KeepAlive Timeout. Closing connection.

Updating pyproject.toml to use Sanic 22.9.0, reinstalling dependencies and running again exactly as above shows the middleware no longer runs for us. I also tried removing and recreating the venv, but still had the same results.

$ poetry run python server.py
[2023-03-01 20:16:31 -0500] [1411105] [INFO] 
  ┌──────────────────────────────────────────────────────────────────────────────────────┐
  │                                    Sanic v22.9.0                                     │
  │                           Goin' Fast @ http://0.0.0.0:8000                           │
  ├───────────────────────┬──────────────────────────────────────────────────────────────┤
  │                       │        mode: debug, single worker                            │
  │     ▄███ █████ ██     │      server: sanic, HTTP/1.1                                 │
  │    ██                 │      python: 3.10.9                                          │
  │     ▀███████ ███▄     │    platform: Linux-6.1.7-100.fc36.x86_64-x86_64-with-glibc2. │
  │                 ██    │              35                                              │
  │    ████ ████████▀     │ auto-reload: enabled                                         │
  │                       │    packages: sanic-routing==22.8.0                           │
  │ Build Fast. Run Fast. │                                                              │
  └───────────────────────┴──────────────────────────────────────────────────────────────┘

[2023-03-01 20:16:31 -0500] [1411105] [DEBUG] Starting a process: Sanic-Server-0-0
[2023-03-01 20:16:31 -0500] [1411105] [DEBUG] Starting a process: Sanic-Reloader-0
[2023-03-01 20:16:31 -0500] [1411116] [INFO] Starting worker [1411116]
this is request middleware
[2023-03-01 20:16:36 -0500] - (sanic.access)[INFO][127.0.0.1:46656]: GET http://localhost:8000/  200 17
[2023-03-01 20:16:41 -0500] [1411116] [DEBUG] KeepAlive Timeout. Closing connection.
[2023-03-01 20:16:41 -0500] [1411116] [DEBUG] KeepAlive Timeout. Closing connection.

If you don't see anything obvious, we'll start digging into the code.

@ahopkins
Copy link
Member

ahopkins commented Mar 2, 2023

I am not experiencing that
image

@ahopkins
Copy link
Member

ahopkins commented Mar 2, 2023

Oh.... I see. 🤔 The difference was I had sanic-ext in my environment. Going to a clean env without that I see what you are seeing.

So the reason is that you are altering the router after it has tried to optimize itself. I will add something to the documentation about this.

Add this:

    app.router.reset()
    app.register_middleware(this_fails_in_22_9_0_and_after, "request")
    app.finalize()

@ahopkins
Copy link
Member

ahopkins commented Mar 2, 2023

@semmypurewal I am adding a convenience for this into the next release:

@app.before_server_start
async def dynamic_router_changes(app: Sanic) -> None:
    with app.amend():
        app.add_route(...)
        app.register_middleware(...)

@Tronic
Copy link
Member

Tronic commented Mar 2, 2023

Couldn't that be handled internally within the router whenever any changes are done? Keep track of when it has already optimized and whether it needs to do that again, and/or postpone optimization until after server start?

@ahopkins
Copy link
Member

ahopkins commented Mar 2, 2023

Couldn't that be handled internally within the router whenever any changes are done?

This could work for before_server_start, and anytime after that would take more consideration. This comes back to the discussion about dynamically adding and removing routes, which has been discussed before (here, here, and here).

That is not a bad idea, but I still like the idea of wrapping this so that we an expose an API that allows us to continue to modify what happens inside and keep a constant external API.

@semmypurewal
Copy link
Author

Wow, thanks for the quick turnaround on this! I can confirm that the same issue appears on my Mac. I also can confirm that your suggested code change resolves the issue.

Can you clarify: is this a bug or just an inadvertent change? Or was this change expected? I'm wondering if this change should have been introduced as a major version update?

I have 3 Sanic apps that I maintain, and this particular change affects two of the 3, meaning that they will no longer work if I update to 22.9+ without code changes. Does this mean all production apps that use this pattern will break on an update?

In our case we have a few submodules that add middleware. From some quick testing, it looks like we'll have to update all of those callsites as well as the main app.

I agree with @Tronic that it might be better to hide this from the user for two reasons: (1) so that existing apps that use this pattern will continue to work as expected (at least until a major version update), and (2) the proposed change seems to leak implementation details of the underlying framework (e.g. a user has to understand when these router optimizations happen, and then slightly change their code depending on whether it has already happened or not).

In any case, thanks for your help! This gives us a workaround in the short term if we decide to upgrade.

@sjsadowski
Copy link
Contributor

Hey @semmypurewal I want to jump in real quick and interject a) thanks for using Sanic! and b) a couple of notes

First is that we do try our very best to include deprecation notice two release cycles prior to breaking changes. I'm coming in late and have not looked over the whole issue or dug in to it, so I do want to say that a non-LTS release may have breaking changes. We do our very best to limit this and when we know about it, advise the community that it's coming.

With that being said, my suggestion may be to move to 22.12 LTS which, for production apps, is supported for 2 years (thru 2024) and means you should not have any breaking changes to worry about, and we will backport important fixes and security fixes.

Here's the release policies, and please reach out on discord (if you haven't already) - happy to help.

@semmypurewal
Copy link
Author

@sjsadowski Ah, got it! Thank you. I was thinking you were using semantic versioning but now I see you're not.

We will look into migrating to an LTS.

@miss85246
Copy link

Thanks god, I thought I was the only one who had such a problem.
I found the middleware's will work If I define the middleware in the same context with app, like this:

from sanic import Sanic
from sanic_jwt import Initialize

from config import Config
from helpers import ChatGptBot, DBClient
from middlewares import RequestMiddleware, ResponseMiddleware
from views import server_bp, jwt_bp

app = Sanic('chat-gpt')
Initialize(app, **Config.JWT_CONFIG, **jwt_bp)
app.blueprint(server_bp)


@app.listener('before_server_start')
async def start_listener(_, __):
    app.ctx.bot = ChatGptBot(Config.OPENAI_KEYS)
    app.ctx.database = await DBClient(**Config.DB_CONFIG)



@app.middleware('request')
async def create_database_session_middleware(request):
    db_session = request.app.ctx.database.maker()
    request.ctx.db_session = db_session


@app.middleware('response')
async def close_database_session_middleware(request, _) -> None:
    await request.ctx.db_session.close()


@app.listener('before_server_stop')
async def stop_listener(_, __):
    await app.ctx.database.close()
    app.ctx.bot.close()


if __name__ == '__main__':
    app.run(**Config.SERVER_CONFIG)

After listening to @ahopkins's explanation, I know why this happened.
Hope to fix this problem as soon as possible. 😸

@ahopkins
Copy link
Member

ahopkins commented Mar 5, 2023

@miss85246 Oooo exciting
image

First one of these I've seen so far. Interested to see how this turns out.

@ahopkins
Copy link
Member

Couldn't that be handled internally within the router whenever any changes are done?

@Tronic The PR does this. It uses the application and routers' states to determine what to do. It will happen all implicitly now.

@app.before_server_start
async def setup(app: Sanic):
    app.exception(ZeroDivisionError)(zero)
    app.add_route(handler, "/foo")
    app.on_request(on_request)
    app.add_signal(signal, "http.lifecycle.request")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants