async-http 0.74.0 → 0.76.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/http/body/hijack.rb +1 -1
- data/lib/async/http/body/pipe.rb +7 -3
- data/lib/async/http/body/writable.rb +2 -93
- data/lib/async/http/protocol/http1/client.rb +4 -10
- data/lib/async/http/protocol/http1/server.rb +36 -15
- data/lib/async/http/protocol/http2/connection.rb +4 -2
- data/lib/async/http/protocol/http2/input.rb +2 -2
- data/lib/async/http/protocol/http2/output.rb +28 -13
- data/lib/async/http/protocol/http2/request.rb +1 -1
- data/lib/async/http/protocol/http2/response.rb +7 -2
- data/lib/async/http/protocol/http2/stream.rb +3 -3
- data/lib/async/http/protocol/request.rb +1 -1
- data/lib/async/http/version.rb +1 -1
- data/readme.md +4 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +4 -6
- metadata.gz.sig +0 -0
- data/lib/async/http/body/delayed.rb +0 -32
- data/lib/async/http/body/slowloris.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd45f82b4d28a3e9a72bbdd7eaec1877b69cfce59a3d67b49a09b99b59b42fa4
|
4
|
+
data.tar.gz: 05ceb7e93478b63e53bc9a16b829cc2c34efdc22953143192a5a60b310aa04b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b293794ba1fb14494a7187a0c1255674ea771df8cc587c6fa031abca25b8bf803f32ba4889d772d53fb8f4dce0705938f1013ab72c22353b4e09c7cc54f4467
|
7
|
+
data.tar.gz: b0c407ee2c817bd8436bff520ae09744f44e35ba79bd992e76a783be52911fdedb0b397ab12c3cd1ffb00b3042bfc5d429f11b5252e16edbc2bab178172d747d
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/async/http/body/pipe.rb
CHANGED
@@ -17,7 +17,7 @@ module Async
|
|
17
17
|
|
18
18
|
head, tail = ::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
19
19
|
|
20
|
-
@head = ::IO::Stream
|
20
|
+
@head = ::IO::Stream(head)
|
21
21
|
@tail = tail
|
22
22
|
|
23
23
|
@reader = nil
|
@@ -52,8 +52,10 @@ module Async
|
|
52
52
|
end
|
53
53
|
|
54
54
|
@head.close_write
|
55
|
+
rescue => error
|
56
|
+
raise
|
55
57
|
ensure
|
56
|
-
@input.close(
|
58
|
+
@input.close(error)
|
57
59
|
|
58
60
|
close_head if @writer&.finished?
|
59
61
|
end
|
@@ -68,8 +70,10 @@ module Async
|
|
68
70
|
while chunk = @head.read_partial
|
69
71
|
@output.write(chunk)
|
70
72
|
end
|
73
|
+
rescue => error
|
74
|
+
raise
|
71
75
|
ensure
|
72
|
-
@output.
|
76
|
+
@output.close_write(error)
|
73
77
|
|
74
78
|
close_head if @reader&.finished?
|
75
79
|
end
|
@@ -3,104 +3,13 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2018-2023, by Samuel Williams.
|
5
5
|
|
6
|
-
require 'protocol/http/body/
|
6
|
+
require 'protocol/http/body/writable'
|
7
7
|
require 'async/queue'
|
8
8
|
|
9
9
|
module Async
|
10
10
|
module HTTP
|
11
11
|
module Body
|
12
|
-
|
13
|
-
|
14
|
-
# A dynamic body which you can write to and read from.
|
15
|
-
class Writable < Readable
|
16
|
-
class Closed < StandardError
|
17
|
-
end
|
18
|
-
|
19
|
-
# @param [Integer] length The length of the response body if known.
|
20
|
-
# @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
|
21
|
-
def initialize(length = nil, queue: Async::Queue.new)
|
22
|
-
@queue = queue
|
23
|
-
|
24
|
-
@length = length
|
25
|
-
|
26
|
-
@count = 0
|
27
|
-
|
28
|
-
@finished = false
|
29
|
-
|
30
|
-
@closed = false
|
31
|
-
@error = nil
|
32
|
-
end
|
33
|
-
|
34
|
-
def length
|
35
|
-
@length
|
36
|
-
end
|
37
|
-
|
38
|
-
# Stop generating output; cause the next call to write to fail with the given error.
|
39
|
-
def close(error = nil)
|
40
|
-
unless @closed
|
41
|
-
@queue.enqueue(nil)
|
42
|
-
|
43
|
-
@closed = true
|
44
|
-
@error = error
|
45
|
-
end
|
46
|
-
|
47
|
-
super
|
48
|
-
end
|
49
|
-
|
50
|
-
def closed?
|
51
|
-
@closed
|
52
|
-
end
|
53
|
-
|
54
|
-
def ready?
|
55
|
-
!@queue.empty?
|
56
|
-
end
|
57
|
-
|
58
|
-
# Has the producer called #finish and has the reader consumed the nil token?
|
59
|
-
def empty?
|
60
|
-
@finished
|
61
|
-
end
|
62
|
-
|
63
|
-
# Read the next available chunk.
|
64
|
-
def read
|
65
|
-
return if @finished
|
66
|
-
|
67
|
-
unless chunk = @queue.dequeue
|
68
|
-
@finished = true
|
69
|
-
end
|
70
|
-
|
71
|
-
return chunk
|
72
|
-
end
|
73
|
-
|
74
|
-
# Write a single chunk to the body. Signal completion by calling `#finish`.
|
75
|
-
def write(chunk)
|
76
|
-
# If the reader breaks, the writer will break.
|
77
|
-
# The inverse of this is less obvious (*)
|
78
|
-
if @closed
|
79
|
-
raise(@error || Closed)
|
80
|
-
end
|
81
|
-
|
82
|
-
@count += 1
|
83
|
-
@queue.enqueue(chunk)
|
84
|
-
end
|
85
|
-
|
86
|
-
alias << write
|
87
|
-
|
88
|
-
def inspect
|
89
|
-
"\#<#{self.class} #{@count} chunks written, #{status}>"
|
90
|
-
end
|
91
|
-
|
92
|
-
private
|
93
|
-
|
94
|
-
def status
|
95
|
-
if @finished
|
96
|
-
'finished'
|
97
|
-
elsif @closed
|
98
|
-
'closing'
|
99
|
-
else
|
100
|
-
'waiting'
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
12
|
+
Writable = ::Protocol::HTTP::Body::Writable
|
104
13
|
end
|
105
14
|
end
|
106
15
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative 'connection'
|
7
7
|
|
@@ -33,22 +33,16 @@ module Async
|
|
33
33
|
|
34
34
|
if protocol = request.protocol
|
35
35
|
# This is a very tricky apect of handling HTTP/1 upgrade connections. In theory, this approach is a bit inefficient, because we spin up a task just to handle writing to the underlying stream when we could be writing to the stream directly. But we need to maintain some level of compatibility with HTTP/2. Additionally, we don't know if the upgrade request will be accepted, so starting to write the body at this point needs to be handled with care.
|
36
|
-
task.async do
|
37
|
-
subtask.annotate("Upgrading request.")
|
38
|
-
|
36
|
+
task.async(annotation: "Upgrading request...") do
|
39
37
|
# If this fails, this connection will be closed.
|
40
38
|
write_upgrade_body(protocol, body)
|
41
39
|
end
|
42
40
|
elsif request.connect?
|
43
|
-
task.async do
|
44
|
-
subtask.annotate("Tunnelling body.")
|
45
|
-
|
41
|
+
task.async(annotation: "Tunnneling request...") do
|
46
42
|
write_tunnel_body(@version, body)
|
47
43
|
end
|
48
44
|
else
|
49
|
-
task.async do
|
50
|
-
subtask.annotate("Streaming body.")
|
51
|
-
|
45
|
+
task.async(annotation: "Streaming request...") do
|
52
46
|
# Once we start writing the body, we can't recover if the request fails. That's because the body might be generated dynamically, streaming, etc.
|
53
47
|
write_body(@version, body, false, trailer)
|
54
48
|
end
|
@@ -59,33 +59,54 @@ module Async
|
|
59
59
|
if response
|
60
60
|
trailer = response.headers.trailer!
|
61
61
|
|
62
|
-
write_response(@version, response.status, response.headers)
|
63
|
-
|
64
62
|
# Some operations in this method are long running, that is, it's expected that `body.call(stream)` could literally run indefinitely. In order to facilitate garbage collection, we want to nullify as many local variables before calling the streaming body. This ensures that the garbage collection can clean up as much state as possible during the long running operation, so we don't retain objects that are no longer needed.
|
65
|
-
|
63
|
+
|
66
64
|
if body and protocol = response.protocol
|
65
|
+
# We force a 101 response if the protocol is upgraded - HTTP/2 CONNECT will return 200 for success, but this won't be understood by HTTP/1 clients:
|
66
|
+
write_response(@version, 101, response.headers)
|
67
|
+
|
67
68
|
stream = write_upgrade_body(protocol)
|
68
69
|
|
69
70
|
# At this point, the request body is hijacked, so we don't want to call #finish below.
|
70
|
-
request =
|
71
|
+
request = nil
|
72
|
+
response = nil
|
73
|
+
|
74
|
+
# We must return here as no further request processing can be done:
|
75
|
+
return body.call(stream)
|
76
|
+
elsif response.status == 101
|
77
|
+
# This code path is to support legacy behavior where the response status is set to 101, but the protocol is not upgraded. This may not be a valid use case, but it is supported for compatibility. We expect the response headers to contain the `upgrade` header.
|
78
|
+
write_response(@version, response.status, response.headers)
|
71
79
|
|
72
|
-
body.call(stream)
|
73
|
-
elsif request.connect? and response.success?
|
74
80
|
stream = write_tunnel_body(request.version)
|
75
81
|
|
76
82
|
# Same as above:
|
77
|
-
request =
|
83
|
+
request = nil
|
84
|
+
response = nil
|
78
85
|
|
79
|
-
|
86
|
+
# We must return here as no further request processing can be done:
|
87
|
+
return body&.call(stream)
|
80
88
|
else
|
81
|
-
|
82
|
-
version = request.version
|
83
|
-
|
84
|
-
# Same as above:
|
85
|
-
request = nil unless request.body
|
86
|
-
response = nil
|
89
|
+
write_response(@version, response.status, response.headers)
|
87
90
|
|
88
|
-
|
91
|
+
if request.connect? and response.success?
|
92
|
+
stream = write_tunnel_body(request.version)
|
93
|
+
|
94
|
+
# Same as above:
|
95
|
+
request = nil
|
96
|
+
response = nil
|
97
|
+
|
98
|
+
# We must return here as no further request processing can be done:
|
99
|
+
return body.call(stream)
|
100
|
+
else
|
101
|
+
head = request.head?
|
102
|
+
version = request.version
|
103
|
+
|
104
|
+
# Same as above:
|
105
|
+
request = nil unless request.body
|
106
|
+
response = nil
|
107
|
+
|
108
|
+
write_body(version, body, head, trailer)
|
109
|
+
end
|
89
110
|
end
|
90
111
|
|
91
112
|
# We are done with the body, you shouldn't need to call close on it:
|
@@ -66,14 +66,14 @@ module Async
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def close(error = nil)
|
69
|
-
super
|
70
|
-
|
71
69
|
# Ensure the reader task is stopped.
|
72
70
|
if @reader
|
73
71
|
reader = @reader
|
74
72
|
@reader = nil
|
75
73
|
reader.stop
|
76
74
|
end
|
75
|
+
|
76
|
+
super
|
77
77
|
end
|
78
78
|
|
79
79
|
def read_in_background(parent: Task.current)
|
@@ -101,6 +101,8 @@ module Async
|
|
101
101
|
ensure
|
102
102
|
# Don't call #close twice.
|
103
103
|
if @reader
|
104
|
+
@reader = nil
|
105
|
+
|
104
106
|
self.close(error)
|
105
107
|
end
|
106
108
|
end
|
@@ -3,14 +3,14 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2020-2023, by Samuel Williams.
|
5
5
|
|
6
|
-
|
6
|
+
require 'protocol/http/body/writable'
|
7
7
|
|
8
8
|
module Async
|
9
9
|
module HTTP
|
10
10
|
module Protocol
|
11
11
|
module HTTP2
|
12
12
|
# A writable body which requests window updates when data is read from it.
|
13
|
-
class Input < Body::Writable
|
13
|
+
class Input < ::Protocol::HTTP::Body::Writable
|
14
14
|
def initialize(stream, length)
|
15
15
|
super(length)
|
16
16
|
|
@@ -50,18 +50,25 @@ module Async
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
if @stream
|
56
|
-
@stream.finish_output(error)
|
53
|
+
def close_write(error = nil)
|
54
|
+
if stream = @stream
|
57
55
|
@stream = nil
|
56
|
+
stream.finish_output(error)
|
58
57
|
end
|
59
58
|
end
|
60
59
|
|
60
|
+
# This method should only be called from within the context of the output task.
|
61
|
+
def close(error = nil)
|
62
|
+
close_write(error)
|
63
|
+
stop(error)
|
64
|
+
end
|
65
|
+
|
61
66
|
# This method should only be called from within the context of the HTTP/2 stream.
|
62
67
|
def stop(error)
|
63
|
-
@task
|
64
|
-
|
68
|
+
if task = @task
|
69
|
+
@task = nil
|
70
|
+
task.stop(error)
|
71
|
+
end
|
65
72
|
end
|
66
73
|
|
67
74
|
private
|
@@ -70,10 +77,12 @@ module Async
|
|
70
77
|
task.annotate("Streaming #{@body} to #{@stream}.")
|
71
78
|
|
72
79
|
input = @stream.wait_for_input
|
80
|
+
stream = ::Protocol::HTTP::Body::Stream.new(input, self)
|
73
81
|
|
74
|
-
@body.call(
|
75
|
-
rescue
|
76
|
-
|
82
|
+
@body.call(stream)
|
83
|
+
rescue => error
|
84
|
+
self.close(error)
|
85
|
+
raise
|
77
86
|
end
|
78
87
|
|
79
88
|
# Reads chunks from the given body and writes them to the stream as fast as possible.
|
@@ -86,11 +95,17 @@ module Async
|
|
86
95
|
# chunk.clear unless chunk.frozen?
|
87
96
|
# GC.start
|
88
97
|
end
|
89
|
-
|
90
|
-
|
98
|
+
rescue => error
|
99
|
+
raise
|
91
100
|
ensure
|
92
|
-
|
93
|
-
|
101
|
+
# Ensure the body we are reading from is fully closed:
|
102
|
+
if body = @body
|
103
|
+
@body = nil
|
104
|
+
body.close(error)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Ensure the output of this body is closed:
|
108
|
+
self.close_write(error)
|
94
109
|
end
|
95
110
|
|
96
111
|
# Send `maximum_size` bytes of data using the specified `stream`. If the buffer has no more chunks, `END_STREAM` will be sent on the final chunk.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative '../response'
|
7
7
|
require_relative 'stream'
|
@@ -54,6 +54,11 @@ module Async
|
|
54
54
|
@response.status = status
|
55
55
|
@headers = ::Protocol::HTTP::Headers.new
|
56
56
|
|
57
|
+
# If the protocol request was successful, ensure the response protocol matches:
|
58
|
+
if status == 200 and protocol = @response.request.protocol
|
59
|
+
@response.protocol = Array(protocol).first
|
60
|
+
end
|
61
|
+
|
57
62
|
headers.each do |key, value|
|
58
63
|
# It's guaranteed that this should be the first header:
|
59
64
|
if key == CONTENT_LENGTH
|
@@ -118,7 +123,7 @@ module Async
|
|
118
123
|
|
119
124
|
@exception = error
|
120
125
|
|
121
|
-
notify!
|
126
|
+
self.notify!
|
122
127
|
end
|
123
128
|
end
|
124
129
|
|
@@ -59,7 +59,7 @@ module Async
|
|
59
59
|
|
60
60
|
# TODO this might need to be in an ensure block:
|
61
61
|
if @input and frame.end_stream?
|
62
|
-
@input.
|
62
|
+
@input.close_write
|
63
63
|
@input = nil
|
64
64
|
end
|
65
65
|
rescue ::Protocol::HTTP2::HeaderError => error
|
@@ -98,7 +98,7 @@ module Async
|
|
98
98
|
end
|
99
99
|
|
100
100
|
if frame.end_stream?
|
101
|
-
@input.
|
101
|
+
@input.close_write
|
102
102
|
@input = nil
|
103
103
|
end
|
104
104
|
end
|
@@ -149,7 +149,7 @@ module Async
|
|
149
149
|
super
|
150
150
|
|
151
151
|
if @input
|
152
|
-
@input.
|
152
|
+
@input.close_write(error)
|
153
153
|
@input = nil
|
154
154
|
end
|
155
155
|
|
data/lib/async/http/version.rb
CHANGED
data/readme.md
CHANGED
@@ -16,6 +16,10 @@ Please see the [project documentation](https://socketry.github.io/async-http/) f
|
|
16
16
|
|
17
17
|
Please see the [project releases](https://socketry.github.io/async-http/releases/index) for all releases.
|
18
18
|
|
19
|
+
### v0.75.0
|
20
|
+
|
21
|
+
- Better handling of HTTP/1 \<-\> HTTP/2 proxying, specifically upgrade/CONNECT requests.
|
22
|
+
|
19
23
|
### v0.74.0
|
20
24
|
|
21
25
|
- [`Async::HTTP::Internet` accepts keyword arguments](https://socketry.github.io/async-http/releases/index#async::http::internet-accepts-keyword-arguments)
|
data/releases.md
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.76.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -58,7 +58,7 @@ cert_chain:
|
|
58
58
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
59
59
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
60
60
|
-----END CERTIFICATE-----
|
61
|
-
date: 2024-
|
61
|
+
date: 2024-09-10 00:00:00.000000000 Z
|
62
62
|
dependencies:
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
64
|
name: async
|
@@ -122,14 +122,14 @@ dependencies:
|
|
122
122
|
requirements:
|
123
123
|
- - "~>"
|
124
124
|
- !ruby/object:Gem::Version
|
125
|
-
version: '0.
|
125
|
+
version: '0.34'
|
126
126
|
type: :runtime
|
127
127
|
prerelease: false
|
128
128
|
version_requirements: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
130
|
- - "~>"
|
131
131
|
- !ruby/object:Gem::Version
|
132
|
-
version: '0.
|
132
|
+
version: '0.34'
|
133
133
|
- !ruby/object:Gem::Dependency
|
134
134
|
name: protocol-http1
|
135
135
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,10 +182,8 @@ files:
|
|
182
182
|
- bake/async/http/h2spec.rb
|
183
183
|
- lib/async/http.rb
|
184
184
|
- lib/async/http/body.rb
|
185
|
-
- lib/async/http/body/delayed.rb
|
186
185
|
- lib/async/http/body/hijack.rb
|
187
186
|
- lib/async/http/body/pipe.rb
|
188
|
-
- lib/async/http/body/slowloris.rb
|
189
187
|
- lib/async/http/body/writable.rb
|
190
188
|
- lib/async/http/client.rb
|
191
189
|
- lib/async/http/endpoint.rb
|
metadata.gz.sig
CHANGED
Binary file
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-2023, by Samuel Williams.
|
5
|
-
# Copyright, 2020, by Bruno Sutic.
|
6
|
-
# Copyright, 2023, by Thomas Morgan.
|
7
|
-
|
8
|
-
require 'protocol/http/body/wrapper'
|
9
|
-
|
10
|
-
module Async
|
11
|
-
module HTTP
|
12
|
-
module Body
|
13
|
-
class Delayed < ::Protocol::HTTP::Body::Wrapper
|
14
|
-
def initialize(body, delay = 0.01)
|
15
|
-
super(body)
|
16
|
-
|
17
|
-
@delay = delay
|
18
|
-
end
|
19
|
-
|
20
|
-
def ready?
|
21
|
-
false
|
22
|
-
end
|
23
|
-
|
24
|
-
def read
|
25
|
-
Async::Task.current.sleep(@delay)
|
26
|
-
|
27
|
-
return super
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-2023, by Samuel Williams.
|
5
|
-
|
6
|
-
require_relative 'writable'
|
7
|
-
|
8
|
-
require 'async/clock'
|
9
|
-
|
10
|
-
module Async
|
11
|
-
module HTTP
|
12
|
-
module Body
|
13
|
-
# A dynamic body which you can write to and read from.
|
14
|
-
class Slowloris < Writable
|
15
|
-
class ThroughputError < StandardError
|
16
|
-
def initialize(throughput, minimum_throughput, time_since_last_write)
|
17
|
-
super("Slow write: #{throughput.round(1)}bytes/s less than required #{minimum_throughput.round}bytes/s.")
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# In order for this implementation to work correctly, you need to use a LimitedQueue.
|
22
|
-
# @param minimum_throughput [Integer] the minimum bytes per second otherwise this body will be forcefully closed.
|
23
|
-
def initialize(*arguments, minimum_throughput: 1024, **options)
|
24
|
-
super(*arguments, **options)
|
25
|
-
|
26
|
-
@minimum_throughput = minimum_throughput
|
27
|
-
|
28
|
-
@last_write_at = nil
|
29
|
-
@last_chunk_size = nil
|
30
|
-
end
|
31
|
-
|
32
|
-
attr :minimum_throughput
|
33
|
-
|
34
|
-
# If #read is called regularly to maintain throughput, that is good. If #read is not called, that is a problem. Throughput is dependent on data being available, from #write, so it doesn't seem particularly problimatic to do this check in #write.
|
35
|
-
def write(chunk)
|
36
|
-
if @last_chunk_size
|
37
|
-
time_since_last_write = Async::Clock.now - @last_write_at
|
38
|
-
throughput = @last_chunk_size / time_since_last_write
|
39
|
-
|
40
|
-
if throughput < @minimum_throughput
|
41
|
-
error = ThroughputError.new(throughput, @minimum_throughput, time_since_last_write)
|
42
|
-
|
43
|
-
self.close(error)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
super.tap do
|
48
|
-
@last_write_at = Async::Clock.now
|
49
|
-
@last_chunk_size = chunk&.bytesize
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|