Notification when failure to bind to port

I have a sanic application where I allow the user to pick the port they run sanic on.
I’m trying to figure out how to get something printed to the screen as a notification to the user if they’re trying to bind to a port that’s unavailable (something else is already listening on that port) or if they’re unauthorized (port is too low and they need to sudo).
Here’s the loops I’m working with:

if __name__ == "__main__":
    asyncio.set_event_loop(dbloop)
    if use_ssl:
        context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
        context.load_cert_chain(ssl_cert_path, keyfile=ssl_key_path)
        server = test.create_server(host=listen_ip, port=listen_port, ssl=context)
    else:
        server = test.create_server(host=listen_ip, port=listen_port, debug=False)
    loop = asyncio.get_event_loop()
    task = asyncio.ensure_future(server)
    db_objects.database.allow_sync = True  
    try:
        loop.run_until_complete(test_db.connect_async(loop=dbloop))
        loop.run_forever()
    except Exception as e:
        print(e)
        loop.stop()

Right now, if a user runs it and Sanic fails to bind to the port, they see nothing different than if it was successful.

1 Like

Why not check if the port is available first? There’s some code over SO that can help you with.

That just leads to potential TOCTTOU and race condition issues. Does Sanic raise any error if it fails to bind?

1 Like

Yes.

╭─adam@thebrewery ~  
╰─$ python /tmp/p.py                                                                                       (env: TEMP) 
[2018-12-15 23:43:29 +0200] [21202] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2018-12-15 23:43:29 +0200] [21202] [ERROR] Unable to start server
Traceback (most recent call last):
  File "uvloop/loop.pyx", line 1083, in uvloop.loop.Loop._create_server
  File "uvloop/handles/streamserver.pyx", line 51, in uvloop.loop.UVStreamServer.listen
  File "uvloop/handles/streamserver.pyx", line 89, in uvloop.loop.UVStreamServer._fatal_error
OSError: [Errno 98] Address already in use

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/adam/.virtualenvs/TEMP/lib/python3.6/site-packages/sanic/server.py", line 612, in serve
    http_server = loop.run_until_complete(server_coroutine)
  File "uvloop/loop.pyx", line 1422, in uvloop.loop.Loop.run_until_complete
  File "uvloop/loop.pyx", line 1599, in create_server
  File "uvloop/loop.pyx", line 1087, in uvloop.loop.Loop._create_server
OSError: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8000): address already in use
[2018-12-15 23:43:29 +0200] [21202] [INFO] Server Stopped

Yeah, well, I thought about a racing condition but I also thought that spawning so many Sanic instances would not be, well, “healthy” for your server - but perhaps you have a mammoth hardware config and are not worried about that. In any case, I’m sorry for taking such presumption.

As for running a instance of Sanic on a port that’s already being used, the response is not quite so simple. Given a code that I presume (this time I hope I’m right), like this:

import asyncio
from sanic import Sanic
from sanic.response import text

app = Sanic(__name__, configure_logging=None)


@app.route("/")
async def test_async(request):
    return text("hello, world!")


def main():
    try:
        server = app.create_server(host="0.0.0.0", port=8000)
        loop = asyncio.get_event_loop()
        task = asyncio.ensure_future(server)
        loop.run_forever()
    except Exception as e:  # we expect to get the error here, right?
        from IPython import embed
        embed()


if __name__ == "__main__":
    main()

Then I start one server. Everything works fine. In another console, though:

$ python -B test-sanic.py                
[ waiting forever ]

I think this happens because of some coroutine internals actually prevent you from catching some exceptions - you can take a look in here and here for some information.

Now, changing my code to this:

import asyncio
from sanic import Sanic
from sanic.response import text

app = Sanic(__name__, configure_logging=None)


@app.route("/")
async def test_async(request):
    return text("hello, world!")


def main():
    server = app.create_server(host="0.0.0.0", port=8000)
    loop = asyncio.get_event_loop()
    task = asyncio.ensure_future(server, loop=loop)

    def callback(fut):
        try:
            fetch_count = fut.result()
        except OSError as e:  # hmmm, this seems promising ...
            print("probably the port set is being used")
            fut.get_loop().stop()

    task.add_done_callback(callback)
    loop.run_forever()


if __name__ == "__main__":
    main()

Turns the second console call to this:

$ python -B test-sanic.py           
probably the port set is being used

So, I guess you can take on from there :wink:

2 Likes

Wouldn’t it be a much simple to attach an exception handler to the loop itself instead of the task? Btw, fut.get_loop() works? I think you might have to use fut._loop instead.

import asyncio
from sanic import Sanic
from sanic.response import text

app = Sanic(__name__, configure_logging=None)


@app.route("/")
async def test_async(request):
    return text("hello, world!")

def exception_handler(loop: asyncio.AbstractEventLoop, context):
    exception = context.get("exception")
    if isinstance(exception, OSError) and "address already in use" in str(exception):
        print("Someone has claimed your soul")
        loop.stop()

def main():
    server = app.create_server(host="0.0.0.0", port=8000)
    loop = asyncio.get_event_loop()
    loop.set_exception_handler(exception_handler)
    asyncio.ensure_future(server, loop=loop)
    loop.run_forever()


if __name__ == "__main__":
    main()
2 Likes

Yeap, at least for Python 3.7 - it has some neat new features on the asyncio package :wink:

And your solution looks very nice as well. I think both of them might work - mine just for Python 3.7 I’m affraid.

I think I need to spend some time and go over the asyncio documentation for Python3.7. I’ve been using future._loop so far like I am in the stone age. (But then again, I am still awaiting the changes to get propogated to my head.)

1 Like

You should decrease the $TTL in there :sweat_smile: