Lua Log #6: LASGI - ASGI in Lua

In Atlas, I’m implementing a full web framework. I’m also taking on the challenge of building a web server to pair with the web framework and the application that the framework produces.

How I should I connect the web server and the web application? There’s an interface between those two systems. Since I know a lot about the Python web ecosystem, I know that some very smart developers have given considerable thought to the interface layer between servers and applications. In fact, I know that the current standard, ASGI, took years to draft and develop.

Knowing that, I decided that the quickest way to get a working interface is to try to get ASGI to work in a Lua context. I’ve branded my effort as LASGI for Lua’s Asynchronous Server Gateway Interface.

There are some fundamental differences between Python and Lua that mean that LASGI has to adapt from the ASGI spec.

  • Lua doesn’t have async and await.
  • Lua doesn’t have the bytes and Unicode string separation that Python 3 has.

In the ASGI interface, an ASGI application is called with three things:

  • A dictionary of scope with information about the incoming connection.
  • A receive and send pair of awaitable callables.

Because of Lua’s lack of async and await, I need to make the receive and send functions work as coroutines in their implementation. I showed how that’s possible in my last log entry when I adapted callback interfaces to look like synchronous code. The nice part about applying this pattern for the LASGI interface is that these functions should appear like synchronous code to the calling application.

I’m not sure how I’m going to deal with the bytes and strings differences between Python and Lua. I imagine that the experience will be something like Python 2 when Python strings lacked the same distinction between bytes and Unicode like how Lua strings behave. I’ll probably ignore the problem for now (and regret it later).

By the end of the week, I got the interface into place with stubbed out behavior on both the server side and the application side.

On the server side, I have hardcoded the scope table containing all the request information. The receive callable is also hardcoded to provide virtually no useful data to the application that is suppose to call that function and process the resulting event. I have handled the send callable for basic HTTP responses. The server can handle a response event that the application passes to send and transform the event into the HTTP data that goes on the wire to the browser.

On the application side, the application is sending a hardcoded “Hello World!” response back to the server. The application also followed the ASGI spec for HTTP and calls receive once and send twice (for the http.response.start and http.response.body events).

My favorite part of this process so far is to fire up the server and use hey to see what responses come back. At this stage, benchmarks are completely rubbish since the framework doesn’t do anything useful, but it’s fun to see the server “handle” real traffic.

atlas git:(main) hey

  Total:	0.0095 secs
  Slowest:	0.0051 secs
  Fastest:	0.0002 secs
  Average:	0.0019 secs
  Requests/sec:	21110.0933

  Total data:	2400 bytes
  Size/request:	12 bytes

Response time histogram:
  0.000 [1] 	|
  0.001 [13]	|■■■■■■
  0.001 [25]	|■■■■■■■■■■■
  0.002 [89]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.002 [2] 	|■
  0.003 [19]	|■■■■■■■■■
  0.003 [25]	|■■■■■■■■■■■
  0.004 [11]	|■■■■■
  0.004 [9] 	|■■■■
  0.005 [3] 	|■
  0.005 [3] 	|■

Latency distribution:
  10% in 0.0008 secs
  25% in 0.0012 secs
  50% in 0.0013 secs
  75% in 0.0028 secs
  90% in 0.0034 secs
  95% in 0.0040 secs
  99% in 0.0049 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0003 secs, 0.0002 secs, 0.0051 secs
  DNS-lookup:	0.0000 secs, 0.0000 secs, 0.0000 secs
  req write:	0.0000 secs, 0.0000 secs, 0.0004 secs
  resp wait:	0.0014 secs, 0.0001 secs, 0.0033 secs
  resp read:	0.0000 secs, 0.0000 secs, 0.0007 secs

Status code distribution:
  [200]	200 responses

The next phase of this journey is a deep dive into the application and framework side. My focus will be on building out the routing layer that process and incoming connection, look for a matching route, and pass off to a controller that will handle an HTTP request to return a response.

This effort will be my East meets West moment like the transcontinental railroad in the US, because I’ll be able to connect my server work all the way to the template engine that I built. Once that is working, the framework will really start heating up!

Thanks for reading! Have questions? Let me know on X at @mblayman.