Skip to content

Commit e6e3545

Browse files
authored
avoid chunked encoding / data copies (#267)
* use a body writer to avoid copying data during request sends * avoid chunked encoding in server - since the full response is being held in memory, there's no advantage of chunking it on the server side and we can use the simpler plain response
1 parent 6f1fff8 commit e6e3545

4 files changed

Lines changed: 121 additions & 31 deletions

File tree

json_rpc/clients/httpclient.nim

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,28 @@ method send(
5757
else:
5858
@[]
5959
headers.add(("Content-Type", "application/json"))
60+
headers.add(("Content-Length", $reqData.len))
6061

6162
let
6263
req = HttpClientRequestRef.post(
63-
client.httpSession, client.httpAddress, body = reqData, headers = headers
64+
client.httpSession,
65+
client.httpAddress,
66+
headers = headers,
67+
body = default(seq[byte]),
6468
)
6569

6670
res =
6771
try:
68-
await req.send()
72+
let wr = await req.open()
73+
try:
74+
await wr.write(unsafeAddr reqData[0], reqData.len)
75+
await wr.finish()
76+
finally:
77+
await wr.closeWait()
78+
79+
await req.finish()
80+
except AsyncStreamError as exc:
81+
raise (ref RpcPostError)(msg: exc.msg, parent: exc)
6982
except HttpError as exc:
7083
raise (ref RpcPostError)(msg: exc.msg, parent: exc)
7184
finally:
@@ -92,15 +105,28 @@ method request(
92105
else:
93106
@[]
94107
headers.add(("Content-Type", "application/json"))
108+
headers.add(("Content-Length", $reqData.len))
95109

96110
let
97111
req = HttpClientRequestRef.post(
98-
client.httpSession, client.httpAddress, body = reqData, headers = headers
112+
client.httpSession,
113+
client.httpAddress,
114+
headers = headers,
115+
body = default(seq[byte]),
99116
)
100117

101118
res =
102119
try:
103-
await req.send()
120+
let wr = await req.open()
121+
try:
122+
await wr.write(unsafeAddr reqData[0], reqData.len)
123+
await wr.finish()
124+
finally:
125+
await wr.closeWait()
126+
127+
await req.finish()
128+
except AsyncStreamError as exc:
129+
raise (ref RpcPostError)(msg: exc.msg, parent: exc)
104130
except HttpError as exc:
105131
raise (ref RpcPostError)(msg: exc.msg, parent: exc)
106132
finally:

json_rpc/router.nim

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export chronos, jsonmarshal, json, jrpc_sys
2222
logScope:
2323
topics = "jsonrpc router"
2424

25+
template errorReturnWorkaround(body) =
26+
when NimMajor < 2:
27+
body
28+
raiseAssert "never hit"
29+
else:
30+
body
31+
2532
type
2633
RpcProc* = proc(params: RequestParamsRx): Future[JsonString] {.async.}
2734
## Procedure signature accepted as an RPC call by server - if the function
@@ -211,8 +218,8 @@ macro rpc*(server: RpcRouter, formatType, procList: untyped): untyped =
211218
of nnkIdent, nnkAccQuoted:
212219
$prc[0]
213220
else:
214-
error "Unsupported rpc proc definition", prc
215-
"" # Nim 1.6
221+
errorReturnWorkaround:
222+
error "Unsupported rpc proc definition", prc
216223
result.add rpcImpl(server, path, formatType, prc)
217224
else:
218225
error "Only proc definitions are allowed within an rpc context", prc

json_rpc/servers/httpserver.nim

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type
4040
# This inheritance arrangement is useful for
4141
# e.g. combo HTTP server
4242
RpcHttpHandler* = ref object of RpcServer
43-
maxChunkSize*: int
43+
maxChunkSize* {.deprecated.}: int
4444

4545
RpcHttpServer* = ref object of RpcHttpHandler
4646
httpServers: seq[HttpServerRef]
@@ -56,26 +56,15 @@ proc serveHTTP*(rpcServer: RpcHttpHandler, request: HttpRequestRef):
5656

5757
let
5858
data = await rpcServer.route(req)
59-
chunkSize = rpcServer.maxChunkSize
60-
streamType =
61-
if data.len <= chunkSize:
62-
HttpResponseStreamType.Plain
63-
else:
64-
HttpResponseStreamType.Chunked
6559
response = request.getResponse()
6660

6761
response.addHeader("Content-Type", "application/json")
62+
response.addHeader("Content-Length", $data.len)
6863

69-
await response.prepare(streamType)
70-
let maxLen = data.len
64+
await response.prepare(HttpResponseStreamType.Plain)
7165

72-
var len = data.len
73-
while len > chunkSize:
74-
await response.send(data[maxLen - len].unsafeAddr, chunkSize)
75-
len -= chunkSize
76-
77-
if len > 0:
78-
await response.send(data[maxLen - len].unsafeAddr, len)
66+
if data.len() > 0:
67+
await response.send(unsafeAddr data[0], data.len())
7968

8069
await response.finish()
8170
response
@@ -221,10 +210,10 @@ proc addSecureHttpServer*(server: RpcHttpServer,
221210
addSecureHttpServers(server, toSeq(resolveIP(address, port)), tlsPrivateKey, tlsCertificate)
222211

223212
proc new*(T: type RpcHttpServer, authHooks: seq[HttpAuthHook] = @[]): T =
224-
T(router: RpcRouter.init(), httpServers: @[], authHooks: authHooks, maxChunkSize: 8192)
213+
T(router: RpcRouter.init(), httpServers: @[], authHooks: authHooks)
225214

226215
proc new*(T: type RpcHttpServer, router: RpcRouter, authHooks: seq[HttpAuthHook] = @[]): T =
227-
T(router: router, httpServers: @[], authHooks: authHooks, maxChunkSize: 8192)
216+
T(router: router, httpServers: @[], authHooks: authHooks)
228217

229218
proc newRpcHttpServer*(authHooks: seq[HttpAuthHook] = @[]): RpcHttpServer =
230219
RpcHttpServer.new(authHooks)
@@ -273,5 +262,5 @@ proc localAddress*(server: RpcHttpServer): seq[TransportAddress] =
273262
for item in server.httpServers:
274263
result.add item.instance.localAddress()
275264

276-
proc setMaxChunkSize*(server: RpcHttpServer, maxChunkSize: int) =
265+
proc setMaxChunkSize*(server: RpcHttpServer, maxChunkSize: int) {.deprecated: "unused".} =
277266
server.maxChunkSize = maxChunkSize

tests/testhttp.nim

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
# those terms.
99

1010
import
11-
unittest2, chronos/unittest2/asynctests,
11+
unittest2,
12+
chronos/unittest2/asynctests,
1213
../json_rpc/[rpcserver, rpcclient, jsonmarshal],
13-
./private/helpers
14+
./private/helpers,
15+
stew/byteutils
1416

1517
const TestsCount = 100
1618
const bigChunkSize = 4 * 8192
@@ -19,14 +21,14 @@ suite "JSON-RPC/http":
1921
setup:
2022
var httpsrv = newRpcHttpServer(["127.0.0.1:0"])
2123
# Create RPC on server
22-
httpsrv.rpc("myProc") do(input: string, data: array[0..3, int]):
24+
httpsrv.rpc("myProc") do(input: string, data: array[0 .. 3, int]):
2325
result = %("Hello " & input & " data: " & $data)
2426
httpsrv.rpc("noParamsProc") do():
2527
result = %("Hello world")
2628

2729
httpsrv.rpc("bigchunkMethod") do() -> seq[byte]:
2830
result = newSeq[byte](bigChunkSize)
29-
for i in 0..<result.len:
31+
for i in 0 ..< result.len:
3032
result[i] = byte(i mod 255)
3133

3234
httpsrv.setMaxChunkSize(8192)
@@ -47,10 +49,11 @@ suite "JSON-RPC/http":
4749

4850
asyncTest "Continuous RPC calls (" & $TestsCount & " messages)":
4951
var client = newRpcHttpClient()
50-
for i in 0..<TestsCount:
52+
for i in 0 ..< TestsCount:
5153
await client.connect("http://" & serverAddress)
5254
var r = await client.call("myProc", %[%"abc", %[1, 2, 3, i]])
53-
check: r.string == "\"Hello abc data: [1, 2, 3, " & $i & "]\""
55+
check:
56+
r.string == "\"Hello abc data: [1, 2, 3, " & $i & "]\""
5457
await client.close()
5558

5659
asyncTest "Invalid RPC calls":
@@ -88,3 +91,68 @@ suite "JSON-RPC/http":
8891

8992
check:
9093
notif
94+
95+
asyncTest "Chunked encoding":
96+
# The server doesn't use chunked encoding but the client might encounter
97+
# such servers - test that it works as expected
98+
let host = "127.0.0.1"
99+
var server = createStreamServer(initTAddress(host & ":0"), {ReuseAddr})
100+
defer:
101+
await server.closeWait()
102+
103+
let port = server.localAddress().port
104+
105+
proc simpleServerResponder() {.async.} =
106+
try:
107+
let conn = await server.accept()
108+
109+
# 1. Consume the incoming headers but ignore the body hoping it's small
110+
# enough
111+
var headerBuf: seq[byte] = newSeq[byte](8192)
112+
discard
113+
await conn.readUntil(addr headerBuf[0], headerBuf.len, toBytes("\r\n\r\n"))
114+
115+
# 2. Send HTTP response with Chunked Transfer Encoding
116+
var respHeader = (
117+
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" &
118+
"Transfer-Encoding: chunked\r\n\r\n"
119+
).toBytes()
120+
discard await conn.write(respHeader)
121+
122+
# JSON Response broken into two chunks
123+
let fullMsg = """{"jsonrpc":"2.0","result":{"s":"part1"},"id":1}"""
124+
let mid = fullMsg.len div 2
125+
let part1 = fullMsg[0 ..< mid]
126+
let part2 = fullMsg[mid ..^ 1]
127+
128+
# Chunk 1: size in hex + \r\n
129+
discard await conn.write(
130+
(toHex(part1.len).strip(trailing = false, chars = {'0'}) & "\r\n").toBytes()
131+
)
132+
discard await conn.write(part1.toBytes())
133+
134+
# Chunk 2: size in hex + \r\n
135+
discard await conn.write(
136+
("\r\n" & toHex(part2.len).strip(trailing = false, chars = {'0'}) & "\r\n").toBytes()
137+
)
138+
discard await conn.write(part2.toBytes())
139+
140+
# Final empty chunk to end the stream (0\r\n\r\n)
141+
discard await conn.write("\r\n0\r\n\r\n".toBytes())
142+
await conn.shutdownWait()
143+
await conn.closeWait()
144+
except CatchableError as exc:
145+
raiseAssert exc.msg
146+
147+
var st = simpleServerResponder()
148+
149+
var client = newRpcHttpClient()
150+
151+
try:
152+
await client.connect(host, port, false)
153+
154+
let res = await client.call("anyMethod", %*{"param": "value"})
155+
check res == JsonString """{"s":"part1"}"""
156+
finally:
157+
await client.close()
158+
await st.cancelAndWait()

0 commit comments

Comments
 (0)