API Innovation

231 views
Skip to first unread message

Tim Caswell

unread,
Aug 30, 2012, 11:49:18 AM8/30/12
to lu...@googlegroups.com
I've been exploring new APIs that deviate from what nodejs has had
historically with great success. I wrote a simple web module that
replaces the http module. It's easier to use and a *lot* faster.
(granted it's not 100% done and will probably not as fast in the end).

## Proposed "web" Interface

I propose we remove the "http" module from core and maintain it as a
third-party library for people wanting the node API.

I want to make a new module called "web". We can possible keep this
external as well. Ideally it only depends on a socket connection
(duplex stream) and the http_parser binding. Currently I get the
stream by using uv.Tcp directly.

The main interface to the web module is the "app" function. This API
is borrowed heavily from wsgi, rack, and stratajs. An app is defined
as a function.

local function app(request, respond)
local body = "You requested " .. request.url.path .. "\n"
respond(200, {
["Content-Type"] = "text/plain",
["Content-Length"] = #body
}, body)
end

The `request` table will contain the output of the http_parser as well
as the original socket.

- socket: The raw duplex socket over which we're doing http. This
implements the stream interface. Websocket implementors will want to
use this.
- method: The http method as a string ("GET", "POST", etc..)
- url: A table that's the result of http_parser's parseUrl function.
- url.path - The path with query string removed ("/foo/bar")
- ... other properties from parseUrl
- shouldKeepAlive: A flag set by http_parser
- upgrade: A flag set by http_parser
- ... other properties set by http_parser's info table

Also, the `request` table itself will be a readable stream that
outputs the http body data (chunked encoding, etc will be already
decoded)

As seen in the example, `respond` is a function that accepts the http
response status code, the response headers as a table, and the body.
The body can be either a string or a readable stream.

Using this new interface, making middleware modules is trivially easy.
If, for example, you want to wrap an app with a middleware that logs
all requests, it can be defined like this:

-- The "log" module
return function (app)
return function (req, res)
app(req, function (code, headers, body)
print(req.method .. ' ' .. req.url.path .. ' ' .. code)
res(code, headers, body)
end)
end
end

And used like this:

-- Replace our app function with a new function that logs all requests.
app = require('log')(app)

Instead of creating the tcp server automatically, the http module will
just provide a way to wrap the app into a stream handler function.
(not implemented yet)

-- Example using the net module to get the duplex stream over tcp
connections
-- Assume `app` from above
local handler = web.createStreamHandler(app)
net.createStream(handler):listen(8080)

I haven't implemented that last part, but I think decoupling http from
tcp is a good thing. I'll leave it up to web frameworks to create the
convenience functions for auto-creating the tcp/tls servers.

In initial benchmarking, this new interface is extremely fast. About
10x faster than our current http module. Part of this is because I'm
skipping the net module and using uv.Tcp directly. And part is
because the new API is easier to implement and requires a lot less lua
code. (See the examples/web-app/ folder in luvit's source for my
prototype.) Initial Benchmarks at https://gist.github.com/3517969

## Proposed readable stream Interface.

The current readable stream interface that we borrowed from node has
two events ("data" and "end") and two methods (.pause() and
.resume()). The problem with this interface is data events may emit
before you've had a chance to start listening for them and you will
lose data. This is especially common in a middleware system where a
user is trying to upload a file, but we first need to make some async
calls against a db to validate the upload. While the auth layer is
checking the request, the upload data gets emitted. The upload module
never gets to see the events!

The new interface works instead by pulling data out of the stream.

local stream = fs.createReadStream("myfile.txt")

local function onRead(chunk)
if chunk then
-- This was a "data" event
-- read the next chunk
stream.read(onRead)
else
-- We're done! This would have been an "end" event.
end
end
stream.read(onRead)

Backpressure is exerted if you don't call stream.read in the same tick
as the onRead event. It will pause the upstream source and resume it
later when .read() is called again.

WIth this new interface, middleware modules that don't care about the
input stream, won't have to care about it. The data won't start
flowing till the first middleware asks for it.

---------------------

I have other ideas as well, but let's get these two right first. They
will form a solid base for streaming http servers that are easy to
write and compose libraries.

Please give feedback.

-Tim Caswell

Vladimir Dronnikov

unread,
Aug 30, 2012, 12:28:52 PM8/30/12
to lu...@googlegroups.com
These two would be great

--dvv

Michal Kolodziejczyk

unread,
Aug 31, 2012, 3:46:54 AM8/31/12
to lu...@googlegroups.com
On 30.08.2012 17:49, Tim Caswell wrote:

> Instead of creating the tcp server automatically, the http module will
> just provide a way to wrap the app into a stream handler function.

This is great, I have implemented something sililar sime time ago. Two
selling points for me are:
- you can test HTTP applications in plain lua (without using sockets),
so really fast
- you can use the same middleware application with other webserver
(ouside of luvit) to make speed comparisons or make app more portable.

While we are at it, I have a suggestion to simplify url.parse(): today
the info.query field can be a string or a table (based on a switch),
which annoys me. I would suggest to leave info.query as a string, and to
make additional info.params as a table if parsing is requested.

> ## Proposed readable stream Interface.
Is this idea based on luasocket ltn12
http://lua-users.org/wiki/FiltersSourcesAndSinks ? When I first looked
at luvit EventEmitter model, I liked it more than ltn12 because ltn12
source could signal only "data" or "end" requests (with events like
errors being kind of "hacks"), while EventEmitter was free to emit any
signal with the same interface.

> The new interface works instead by pulling data out of the stream.
>
> local stream = fs.createReadStream("myfile.txt")
>
> local function onRead(chunk)
> if chunk then
> -- This was a "data" event
> -- read the next chunk
> stream.read(onRead)
> else
> -- We're done! This would have been an "end" event.
> end
> end
> stream.read(onRead)
And how would you signal "error" event?
> Backpressure is exerted if you don't call stream.read in the same tick
> as the onRead event. It will pause the upstream source and resume it
> later when .read() is called again.
>
> WIth this new interface, middleware modules that don't care about the
> input stream, won't have to care about it. The data won't start
> flowing till the first middleware asks for it.
I like the above idea. With one exception that I would like to see
explicit signals ("data", "end", "error") instead of guessing what the
signal was based on the data returned.

Regards,
miko

Vladimir Dronnikov

unread,
Aug 31, 2012, 5:29:01 AM8/31/12
to lu...@googlegroups.com

chunk == false could mean error

31.08.2012 11:46 пользователь "Michal Kolodziejczyk" <mic...@gmail.com> написал:

Tim Caswell

unread,
Aug 31, 2012, 8:29:40 AM8/31/12
to lu...@googlegroups.com
While playing with "continuables" syntax last night, I implemented
slightly different streams. In the new interface, streams are super
simple.

Readable Stream:

A readable stream is any table that has a :read()(callback) method.
The callback will be called with (err, chunk). The three event types
can be encoded as thus:

- Data: callback(nil, data)
- EOF: callback() or callback(nil, nil)
- Error: callback(err)

The readable stream doesn't even have to be an Emitter instance or
have a .readable property. The existence of the :read()(callback)
method denotes that it's readable.

Writable Stream:

Likewise a writable stream is any table that has a
:write(chunk)(callback) method. The callback will be called with
(err). The three possible responses are:

- Error: callback(err)
- Write, but buffer full: callback() or callback(nil) on a later tick
when the buffer is ready for more input.
- Write, buffer still has room: callback() or callback(nil) is called
before the call to the continuable returns.

Combining this new interface with my new proposed wait and await based
fiber module, piping from one stream to another is as simple as.

repeat
local chunk = await(input:read())
await(output:write(chunk))
until not chunk

That's it! The non-fiber version would be:

local function pipe(input, output, callback)
local consume, onRead, onDone
consume = function (err)
if err then return callback(err) end
input:read()(onRead)
end
onRead = function (err, chunk)
if err then return callback(err) end
output:write(chunk)(chunk and consume or callback)
end
consume()
end

Notice that the chunk value of `nil` for eof in the readable matches
the `nil` as eof argument in the writable. This symmetric API keeps
things simple.

For the full continuable, stream, fiber example, see the new "stream"
example in the luvit examples folder.
https://github.com/luvit/luvit/blob/master/examples/stream/test.lua

Tim Caswell

unread,
Sep 13, 2012, 1:05:34 AM9/13/12
to lu...@googlegroups.com
Alright, so after talking to everyone, I decided that it's not a good
idea to break all APIs in luvit right now. Instead I've pushed my
innovation (aka experimentation) to userspace as the moonslice
project.

If everyone likes this then eventually it can be pushed into core, but
I'm not in a rush.

## Continuable

This module provides a continuable interface to libuv. Continuables
are async functions that return continuations instead of taking the
callback as the last arg. The continuation is just a function that
takes the callback.

local fs = require('continuable').fs
local continuation = fs.open(filename, "r")
continuation(function (err, fd)
-- do something
end)

These work particularly nice with the latest fiber module included in
continuable when within a coroutine.

local continuation = fs.open(filename, "r")
local fd = await(continuation)

See a working example at
https://github.com/creationix/moonslice/blob/master/test-coro-fs.lua

## Continuable based streams

In this new stream interface a readable stream is nothing more than
any table with a :read() method. This method takes no args and
returns a continuation. In the callback, there will be (err, chunk).
This corresponds to three events from the node style readable streams.

callback(err) -> "error" happened
callback(nil, data) -> "data" happened
callback() -> "end" happened

Writable streams are the same. They are any table that has a
:write(chunk) method. This method will return a continuation with
possible (err). If you want to signal the end of the stream, call
without the chunk as :write().

Combining this with await based resolving of the continuations, making
a pipe loop with proper backpressure is as simple as:

repeat
local chunk = await(input:read())
await(output:write(chunk))
until not chunk

When the input stream reaches EOF, chunk will be nil. We pipe this
EOF event automatically when we call output:write(nil), but then the
repeat..until loop terminates.

## Web

In addition to new streams and new callbacks, I designed a new web
interface meant to replace the http module. In this new library, a
web app is defined using a very wsgi style interface:

local function app(request, respond)
respond(200, {
["Content-Type"]: "text/plain"
}, "Hello World\n")
end

The request table is itself a readable stream with a :read() method.
It also contains properties like .method, .headers, and the other http
request information.

The respond function accepts (code, headers, body) where body can be a
string or a readable stream.

In this new interface, functional composition of middleware libraries
is super easy and doesn't require external helper libraries like the
step module.

A logging middleware could be implemented as:

return function (app)
return function (req, res)
app(req, function (code, headers, body)
print(req.method .. ' ' .. req.url.path .. ' ' .. code)
res(code, headers, body)
end)
end
end

Which is then used in the main app to wrap and replace app:

app = require('./log')(app)

Once you've finished wrapping your app in middleware modules, just
create any stream and attach the web http parser to it.

local tcp = require('continuable').tcp
local web = require('web')

tcp.createServer(host, port, web.socketHandler(app))

Since the result of web.socketHandler is just a function for handling
stream connections, any kind of stream can be used with it. This
makes the module portable to other systems or unit tests.

Comments? Questions? Ideas? Let me know.

Tim Caswell

unread,
Sep 13, 2012, 1:20:43 AM9/13/12
to lu...@googlegroups.com
I forgot to link to the code.

https://github.com/creationix/moonslice: A sample using all the libraries, someday this will be a framework like express or strata. 
https://github.com/luvit/continuable: The continuable interface for libuv in luvit.
https://github.com/luvit/web: The http parser and middleware system for luvit (depends on continuable style APIs)
https://github.com/luvit/web-static: A fairly complete static file serving middleware.  luvit.io is running on this.
https://github.com/luvit/web-autoheaders: Implements common important http header stuff like Date header, keepAlive and chunked encoding.
https://github.com/luvit/web-log: A super simple logging middleware
Reply all
Reply to author
Forward
0 new messages