Repository: sanic-org/sanic

Issue:
Transport may remain open after `close()`
**Describe the bug**
After applying the workaround in #2189 Sanic hangs on shutdown if it tries to force close SSL connection after `GRACEFUL_SHUTDOWN_TIMEOUT` expires. Digging into the code shows that the transport does not close after `close()` has been called but stays open for a prolonged time (I guess it will timeout eventually), and `connection_lost` is not called.

There are other reports of the same issue with asyncio SSL transports on the web, i.e. this [stack overflow thread](https://stackoverflow.com/questions/65168635/python-asyncio-ssl-transport-not-closing). I don't think this is really a bug on Sanic's end, but more of a pitfall with the transport.

Changing `HttpProtocol::close` to call `abort()` instead of `close()` fixes the issue. As this is a non-graceful shutdown of the connection anyway I think the change would be acceptable, but of course something more elaborate (like waiting another timeout before aborting the transports) would be possible, too.

I can prepare a PR if you like.

**Code snippet**
This can be reproduced the same way as #2189 --- launch a simple server with SSL, connect using Chrome and terminate. If

```
    @app.listener("after_server_stop")
    async def afterServerStop(app, loop):
        await asyncio.gather(*[task for task in asyncio.all_tasks() if task != asyncio.current_task()])
```

is added to the server it will hang on exit. Debugging shows that `connection_lost` is never called.


**Expected behavior**
The transport closes.


**Environment (please complete the following information):**
 - OS: MacOS
 - Version: 21.6.0



Discussion:
PRs are always welcome.
There you go ;) 

Resolving patch:
diff --git a/sanic/server/protocols/base_protocol.py b/sanic/server/protocols/base_protocol.py
--- a/sanic/server/protocols/base_protocol.py
+++ b/sanic/server/protocols/base_protocol.py
@@ -81,13 +81,24 @@ async def receive_more(self):
         self._data_received.clear()
         await self._data_received.wait()
 
-    def close(self):
+    def close(self, timeout: Optional[float] = None):
         """
-        Force close the connection.
+        Attempt close the connection.
         """
         # Cause a call to connection_lost where further cleanup occurs
         if self.transport:
             self.transport.close()
+            if timeout is None:
+                timeout = self.app.config.GRACEFUL_SHUTDOWN_TIMEOUT
+            self.loop.call_later(timeout, self.abort)
+
+    def abort(self):
+        """
+        Force close the connection.
+        """
+        # Cause a call to connection_lost where further cleanup occurs
+        if self.transport:
+            self.transport.abort()
             self.transport = None
 
     # asyncio.Protocol API Callbacks #
diff --git a/sanic/server/runners.py b/sanic/server/runners.py
--- a/sanic/server/runners.py
+++ b/sanic/server/runners.py
@@ -180,7 +180,7 @@ def serve(
             if hasattr(conn, "websocket") and conn.websocket:
                 coros.append(conn.websocket.close_connection())
             else:
-                conn.close()
+                conn.abort()
 
         _shutdown = asyncio.gather(*coros)
         loop.run_until_complete(_shutdown)


Test patch: