multi-server broken in 22.12
militantwalrus opened this issue · comments
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
If running a simple sanic server script with multi-serve
under sanic==22.12.0, the "second" port configured is always "broken".
Works fine under sanic==22.9.0 (pythons 3.8.5, 3.11.0)
An OSError was detected on startup. The encountered error was: [Errno 98] error while attempting to bind on address ('127.0.0.1', 9000): address already in use
Traceback (most recent call last):
File "/opt/python/3.8.5/lib/python3.8/site-packages/sanic/mixins/startup.py", line 1083, in _start_servers
server_info.server = await serve(**serve_args)
File "/opt/python/3.8.5/lib/python3.8/site-packages/sanic/server/async_server.py", line 121, in __await__
self.server = task.result()
File "uvloop/loop.pyx", line 1790, in create_server
OSError: [Errno 98] error while attempting to bind on address ('127.0.0.1', 9000): address already in use
Code snippet
#!/opt/python/3.8.5/bin/python3
from sanic import Sanic
from sanic.response import text
app = Sanic("main")
ctl = Sanic("control")
app.prepare(port=8080)
ctl.prepare(port=9000)
@app.get("/")
async def index(request):
return text("index")
@ctl.get("/control")
async def ctl(request):
return text("control")
if __name__ == "__main__":
Sanic.serve()
Expected Behavior
No response
How do you run Sanic?
As a script (app.run
or Sanic.serve
)
Operating System
Linux (openSUSE Leap 15.2)
Sanic Version
Sanic v22.12.0 sanic-routing==22.8.0, sanic-ext==22.12.0
Additional context
No response
Your example can be fixed by moving the definition of the applications into the __main__
block.
from sanic import Sanic
from sanic.response import text
app = Sanic("main")
ctl = Sanic("control")
@app.get("/")
async def index(request):
return text("index")
@ctl.get("/control")
async def control(request):
return text("control")
if __name__ == "__main__":
app.prepare(port=8080)
ctl.prepare(port=9000)
Sanic.serve()
In the context of my work (where the application isn't as simple as the example), I tried arranging the code to follow the order of your "fixed" example, where .prepare()
comes after both the calls to Sanic(name)
and the definition and attach of the routes. This however, got me errors which suggested the use of AppLoader
.
sanic.exceptions.SanicException: Sanic app name 'control' not found.
App instantiation must occur outside if __name__ == '__main__' block or by using an AppLoader.
See https://sanic.dev/en/guide/deployment/app-loader.html for more details.
I assume that the outside if __name__
in the error message should actually be of, not if.
My __main__
block in the real app looks more like:
if __name__ == "__main__":
AppBuilder.build() # parses a config file, gets all relevant app names and ports from config
Sanic.serve()
All calls to some_app = Sanic("some_name)
[there are 2 such] and all calls to .prepare()
have happpened inside of AppBuilder.build()
and so have executed prior to the call to Sanic.serve()
, but within the __name__ == "__main__"
block.
Can you give any further clarification on what needs to happen in such a situation?
If the answer is AppLoader
: https://sanic.dev/en/guide/deployment/app-loader.html
(I tried a "straightforward" conversion, which turned out to be not-so-straightforward),
Could you give some rules of the road for a multi-app, multi-port application?
The example for AppLoader
seems to support a single Sanic app, but how to use that pattern to set up multiple apps (on different ports) like my simplified example?
Thank you very much.
@militantwalrus How about something like this:
from typing import Any
from sanic import Sanic
from sanic.response import text
class EarlyPrepareSanic(Sanic):
def early_prepare(self, **kwargs: Any) -> None:
self.ctx.prepare_kwargs = kwargs
class AppContainer:
def __init__(self, *apps: EarlyPrepareSanic) -> None:
self.apps = apps
def prepare(self) -> None:
for app in self.apps:
app.prepare(**app.ctx.prepare_kwargs)
class AppBuilder:
@classmethod
def build(cls) -> AppContainer:
app = EarlyPrepareSanic("main")
ctl = EarlyPrepareSanic("control")
@app.get("/")
async def index(request):
return text("index")
@ctl.get("/control")
async def control(request):
return text("control")
app.early_prepare(port=8080, fast=True)
ctl.early_prepare(port=9000, fast=True)
return AppContainer(app, ctl)
app_container = AppBuilder.build()
if __name__ == "__main__":
app_container.prepare()
Sanic.serve()
Without knowing your architecture, I am taking some guesses here. It does not seem like the AppBuilder.build
class method returned anything, so I created a container object to hold the application instances. I am sure you can tweak this to fit your needs.
The goal is to:
- Get the calls to
Sanic(...)
to happen in the global scope so that they are registered. This avoids the complexity of having to deal with theAppLoader
yourself. If you would like to tuckAppBuilder.build
back into the__main__
block, then LMK and I can help you with that solution. - Since
prepare
needs to be only in the main process, we will just tuck away the arguments until they are needed. This allows the server definition to be done inside theAppBuilder.build
, which is no longer only in the main process. TBH, I kind of like this sort of lazy pattern and maybe it is worth adding something similar toearly_prepare
for these sorts of cases.
LMK your thoughts. Happy to help you iterate on this. Might be easier to go back and forth if you ping me on Discord though.
You're fairly close to what I'm doing; the principle difference is that the addition of the route methods, and the .prepare()
are happening in AppContainer.__init__()
- the ordering of events is otherwise the same, however.
But yes, we'd want AppBuilder.build()
in that __main__
. I'm afraid I don't quite see how the (global) scoping thing is meant to work (what the expectations and rules are). startup.py and the _app_registry
? Haven't groked the full path yet.
Aha, found the discord channel.
Thank you!