-
-
Notifications
You must be signed in to change notification settings - Fork 88
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
Ability to serve an ASGI app object directly, rather than passing a module string #35
Comments
Hi @simonw, You should be able to serve your application just writing a from datasette.app import Datasette
ds = Datasette(memory=True)
app = ds.app() and run Granian from cli with: |
Thanks - that recipe worked for me right now, and didn't return any errors. However... I really want the ability to start Granian running from my own Python scripts, in the manner shown above. The reason is that Datasette is configured by the command line. The usual way of starting it looks like this:
There are a whole ton of options like those: https://docs.datasette.io/en/stable/cli-reference.html#datasette-serve
I ran into the "can't pickle local object" error while trying to build a new plugin, The implementation of So I guess this is a feature request: I would like a documented way to programatically start a Granian server without having to use the |
@simonw using Benchmarks apps in Granian repo can show you an example: granian/benchmarks/app/asgi.py Lines 76 to 82 in e5139e2
|
The challenge with that is that my application object needs to be instantiated with additional arguments that have been provided by the user. One thing that might work is that I could code-generate a Python script file that instantiates an |
@simonw considering how granian is designed, is not possible to directly pass an application instance to the server, as it would require that every loaded object should be pickable due to Probably we can solve this with a |
That could work. I'm not sure how I'd pass in the additional arguments though. The thing I want to build is effectively a CLI script that looks something like this:
(Plus a whole bunch more options). Running this script would instantiate my existing |
@simonw wait, I got an idea to get this working with the current implementation. The Line 303 in 9265a03
You can use this to override the default behaviour to loading the application, and thus you can write something like this in a run.py file: from datasette.app import Datasette
from granian import Granian
ds_kwargs = {}
def load_app(target):
if target != "dyn":
raise RuntimeError("Should never get there")
ds = Datasette(**ds_kwargs)
return ds.app()
def main():
global ds_kwargs
ds_kwargs.update(some_util_to_convert_cli_param_to_dict())
srv = Granian("dyn", address="127.0.0.1", port=8002, interface="asgi")
srv.serve(target_loader=load_app)
if __name__ == "__main__":
main() then running |
That didn't quite work - the subprocess couldn't see the I passed the arguments as serialized JSON: srv = Granian(
# Pass kwars as serialized JSON to the subprocess
json.dumps(kwargs),
address=host, Then in def load_app(target):
from datasette import cli
ds_kwargs = json.loads(target)
ds = cli.serve.callback(**ds_kwargs)
return ds.app() |
You can try the above out like this:
This will start a server on port 8000 serving the Datasette interface. |
@simonw probably wrapping works and is a cleaner solution. Eg: from datasette.app import Datasette
from granian import Granian
def app_loader(kwargs):
def load_app(target):
if target != "dyn":
raise RuntimeError("Should never get there")
ds = Datasette(**kwargs)
return ds.app()
return load_app
def main():
ds_kwargs = some_util_to_convert_cli_param_to_dict()
srv = Granian("dyn", address="127.0.0.1", port=8002, interface="asgi")
srv.serve(target_loader=app_loader(ds_kwargs))
if __name__ == "__main__":
main() |
Tried that just now but it didn't work - I got this error:
|
How about if Granian had some kind of mechanism where you could specify a pickle-able object which should be passed to each of the workers, specifically designed for this kind of use-case? |
Gonna think about it. Probably the theme here is making the |
@gi0baro Is there a solution to the problem? I want to run application without global variables.
not work |
@novichikhin what's the error? Do you have a stack trace? |
|
@novichikhin can you try with def app_loader():
from whatever import get_app_settings, register_app
settings = ...
return register_app(...) |
|
@novichikhin I don't really know how to solve the pickling issues. Maybe adding support for factories should solve your need? |
Subscribing, this issue is unfortunately a blocker for me. I currently use |
@novichikhin pickle cannot serialise closures for obvious reasons, they are not compiled into byte-code until called at least once. If you want to use settings as an argument you can either:
Both variants are applicable only for global module scope, i. e. no closures :) # runner.py
def app_loader(_: str, *, settings: AppSettings):
return register_app(settings=settings)
# __main__.py
import functools
from .runner import app_loader
settings = ...
loader = functools.partial(app_loader, settings=settings)
Granian(...).serve(target_loader=loader) |
Maybe it's little late Also, for some PoC I used this code with app object (not str) from granian.asgi import _callback_wrapper
from granian._futures import future_watcher_wrapper
from granian._granian import ASGIWorker
from granian._loops import WorkerSignal
import contextvars
import asyncio
from fastapi import FastAPI
sock = bind_socket('0.0.0.0', 9400)
loop = asyncio.new_event_loop()
fastapi_app = FastAPI()
asgi_worker = ASGIWorker(
worker_id=worker_id,
socket_fd=sock.fileno(),
threads=1,
blocking_threads=1,
backpressure=1024,
http_mode='1',
http1_opts=None,
http2_opts=None,
websockets_enabled=False,
opt_enabled=False,
ssl_enabled=False,
ssl_cert=None,
ssl_key=None,
)
wcallback = _callback_wrapper(fastapi_app, {}, {}, None)
wcallback = future_watcher_wrapper(wcallback)
sock.listen()
asgi_worker.serve_wth(wcallback, loop, contextvars.copy_context(), WorkerSignal()) # or serve_rth all things are stolen from: Line 212 in 80b80cb
|
While this is true for Linux, it won't work on other platforms, which is something to be aware of.
It's not clear to me the advantage in the proposed solution, given you're limited to 1 worker and you're sacrificing the entirety of Granian processes management (SIGHUP, workers reload, worker respawn in case of crashes, just to name a few of them). Also, as I wrote in #330
So let's just say, in general, I won't support issues caused by this kind of usage :) |
In my specific case, we have a complex legacy project with its own supervisor process. In addition to managing the worker, the master process also receives some data and distributes it to the workers through pipes. So I need to create worker processes myself and pass pipes as arguments |
@gi0baro Is this the correct ticket to monitor for eventually adding a CLI option allowing Granian to accept an app factory function name? I'm currently using your suggested workaround (and it works great), but I want to keep an eye out for a more official method. Thanks! |
Not really, please open up a separate issue for that. |
This is a very exciting project - thanks for releasing this!
I tried to get it working with my https://datasette.io/ ASGI app and ran into this error:
Here's the script I wrote to replicate the problem, saved as
serve_datasette_with_granian.py
:Run it like this to see the error (run
pip install datasette
first):Are there changes I can make to Datasette to get this to work, or is this something that illustrates a bug in Granian?
Relevant Datasette code is here: https://github.com/simonw/datasette/blob/6a352e99ab988dbf8fd22a100049caa6ad33f1ec/datasette/app.py#L1429-L1454
It's applying my
asgi-csrf
ASGI middleware from https://github.com/simonw/asgi-csrfFunding
The text was updated successfully, but these errors were encountered: