Notification when failure to bind to port


#1

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.


#2

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


#3

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


#4

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

#5

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:


#6

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()

#7

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.


#8

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.)


#9

You should decrease the $TTL in there :sweat_smile: