Skip to content

Commit 0bb3b0a

Browse files
authored
Improved streamable body implementation. (#67)
1 parent e9a5ffc commit 0bb3b0a

23 files changed

+761
-116
lines changed

examples/streaming/bidirectional.rb

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Released under the MIT License.
5+
# Copyright, 2024, by Samuel Williams.
6+
7+
require 'async'
8+
require 'async/http/client'
9+
require 'async/http/server'
10+
require 'async/http/endpoint'
11+
12+
require 'protocol/http/body/streamable'
13+
require 'protocol/http/body/writable'
14+
require 'protocol/http/body/stream'
15+
16+
endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
17+
18+
Async do
19+
server = Async::HTTP::Server.for(endpoint) do |request|
20+
output = Protocol::HTTP::Body::Streamable.response(request) do |stream|
21+
# Simple echo server:
22+
while chunk = stream.readpartial(1024)
23+
stream.write(chunk)
24+
end
25+
rescue EOFError
26+
# Ignore EOF errors.
27+
ensure
28+
stream.close
29+
end
30+
31+
Protocol::HTTP::Response[200, {}, output]
32+
end
33+
34+
server_task = Async{server.run}
35+
36+
client = Async::HTTP::Client.new(endpoint)
37+
38+
streamable = Protocol::HTTP::Body::Streamable.request do |stream|
39+
stream.write("Hello, ")
40+
stream.write("World!")
41+
stream.close_write
42+
43+
while chunk = stream.readpartial(1024)
44+
puts chunk
45+
end
46+
rescue EOFError
47+
# Ignore EOF errors.
48+
ensure
49+
stream.close
50+
end
51+
52+
response = client.get("/", body: streamable)
53+
streamable.stream(response.body)
54+
ensure
55+
server_task.stop
56+
end

examples/streaming/gems.locked

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
PATH
2+
remote: ../..
3+
specs:
4+
protocol-http (0.33.0)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
async (2.17.0)
10+
console (~> 1.26)
11+
fiber-annotation
12+
io-event (~> 1.6, >= 1.6.5)
13+
async-http (0.75.0)
14+
async (>= 2.10.2)
15+
async-pool (~> 0.7)
16+
io-endpoint (~> 0.11)
17+
io-stream (~> 0.4)
18+
protocol-http (~> 0.30)
19+
protocol-http1 (~> 0.20)
20+
protocol-http2 (~> 0.18)
21+
traces (>= 0.10)
22+
async-pool (0.8.1)
23+
async (>= 1.25)
24+
metrics
25+
traces
26+
console (1.27.0)
27+
fiber-annotation
28+
fiber-local (~> 1.1)
29+
json
30+
fiber-annotation (0.2.0)
31+
fiber-local (1.1.0)
32+
fiber-storage
33+
fiber-storage (1.0.0)
34+
io-endpoint (0.13.1)
35+
io-event (1.6.5)
36+
io-stream (0.4.0)
37+
json (2.7.2)
38+
metrics (0.10.2)
39+
protocol-hpack (1.5.0)
40+
protocol-http1 (0.22.0)
41+
protocol-http (~> 0.22)
42+
protocol-http2 (0.18.0)
43+
protocol-hpack (~> 1.4)
44+
protocol-http (~> 0.18)
45+
traces (0.13.1)
46+
47+
PLATFORMS
48+
ruby
49+
x86_64-linux
50+
51+
DEPENDENCIES
52+
async
53+
async-http
54+
protocol-http!
55+
56+
BUNDLED WITH
57+
2.5.16

examples/streaming/gems.rb

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
source "https://rubygems.org"
7+
8+
gem "async"
9+
gem "async-http"
10+
gem "protocol-http", path: "../../"

examples/streaming/unidirectional.rb

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Released under the MIT License.
5+
# Copyright, 2024, by Samuel Williams.
6+
7+
require 'async'
8+
require 'async/http/client'
9+
require 'async/http/server'
10+
require 'async/http/endpoint'
11+
12+
require 'protocol/http/body/stream'
13+
require 'protocol/http/body/writable'
14+
15+
endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
16+
17+
Async do
18+
server = Async::HTTP::Server.for(endpoint) do |request|
19+
output = Protocol::HTTP::Body::Writable.new
20+
stream = Protocol::HTTP::Body::Stream.new(request.body, output)
21+
22+
Async do
23+
# Simple echo server:
24+
while chunk = stream.readpartial(1024)
25+
stream.write(chunk)
26+
end
27+
rescue EOFError
28+
# Ignore EOF errors.
29+
ensure
30+
stream.close
31+
end
32+
33+
Protocol::HTTP::Response[200, {}, output]
34+
end
35+
36+
server_task = Async{server.run}
37+
38+
client = Async::HTTP::Client.new(endpoint)
39+
40+
input = Protocol::HTTP::Body::Writable.new
41+
response = client.get("/", body: input)
42+
43+
begin
44+
stream = Protocol::HTTP::Body::Stream.new(response.body, input)
45+
46+
stream.write("Hello, ")
47+
stream.write("World!")
48+
stream.close_write
49+
50+
while chunk = stream.readpartial(1024)
51+
puts chunk
52+
end
53+
rescue EOFError
54+
# Ignore EOF errors.
55+
ensure
56+
stream.close
57+
end
58+
ensure
59+
server_task.stop
60+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
module Protocol
7+
module HTTP
8+
module Body
9+
AReadableBody = Sus::Shared("a readable body") do
10+
with "#read" do
11+
it "after closing, returns nil" do
12+
body.close
13+
14+
expect(body.read).to be_nil
15+
end
16+
end
17+
18+
with "empty?" do
19+
it "returns true after closing" do
20+
body.close
21+
22+
expect(body).to be(:empty?)
23+
end
24+
end
25+
end
26+
end
27+
end
28+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
module Protocol
7+
module HTTP
8+
module Body
9+
AWritableBody = Sus::Shared("a readable body") do
10+
with "#read" do
11+
it "after closing the write end, returns all chunks" do
12+
body.write("Hello ")
13+
body.write("World!")
14+
body.close_write
15+
16+
expect(body.read).to be == "Hello "
17+
expect(body.read).to be == "World!"
18+
expect(body.read).to be_nil
19+
end
20+
end
21+
22+
with "empty?" do
23+
it "returns false before writing" do
24+
expect(body).not.to be(:empty?)
25+
end
26+
27+
it "returns true after all chunks are consumed" do
28+
body.write("Hello")
29+
body.close_write
30+
31+
expect(body).not.to be(:empty?)
32+
expect(body.read).to be == "Hello"
33+
expect(body.read).to be_nil
34+
35+
expect(body).to be(:empty?)
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end

guides/links.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
getting-started:
22
order: 1
33
design-overview:
4-
order: 2
4+
order: 10

guides/streaming/readme.md

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Streaming
2+
3+
This guide gives an overview of how to implement streaming requests and responses.
4+
5+
## Independent Uni-directional Streaming
6+
7+
The request and response body work independently of each other can stream data in both directions. {ruby Protocol::HTTP::Body::Stream} provides an interface to merge these independent streams into an IO-like interface.
8+
9+
```ruby
10+
#!/usr/bin/env ruby
11+
12+
require 'async'
13+
require 'async/http/client'
14+
require 'async/http/server'
15+
require 'async/http/endpoint'
16+
17+
require 'protocol/http/body/stream'
18+
require 'protocol/http/body/writable'
19+
20+
endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
21+
22+
Async do
23+
server = Async::HTTP::Server.for(endpoint) do |request|
24+
output = Protocol::HTTP::Body::Writable.new
25+
stream = Protocol::HTTP::Body::Stream.new(request.body, output)
26+
27+
Async do
28+
# Simple echo server:
29+
while chunk = stream.readpartial(1024)
30+
stream.write(chunk)
31+
end
32+
rescue EOFError
33+
# Ignore EOF errors.
34+
ensure
35+
stream.close
36+
end
37+
38+
Protocol::HTTP::Response[200, {}, output]
39+
end
40+
41+
server_task = Async{server.run}
42+
43+
client = Async::HTTP::Client.new(endpoint)
44+
45+
input = Protocol::HTTP::Body::Writable.new
46+
response = client.get("/", body: input)
47+
48+
begin
49+
stream = Protocol::HTTP::Body::Stream.new(response.body, input)
50+
51+
stream.write("Hello, ")
52+
stream.write("World!")
53+
stream.close_write
54+
55+
while chunk = stream.readpartial(1024)
56+
puts chunk
57+
end
58+
rescue EOFError
59+
# Ignore EOF errors.
60+
ensure
61+
stream.close
62+
end
63+
ensure
64+
server_task.stop
65+
end
66+
```
67+
68+
This approach works quite well, especially when the input and output bodies are independently compressed, decompressed, or chunked. However, some protocols, notably, WebSockets operate on the raw connection and don't require this level of abstraction.
69+
70+
## Bi-directional Streaming
71+
72+
While WebSockets can work on the above streaming interface, it's a bit more convenient to use the streaming interface directly, which gives raw access to the underlying stream where possible.
73+
74+
```ruby
75+
#!/usr/bin/env ruby
76+
77+
require 'async'
78+
require 'async/http/client'
79+
require 'async/http/server'
80+
require 'async/http/endpoint'
81+
82+
require 'protocol/http/body/stream'
83+
require 'protocol/http/body/writable'
84+
85+
endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
86+
87+
Async do
88+
server = Async::HTTP::Server.for(endpoint) do |request|
89+
streamable = Protocol::HTTP::Body::Streamable.
90+
output = Protocol::HTTP::Body::Writable.new
91+
stream = Protocol::HTTP::Body::Stream.new(request.body, output)
92+
93+
Async do
94+
# Simple echo server:
95+
while chunk = stream.readpartial(1024)
96+
stream.write(chunk)
97+
end
98+
rescue EOFError
99+
# Ignore EOF errors.
100+
ensure
101+
stream.close
102+
end
103+
104+
Protocol::HTTP::Response[200, {}, output]
105+
end
106+
107+
server_task = Async{server.run}
108+
109+
client = Async::HTTP::Client.new(endpoint)
110+
111+
input = Protocol::HTTP::Body::Writable.new
112+
response = client.get("/", body: input)
113+
114+
begin
115+
stream = Protocol::HTTP::Body::Stream.new(response.body, input)
116+
117+
stream.write("Hello, ")
118+
stream.write("World!")
119+
stream.close_write
120+
121+
while chunk = stream.readpartial(1024)
122+
puts chunk
123+
end
124+
rescue EOFError
125+
# Ignore EOF errors.
126+
ensure
127+
stream.close
128+
end
129+
ensure
130+
server_task.stop
131+
end

0 commit comments

Comments
 (0)