···11+#!/usr/bin/env bash
22+33+# A packaged relx release changes its working directory to the release
44+# root, which would break the ../priv relative file paths, so this runs
55+# straight from source instead.
66+rebar3 compile
77+erl -pa _build/default/lib/*/ebin \
88+ -eval "application:ensure_all_started(roadrunner_bench)" \
99+ -noshell
···11+-- POST /echo with a fixed Content-Length body, exercising the buffered
22+-- (whole-body) request read path.
33+44+wrk.method = "POST"
55+wrk.body = string.rep("a", 4096)
66+wrk.headers["Content-Type"] = "application/octet-stream"
+27
benchmark/wrk/post_echo_chunked.lua
···11+-- POST /echo/chunked with a Transfer-Encoding: chunked body, exercising the
22+-- incremental/streamed request read path. wrk has no built-in support for
33+-- chunked request bodies, so the raw request is hand-built here.
44+55+wrk.method = "POST"
66+77+local chunks = {
88+ string.rep("a", 1024),
99+ string.rep("b", 1024),
1010+ string.rep("c", 1024),
1111+ string.rep("d", 1024),
1212+}
1313+1414+local encoded = ""
1515+for _, chunk in ipairs(chunks) do
1616+ encoded = encoded .. string.format("%x", #chunk) .. "\r\n" .. chunk .. "\r\n"
1717+end
1818+encoded = encoded .. "0\r\n\r\n"
1919+2020+request = function()
2121+ return "POST /echo/chunked HTTP/1.1\r\n" ..
2222+ "Host: 127.0.0.1\r\n" ..
2323+ "Transfer-Encoding: chunked\r\n" ..
2424+ "Content-Type: application/octet-stream\r\n" ..
2525+ "Connection: keep-alive\r\n\r\n" ..
2626+ encoded
2727+end
+32
benchmark/wrk2-setup.sh
···11+#!/usr/bin/env bash
22+#
33+# Clones and builds wrk2 (https://github.com/giltene/wrk2) into benchmark/.wrk2/,
44+# for use by bench.sh.
55+#
66+# On newer binutils 2.4x+, wrk2's vendored LuaJIT bytecode-to-object step links
77+# fine on its own, but the final link fails with "string table is corrupt" using
88+# the default bfd linker. I don't know if it's issue on my side, but building
99+# with the gold linker avoids it.
1010+1111+set -eu
1212+1313+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1414+DEST="$ROOT/.wrk2"
1515+1616+if [ -x "$DEST/wrk2" ]; then
1717+ echo "Already built: $DEST/wrk2"
1818+ exit 0
1919+fi
2020+2121+rm -rf "$DEST/src"
2222+mkdir -p "$DEST"
2323+git clone --depth 1 https://github.com/giltene/wrk2.git "$DEST/src"
2424+2525+(
2626+ cd "$DEST/src"
2727+ make CC="gcc -fuse-ld=gold"
2828+)
2929+3030+cp "$DEST/src/wrk" "$DEST/wrk2"
3131+echo "Built: $DEST/wrk2"
3232+"$DEST/wrk2" --version || true
···11import gleam/bytes_tree
22import gleam/erlang/process
33-import glisten/internal/handler
43import glisten/socket
54import glisten/transport
65···87 Http1(
98 transport: transport.Transport,
109 socket: socket.Socket,
1111- self: process.Subject(handler.Message(Message)),
1010+ self: process.Subject(Http1Signal),
1211 buffer: BitArray,
1312 framing: Framing,
1413 // Body bytes delivered to the caller so far, via `read_body_chunk`.
···4140 FileMetadata(path: String, offset: Int, length: Int)
4241}
43424343+/// Out-of-band events for a connection's own actor loop.
4444pub type Message {
4545 Timeout
4646+}
4747+4848+/// Sent by `http1.gleam` to a request's own private subject, drained
4949+/// synchronously within the same `http1.handle_message` call
5050+pub type Http1Signal {
4651 /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves
4752 /// once the request body has been fully consumed, carrying whatever bytes
4853 /// came after it.
···5661 /// the connection can still resume draining from here instead of from the
5762 /// start of the body.
5863 BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int)
5959- /// Sent by `http1.finish_chunk`/`http1.finish_response` to themselves once
6464+ /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves once
6065 /// a streamed response's terminator has been written.
6166 StreamFinished(keep_alive: Bool)
6267}
···6671 Http1Writer(
6772 transport: transport.Transport,
6873 socket: socket.Socket,
6969- self: process.Subject(handler.Message(Message)),
7474+ self: process.Subject(Http1Signal),
7075 // `True` on HTTP/1.1. Frame each chunk with its hex size and a trailing
7176 // `0\r\n\r\n` terminator.
7277 //
-14
src/ewe/internal/handler.gleam
···55import gleam/http/request
66import gleam/http/response
77import gleam/option
88-import gleam/string
98import glisten
109import glisten/internal/handler
1110import logging
···6059 glisten.User(connection.Timeout) -> {
6160 logging.log(logging.Debug, "Connection idled for too long, closing.")
6261 glisten.stop()
6363- }
6464- // TODO: just use other subject brah
6565- glisten.User(connection.BodyDrained(..))
6666- | glisten.User(connection.BodyAbandoned)
6767- | glisten.User(connection.BodyProgress(..))
6868- | glisten.User(connection.StreamFinished(..)) -> {
6969- logging.log(
7070- logging.Critical,
7171- "Web server loop received a message that should not be reached to the handler! That means there is a bug somewhere within the implementation. Please open an issue addressing the alert, thank you! https://github.com/vshakitskiy/ewe/issues/new\n\nMessage received: "
7272- <> string.inspect(message),
7373- )
7474-7575- glisten.continue(state)
7662 }
7763 glisten.Packet(data) ->
7864 case state {
+27-29
src/ewe/internal/http1.gleam
···5050 transport.Ssl -> http.Https
5151 }
52525353+ let self = process.new_subject()
5454+5355 let body_connection =
5456 connection.Http1(
5557 transport: connection.transport,
5658 socket: connection.socket,
5757- self: connection.subject,
5959+ self:,
5860 buffer: remaining,
5961 framing: metadata.framing,
6062 read: 0,
···75777678 let response = state.handler(request)
77797878- let drained = drain_messages(connection.subject)
8080+ let drained = drain_messages(self)
79818082 let #(buffer, body_drained) =
8183 resolve_body(unsafe_to_http1_connection(body_connection), drained.body)
···105107 connection.Http1Writer(
106108 transport: connection.transport,
107109 socket: connection.socket,
108108- self: connection.subject,
110110+ self:,
109111 chunked:,
110112 keep_alive:,
111113 )
112114 |> stream_handler
113115114114- let stream_drained = drain_messages(connection.subject)
116116+ let stream_drained = drain_messages(self)
115117 let stream_keep_alive = case stream_drained.stream {
116118 option.Some(connection.StreamFinished(keep_alive:)) ->
117119 keep_alive
···135137 }
136138 False
137139 }
138138- option.Some(connection.Timeout)
139139- | option.Some(connection.BodyDrained(..))
140140+ option.Some(connection.BodyDrained(..))
140141 | option.Some(connection.BodyAbandoned)
141142 | option.Some(connection.BodyProgress(..)) ->
142143 panic as "drain_messages routes body messages to the other slot"
···216217 Http1(
217218 transport: transport.Transport,
218219 socket: socket.Socket,
219219- self: process.Subject(handler.Message(connection.Message)),
220220+ self: process.Subject(connection.Http1Signal),
220221 buffer: BitArray,
221222 framing: connection.Framing,
222223 read: Int,
···245246246247 case framing, consume_body(transport, socket, buffer, framing, limit) {
247248 _framing, Ok(#(body, trailers, leftover)) -> {
248248- process.send(self, handler.User(connection.BodyDrained(leftover:)))
249249+ process.send(self, connection.BodyDrained(leftover:))
249250 Ok(#(body, trailers))
250251 }
251252 connection.Fixed(_length), Error(BodyTooLarge) -> Error(BodyTooLarge)
252253 _framing, Error(error) -> {
253253- process.send(self, handler.User(connection.BodyAbandoned))
254254+ process.send(self, connection.BodyAbandoned)
254255 Error(error)
255256 }
256257 }
···278279279280 case pull_chunk(conn, max_chunk_bytes, limit) {
280281 Ok(PulledChunk(data, next)) -> {
281281- handler.User(connection.BodyProgress(buffer:, read:, chunk_remaining:))
282282+ connection.BodyProgress(buffer:, read:, chunk_remaining:)
282283 |> process.send(self, _)
283284284285 Ok(Chunk(data, next))
285286 }
286287 Ok(PulledDone(trailers, leftover)) -> {
287287- process.send(self, handler.User(connection.BodyDrained(leftover:)))
288288+ process.send(self, connection.BodyDrained(leftover:))
288289289290 Ok(Done(trailers))
290291 }
291292 Error(error) -> {
292292- process.send(self, handler.User(connection.BodyAbandoned))
293293+ process.send(self, connection.BodyAbandoned)
293294294295 Error(error)
295296 }
···312313// connection just because the handler didn't care about its body.
313314fn resolve_body(
314315 conn: Connection,
315315- drained: option.Option(connection.Message),
316316+ drained: option.Option(connection.Http1Signal),
316317) -> #(BitArray, Bool) {
317318 case drained {
318319 option.Some(connection.BodyDrained(leftover)) -> #(leftover, True)
319320 option.Some(connection.BodyAbandoned) -> #(<<>>, False)
320321 option.Some(connection.BodyProgress(buffer:, read:, chunk_remaining:)) ->
321322 drain_remaining(Http1(..conn, buffer:, read:, chunk_remaining:))
322322- option.Some(connection.Timeout) | option.None -> drain_remaining(conn)
323323+ option.None -> drain_remaining(conn)
323324 option.Some(connection.StreamFinished(..)) ->
324325 panic as "drain_messages routes stream messages to the other slot"
325326 }
326327}
327328328328-// The latest body read and response stream outcome messages currently queued
329329-// for a connection's own subject, drained in one pass so a handler that both
330330-// reads a request body and streams a response doesn't lose one outcome to the
331331-// other.
329329+// The latest body read and response stream outcome messages currently queued
330330+// for a request's own private subject, drained in one pass so a handler that
331331+// both reads a request body and streams a response doesn't lose one outcome
332332+// to the other.
332333type Drained {
333334 Drained(
334334- body: option.Option(connection.Message),
335335- stream: option.Option(connection.Message),
335335+ body: option.Option(connection.Http1Signal),
336336+ stream: option.Option(connection.Http1Signal),
336337 )
337338}
338339339340// Selectively receives every message already queued for `self`, keeping only
340341// the last one of each kind.
341341-fn drain_messages(
342342- self: process.Subject(handler.Message(connection.Message)),
343343-) -> Drained {
342342+fn drain_messages(self: process.Subject(connection.Http1Signal)) -> Drained {
344343 do_drain_messages(self, Drained(body: option.None, stream: option.None))
345344}
346345347346fn do_drain_messages(
348348- self: process.Subject(handler.Message(connection.Message)),
347347+ self: process.Subject(connection.Http1Signal),
349348 acc: Drained,
350349) -> Drained {
351350 case process.receive(self, 0) {
352352- Ok(handler.User(connection.StreamFinished(..) as message)) ->
351351+ Ok(connection.StreamFinished(..) as message) ->
353352 do_drain_messages(self, Drained(..acc, stream: option.Some(message)))
354354- Ok(handler.User(message)) ->
353353+ Ok(message) ->
355354 do_drain_messages(self, Drained(..acc, body: option.Some(message)))
356356- Ok(handler.Internal(_message)) -> do_drain_messages(self, acc)
357355 Error(Nil) -> acc
358356 }
359357}
···829827 ResponseWriter(
830828 transport: transport.Transport,
831829 socket: socket.Socket,
832832- self: process.Subject(handler.Message(connection.Message)),
830830+ self: process.Subject(connection.Http1Signal),
833831 // `True` on HTTP/1.1: frame each chunk with its hex size and a trailing
834832 // `0\r\n\r\n` terminator. `False` on HTTP/1.0, which has no chunked
835833 // encoding: write raw bytes and let the body end when the connection
···898896fn finish(writer: ResponseWriter) -> Nil {
899897 process.send(
900898 writer.self,
901901- handler.User(connection.StreamFinished(keep_alive: writer.keep_alive)),
899899+ connection.StreamFinished(keep_alive: writer.keep_alive),
902900 )
903901}
904902