pallets / quart

An async Python micro framework for building web applications.
https://quart.palletsprojects.com
MIT License
3k stars 162 forks source link

Run `before_serving` and `while_serving` hooks in order of registration #358

Open FichteFoll opened 1 month ago

FichteFoll commented 1 month ago

I'm building an application where I want to intialize a few things and I am registering the while_serving and before_serving hooks in my create_app, i.e. the main prodedure that generates the application. Some of these hooks have dependencies on others, e.g. settings must be loaded & validated first, then the connection to the database is established and then I do some initialization based on the database. before_serving hooks are executed in order, so that is good.

However, because I want to register the database setup in a while_serving so that I can also easily and precisely release the database resources (registered in the same generator function) once the application shuts down, the FIFO registering of hooks isn't working anymore because all before_serving hooks are called first and only then are the while_serving hooks executed/iterated. For me, it would be ideal if they were instead executed in the order of their registration.

I haven't mentioned after_serving yet and that's because I don't actually use it currently, but if I were to I believe that the the same ordering should apply, i.e. "unload" while_serving hooks and after_serving hooks in the order they were registered.

The abstract example code that illustrates this problem is as follows:

    app.before_serving(check_settings)
    # Uses settings
    app.while_serving(init_db)
    # Uses database
    app.before_serving(exporter.init_metrics)  # fails because `init_db` is executed later

My current workaround, after splitting the init_db generator into two functions:

    app.before_serving(check_settings)
    # Uses settings
    app.before_serving(init_db)
    app.after_serving(uninit_db)
    # Uses database
    app.before_serving(exporter.init_metrics)
FichteFoll commented 1 month ago

I just realized that you would probably want to unload the while_serving hooks in reverse order, much like a stack, which doesn't fare well with my proposal. This is not something that quart is currently doing, but it might be worth considering. The problem is that this makes determining the after serving hook order much harder.

FichteFoll commented 3 days ago

Thinking about this again randomly, unwinding the stack in reverse order for unloading is actually quite reasonable, if you think of a while_serving exactly like I did in my workaround: splitting them into before_serving and after_serving.

Implementation-wise, you would use a single stack for all lifecycle hooks, i.e. before_saving, while_serving and after_serving, to honor their registration order. Then, when executing the before hooks you would also run the while hooks in order until they yield. When unwinding the application, you would iterate the stack in reverse order and execute the after_saving hooks as well as the second parts of the paused while_serving hooks. That would be predictable and not really complicated either.