unit icon indicating copy to clipboard operation
unit copied to clipboard

Feature request: Support application-factory pattern for Python apps

Open bunny-therapist opened this issue 2 years ago • 2 comments

"Application factory" means that the "app" object in your python code that you are passing to unit is not the app itself, but a callable that needs to be called to create the app, i.e. the_actual_app = app().

Uvicorn supports this with the --factory option: https://www.uvicorn.org/settings/ Gunicorn also supports this by specifying the app with parentheses: "app()": https://flask.palletsprojects.com/en/3.0.x/deploying/gunicorn/#running

It would be nice if unit supported this for Python apps (WSGI and ASGI). I looked in the documentation but could not find anything about this, and I could not get it to work, so I assume it is currently not supported by unit.

bunny-therapist avatar Jan 31 '24 10:01 bunny-therapist

Flask supports factory thru the magic name "create_app" or "make_app": https://flask.palletsprojects.com/en/2.3.x/patterns/appfactories/

Personally, I would much prefer factories having zero arguments and then there is a "factory" boolean in the configuration, similar to uvicorn. (I find gunicorn's solution too obscure and flask's too complicated).

bunny-therapist avatar Feb 02 '24 10:02 bunny-therapist

Thank you for filing this; we definitely want to support the application factory pattern. I especially appreciate your linking to gunicorn / uvicorn / Flask docs here.

We'll need to have a discussion to decide on the best pattern, but consider this officially on our backlog :)

callahad avatar Feb 08 '24 14:02 callahad

@callahad Does this issue need work. would be happy to contribute to it.

I have a approach which I tested on my local. Not sure , This is 100% correct or not. Here it is. Allow, to add a new parameter in the config like below:

{
    "applications": {
        "python-app": {
            "type": "python",
            "processes": 4,
            "path": "/www/",
            "targets": {
                "front": {
                    "module": "app",
                    "factory": "application_factory"
                },
                "back": {
                    "module": "app",
                    "factory": "application_factory"
                }
            }
        }
    },
    "listeners": {
        "127.0.0.1:8080": {
            "pass": "applications/python-app/front"
        }
    }
}

For, this need to make change in python related validation object like in nxt_conf_vldt_python_notargets_members and nxt_conf_vldt_python_target_members. which will validate this field.

and now in the nxt_python_set_target, we can set target, if factory option is there. by calling the application factory using PyObject_CallObject and setting returned object to the target.

If support for arguments for application factory need to be given, that can also be achieved with some additional changes

gourav-kandoria avatar Jun 17 '24 14:06 gourav-kandoria

I think what's missing is the why this is needed? and how does it relate to "app"? if you specify "factory" do you need to specify "app"?

ac000 avatar Jun 17 '24 16:06 ac000

If you are asking about why the separate "factory" option is needed. So my point is that "callable" by its name suggests that it is some callable which have interface as per wsgi or asgi standard. So, if "factory" as a different option is introduced that would mean it is some callable, if called would return callable supporting wsgi or asgi standard.

There can be other way of doing this like, if factory option is passed as true, then the callable should be considered as which factory which when called should return wsgi or asgi supporting callable

So, if we go by first way- then below is more explanation It can be like either have callable option in the config or factory option . So both must be mutually exclusive. and in both cases , module have to be specified.

so , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "factory": "application_factory"
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

If we go by the second way then , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "callable": "application_factory",
                    "factory": true
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

gourav-kandoria avatar Jun 17 '24 17:06 gourav-kandoria

If you are asking about why the separate "factory" option is needed. So my point is that "callable" by its name suggests that it is some callable which have interface as per wsgi or asgi standard. So, if "factory" as a different option is introduced that would mean it is some callable, if called would return callable supporting wsgi or asgi standard.

There can be other way of doing this like, if factory option is passed as true, then the callable should be considered as which factory which when called should return wsgi or asgi supporting callable

So, if we go by first way- then below is more explanation It can be like either have callable option in the config or factory option . So both must be mutually exclusive. and in both cases , module have to be specified.

so , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "factory": "application_factory"
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

If we go by the second way then , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "callable": "application_factory",
                    "factory": true
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

any point or suggestion on it

gourav-kandoria avatar Jun 18 '24 15:06 gourav-kandoria

OK, thanks, (it didn't help I mixed callable and app up...). But, yes, you wouldn't specify callable & factory (unless you specify factory as true..)

So I take it "factory" is some Python terminology? I probably need to go read up on it sometime...

ac000 avatar Jun 18 '24 18:06 ac000

"Factory" is just software terminology.

An "application factory" as input to a server runner is supported in a bunch of python server runners, as already described in the issue description. I am not sure about its prevalence in other languages since I don't have experience developing applications in those.

As for the discussion about format, I would personally prefer "callable" to be the path to both factory and app, and a "factory" boolean specifying if it is a factory or not. (The solution called "the second way" above.)

bunny-therapist avatar Jun 18 '24 19:06 bunny-therapist

Heh, not something I've come across in over 25 years in the world of C... ah, it's an OOP thing, well that explains everything...

ac000 avatar Jun 18 '24 19:06 ac000

If a Java design patterns textbook shows up on your doorstep, it wasn't me 🥸

callahad avatar Jun 18 '24 21:06 callahad

@gourav-kandoria If you have a patch, would you mind opening a pull request? Even if it's unpolished, having something concrete is a great starting point.

If support for arguments for application factory need to be given, that can also be achieved with some additional changes

That's probably my biggest question at the moment. Honestly, my next step that I just haven't gotten around to yet is to survey how other WSGI/ASGI servers handle this pattern. If someone wants to do that research and write up a quick summary, I'd really appreciate it.

I guess I'd at least look at gunicorn, uWSGI, and mod_wsgi on the WSGI side, and uvicorn, hypercorn, and daphne on the ASGI side?

callahad avatar Jun 18 '24 22:06 callahad

(Admittedly, I'm just cribbing from memory on the WSGI side and riffing off the Starlette docs for ASGI. If there are other popular servers not represented, do let me know)

callahad avatar Jun 18 '24 22:06 callahad

If a Java design patterns textbook shows up on your doorstep, it wasn't me 🥸

I could use a door stop!

ac000 avatar Jun 18 '24 22:06 ac000

  • Gunicorn
    • Callable: $ gunicorn -w 4 'hello:app'
    • Factory: $ gunicorn -w 4 'hello:create_app()'
    • Positional and keyword arguments can also be passed, but it is recommended to load configuration from environment variables rather than the command line.

  • uWSGI
    • Callable: $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app
    • Factory: Not supported (or is it?... the Flask docs suggest creating a shim).
    • Does not support passing arguments.
  • mod_wsgi
    • Uses WSGIScriptAlias to point to a shim that must define a callable named application
      • Can be renamed by setting WSGICallableObject
      • "It is not possible to use a dotted path to refer to a sub object of a module imported by the WSGI script file."

    • It is not possible to specify an application by Python module name alone

callahad avatar Jun 19 '24 09:06 callahad

  • Uvicorn
    • Callable: $ uvicorn main:app
    • Factory: $ uvicorn --factory main:create_app
    • No support for passing args; specifically defines a factory as a "() -> <ASGI app> callable."
  • Hypercorn
    • Callable: $ hypercorn hello_world:app
    • Does not appear to support factories
  • Daphne
    • Callable: $ daphne django_project.asgi:application
    • Does not appear to support factories

callahad avatar Jun 19 '24 09:06 callahad

So, it seems:

  1. Support for factories isn't ubiquitous (gunicorn, uvicorn, and maybe uwsgi?)
  2. Only gunicorn allows passing arguments to the factory

I find myself agreeing with @bunny-therapist's sensibilities:

I would much prefer factories having zero arguments and then there is a "factory" boolean in the configuration, similar to uvicorn. (I find gunicorn's solution too obscure and flask's too complicated).

"Explicit is better than implicit," right?

callahad avatar Jun 19 '24 11:06 callahad

Concretely: Let's add an optional factory boolean that defaults to false but can be used like:

"hello": {
    "module": "hello",
    "callable": "create_app",
    "factory": true
}

To align with common usage, we may want to consider supporting module:app notation instead of separate module and callable keys, but that's out of scope for this discussion.

callahad avatar Jun 19 '24 11:06 callahad

Amusingly, uWSGI does seem to support factories in the gunicorn style (appending () to the module name)

callahad avatar Jun 21 '24 14:06 callahad