Skip to content

Commit 079cdd2

Browse files
committed
WIP
1 parent 0bb3b0a commit 079cdd2

File tree

5 files changed

+123
-20
lines changed

5 files changed

+123
-20
lines changed

examples/streaming/bidirectional.rb

+13
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
output = Protocol::HTTP::Body::Streamable.response(request) do |stream|
2121
# Simple echo server:
2222
while chunk = stream.readpartial(1024)
23+
$stderr.puts "Server chunk: #{chunk.inspect}"
2324
stream.write(chunk)
2425
end
2526
rescue EOFError
27+
$stderr.puts "Server EOF."
2628
# Ignore EOF errors.
2729
ensure
30+
$stderr.puts "Server closing stream."
2831
stream.close
2932
end
3033

@@ -38,18 +41,28 @@
3841
streamable = Protocol::HTTP::Body::Streamable.request do |stream|
3942
stream.write("Hello, ")
4043
stream.write("World!")
44+
45+
$stderr.puts "Client closing write..."
4146
stream.close_write
4247

48+
$stderr.puts "Client reading response..."
49+
4350
while chunk = stream.readpartial(1024)
51+
$stderr.puts "Client chunk: #{chunk.inspect}"
4452
puts chunk
4553
end
54+
$stderr.puts "Client done reading response."
4655
rescue EOFError
56+
$stderr.puts "Client EOF."
4757
# Ignore EOF errors.
4858
ensure
59+
$stderr.puts "Client closing stream."
4960
stream.close
5061
end
5162

63+
$stderr.puts "Client sending request..."
5264
response = client.get("/", body: streamable)
65+
$stderr.puts "Client received response and streaming it..."
5366
streamable.stream(response.body)
5467
ensure
5568
server_task.stop

examples/streaming/gems.locked

+15
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@ GEM
2727
fiber-annotation
2828
fiber-local (~> 1.1)
2929
json
30+
debug (1.9.2)
31+
irb (~> 1.10)
32+
reline (>= 0.3.8)
3033
fiber-annotation (0.2.0)
3134
fiber-local (1.1.0)
3235
fiber-storage
3336
fiber-storage (1.0.0)
37+
io-console (0.7.2)
3438
io-endpoint (0.13.1)
3539
io-event (1.6.5)
3640
io-stream (0.4.0)
41+
irb (1.14.0)
42+
rdoc (>= 4.0.0)
43+
reline (>= 0.4.2)
3744
json (2.7.2)
3845
metrics (0.10.2)
3946
protocol-hpack (1.5.0)
@@ -42,6 +49,13 @@ GEM
4249
protocol-http2 (0.18.0)
4350
protocol-hpack (~> 1.4)
4451
protocol-http (~> 0.18)
52+
psych (5.1.2)
53+
stringio
54+
rdoc (6.7.0)
55+
psych (>= 4.0.0)
56+
reline (0.5.10)
57+
io-console (~> 0.5)
58+
stringio (3.1.1)
4559
traces (0.13.1)
4660

4761
PLATFORMS
@@ -51,6 +65,7 @@ PLATFORMS
5165
DEPENDENCIES
5266
async
5367
async-http
68+
debug
5469
protocol-http!
5570

5671
BUNDLED WITH

examples/streaming/gems.rb

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88
gem "async"
99
gem "async-http"
1010
gem "protocol-http", path: "../../"
11+
12+
gem "debug"

gems.rb

+2
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@
2525
gem "bake-test"
2626
gem "bake-test-external"
2727
end
28+
29+
gem "debug", ">= 1.0.0"

lib/protocol/http/body/streamable.rb

+91-20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ def self.new(*arguments)
3333
end
3434
end
3535

36+
def self.request(&block)
37+
DeferredBody.new(block)
38+
end
39+
40+
def self.response(request, &block)
41+
Body.new(block, request.body)
42+
end
43+
3644
# Represents an output wrapper around a stream, that can invoke a fiber when `#read`` is called.
3745
#
3846
# This behaves a little bit like a generator or lazy enumerator, in that it can be used to generate chunks of data on demand.
@@ -43,14 +51,12 @@ def initialize(input, block)
4351
stream = Stream.new(input, self)
4452

4553
@from = nil
46-
47-
@fiber = Fiber.new do |from|
48-
@from = from
54+
@to = Fiber.new do
4955
block.call(stream)
5056
rescue => error
5157
# Ignore.
5258
ensure
53-
@fiber = nil
59+
@to = nil
5460
self.close(error)
5561
end
5662
end
@@ -59,39 +65,45 @@ def initialize(input, block)
5965
def write(chunk)
6066
if from = @from
6167
@from = nil
62-
@from = from.transfer(chunk)
68+
@to = Fiber.current
69+
70+
from.transfer(chunk)
6371
else
6472
raise ClosedError, "Stream is not being read!"
6573
end
6674
end
6775

6876
# Indicates that no further output will be generated.
6977
def close_write(error = nil)
70-
# We might want to specialize the implementation later...
7178
close(error)
7279
end
7380

7481
# Can be invoked by the block to close the stream. Closing the output means that no more chunks will be generated.
7582
def close(error = nil)
83+
$stderr.puts "Closing from: #{@from}, to: #{@to}, error: #{error}"
7684
if from = @from
7785
# We are closing from within the output fiber, so we need to transfer back to `@from`:
7886
@from = nil
87+
@to = Fiber.current
88+
7989
if error
8090
from.raise(error)
8191
else
92+
$stderr.puts "Transferring to #{from}...", from.backtrace
8293
from.transfer(nil)
8394
end
84-
elsif @fiber
95+
elsif to = @to
8596
# We are closing from outside the output fiber, so we need to resume the fiber appropriately:
8697
@from = Fiber.current
98+
@to = nil
8799

88100
if error
89101
# The fiber will be resumed from where it last called write, and we will raise the error there:
90-
@fiber.raise(error)
102+
to.raise(error)
91103
else
92104
begin
93105
# If we get here, it means we are closing the fiber from the outside, so we need to transfer control back to the fiber:
94-
@fiber.transfer(nil)
106+
to.transfer(nil)
95107
rescue Protocol::HTTP::Body::Streamable::ClosedError
96108
# If the fiber then tries to write to the stream, it will raise a ClosedError, and we will end up here. We can ignore it, as we are already closing the stream and don't care about further writes.
97109
end
@@ -102,7 +114,14 @@ def close(error = nil)
102114
def read
103115
raise RuntimeError, "Stream is already being read!" if @from
104116

105-
@fiber&.transfer(Fiber.current)
117+
if to = @to
118+
@from = Fiber.current
119+
@to = nil
120+
121+
to.transfer
122+
else
123+
return nil
124+
end
106125
end
107126
end
108127

@@ -155,30 +174,82 @@ def call(stream)
155174

156175
# Closing a stream indicates we are no longer interested in reading from it.
157176
def close(error = nil)
177+
$stderr.puts "Closing output #{@output}..."
178+
if output = @output
179+
@output = nil
180+
# Closing the output here may take some time, as it may need to finish handling the stream:
181+
output.close(error)
182+
end
183+
184+
$stderr.puts "Closing input #{@input}..."
158185
if input = @input
159186
@input = nil
160187
input.close(error)
161188
end
162-
163-
if output = @output
164-
@output = nil
165-
output.close(error)
189+
end
190+
end
191+
192+
class Input
193+
def initialize(from = Fiber.current)
194+
@from = from
195+
@to = nil
196+
end
197+
198+
def read
199+
if from = @from
200+
@from = nil
201+
@to = Fiber.current
202+
203+
return from.transfer
204+
else
205+
raise ClosedError, "Stream is not being written!"
206+
end
207+
end
208+
209+
def write(chunk)
210+
if to = @to
211+
@from = Fiber.current
212+
@to = nil
213+
214+
to.transfer(chunk)
215+
else
216+
raise ClosedError, "Stream is not being read!"
217+
end
218+
end
219+
220+
def close_write(error = nil)
221+
if to = @to
222+
@from = Fiber.current
223+
@to = nil
224+
225+
if error
226+
to.raise(error)
227+
else
228+
to.transfer(nil)
229+
end
230+
end
231+
end
232+
233+
def close(error = nil)
234+
close_write(error)
235+
end
236+
237+
def stream(body)
238+
body&.each do |chunk|
239+
self.write(chunk)
166240
end
167241
end
168242
end
169243

170244
# A deferred body has an extra `stream` method which can be used to stream data into the body, as the response body won't be available until the request has been sent.
171245
class DeferredBody < Body
172246
def initialize(block)
173-
super(block, Writable.new)
247+
super(block, Input.new)
174248
end
175249

176250
# Stream the response body into the block's input.
177-
def stream(input)
178-
input&.each do |chunk|
179-
@input&.write(chunk)
180-
end
181-
@input&.close_write
251+
def stream(body)
252+
@input.stream(body)
182253
end
183254
end
184255
end

0 commit comments

Comments
 (0)