hey.rb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +202 -0
- data/NOTICE +18 -0
- data/README.md +165 -0
- data/exe/hey +405 -0
- data/lib/hey/requester/now_other.rb +29 -0
- data/lib/hey/requester/now_windows.rb +46 -0
- data/lib/hey/requester/print.rb +151 -0
- data/lib/hey/requester/report.rb +312 -0
- data/lib/hey/requester.rb +533 -0
- data/lib/hey/version.rb +5 -0
- data/lib/hey.rb +8 -0
- metadata +162 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# Translated from requester/requester.go.
|
|
16
|
+
#
|
|
17
|
+
# Module Hey::Requester provides commands to run load tests and display
|
|
18
|
+
# results. Go's goroutines/channels become Async tasks and queues; net/http
|
|
19
|
+
# becomes async-http.
|
|
20
|
+
|
|
21
|
+
require "base64"
|
|
22
|
+
require "uri"
|
|
23
|
+
require "socket"
|
|
24
|
+
|
|
25
|
+
require "async"
|
|
26
|
+
require "async/barrier"
|
|
27
|
+
require "async/queue"
|
|
28
|
+
require "async/http"
|
|
29
|
+
require "async/http/endpoint"
|
|
30
|
+
require "async/http/proxy"
|
|
31
|
+
require "async/http/middleware/location_redirector"
|
|
32
|
+
|
|
33
|
+
require "io/endpoint/host_endpoint"
|
|
34
|
+
|
|
35
|
+
require "protocol/http"
|
|
36
|
+
require "protocol/http/accept_encoding"
|
|
37
|
+
require "protocol/http/body/buffered"
|
|
38
|
+
|
|
39
|
+
# Go selects now() with a build tag; Ruby selects it at require time.
|
|
40
|
+
if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
|
|
41
|
+
require_relative "requester/now_windows"
|
|
42
|
+
else
|
|
43
|
+
require_relative "requester/now_other"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require_relative "requester/report"
|
|
47
|
+
require_relative "requester/print"
|
|
48
|
+
|
|
49
|
+
module Hey
|
|
50
|
+
module Requester
|
|
51
|
+
# Max size of the buffer of result queue.
|
|
52
|
+
MAX_RESULT = 1_000_000
|
|
53
|
+
MAX_IDLE_CONN = 500
|
|
54
|
+
|
|
55
|
+
# Go's unexported `result` struct.
|
|
56
|
+
Result = Struct.new(
|
|
57
|
+
:err, # error message, or nil
|
|
58
|
+
:status_code,
|
|
59
|
+
:offset,
|
|
60
|
+
:duration,
|
|
61
|
+
:conn_duration, # connection setup(DNS lookup + Dial up) duration
|
|
62
|
+
:dns_duration, # dns lookup duration
|
|
63
|
+
:req_duration, # request "write" duration
|
|
64
|
+
:res_duration, # response "read" duration
|
|
65
|
+
:delay_duration, # delay between response and request
|
|
66
|
+
:content_length,
|
|
67
|
+
keyword_init: true
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Trace stands in for Go's httptrace.ClientTrace. net/http exposes
|
|
71
|
+
# per-phase callbacks; async-http has no equivalent hook, so the phases are
|
|
72
|
+
# recovered by instrumenting the endpoint (connect/DNS) and the request
|
|
73
|
+
# body (write completion), with the trace for the request in flight held in
|
|
74
|
+
# fiber storage.
|
|
75
|
+
class Trace
|
|
76
|
+
attr_accessor :dns_duration, :conn_duration, :conn_done_at, :req_done_at
|
|
77
|
+
|
|
78
|
+
def initialize
|
|
79
|
+
@dns_duration = 0.0
|
|
80
|
+
@conn_duration = 0.0
|
|
81
|
+
@conn_done_at = nil
|
|
82
|
+
@req_done_at = nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The trace belonging to the request this fiber is performing. Fiber
|
|
87
|
+
# storage is inherited by child fibers, so the connection pool's resource
|
|
88
|
+
# block -- which runs on behalf of the acquiring worker -- sees it too.
|
|
89
|
+
def self.current_trace
|
|
90
|
+
Fiber[:hey_trace]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.current_trace=(trace)
|
|
94
|
+
Fiber[:hey_trace] = trace
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A host endpoint that times name resolution, standing in for Go's
|
|
98
|
+
# DNSStart/DNSDone callbacks. The connect loop mirrors
|
|
99
|
+
# IO::Endpoint::HostEndpoint#connect; only the timing is added.
|
|
100
|
+
class TimingHostEndpoint < ::IO::Endpoint::HostEndpoint
|
|
101
|
+
def connect(wrapper = self.wrapper, &block)
|
|
102
|
+
trace = Requester.current_trace
|
|
103
|
+
return super if trace.nil?
|
|
104
|
+
|
|
105
|
+
last_error = nil
|
|
106
|
+
resolved = false
|
|
107
|
+
dns_start = Requester.now
|
|
108
|
+
|
|
109
|
+
::Addrinfo.foreach(*@specification) do |address|
|
|
110
|
+
unless resolved
|
|
111
|
+
resolved = true
|
|
112
|
+
trace.dns_duration = Requester.now - dns_start
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
socket = wrapper.connect(address, **@options)
|
|
117
|
+
rescue => last_error
|
|
118
|
+
# Try again if possible, otherwise raise...
|
|
119
|
+
else
|
|
120
|
+
return socket unless block_given?
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
return yield(socket)
|
|
124
|
+
ensure
|
|
125
|
+
socket.close
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise last_error
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# An endpoint that times connection establishment, standing in for Go's
|
|
135
|
+
# GetConn/GotConn callbacks. Timing at this level rather than inside the
|
|
136
|
+
# host endpoint means the TLS handshake is included, which is what
|
|
137
|
+
# net/http reports as the connection duration.
|
|
138
|
+
class TimingEndpoint < Async::HTTP::Endpoint
|
|
139
|
+
def connect(&block)
|
|
140
|
+
trace = Requester.current_trace
|
|
141
|
+
return super if trace.nil?
|
|
142
|
+
|
|
143
|
+
conn_start = Requester.now
|
|
144
|
+
begin
|
|
145
|
+
super
|
|
146
|
+
ensure
|
|
147
|
+
done = Requester.now
|
|
148
|
+
trace.conn_duration = done - conn_start
|
|
149
|
+
trace.conn_done_at = done
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def tcp_endpoint
|
|
154
|
+
TimingHostEndpoint.new(
|
|
155
|
+
[self.hostname, self.port, nil, ::Socket::SOCK_STREAM], **tcp_options
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A request body that records when the protocol finished writing it,
|
|
161
|
+
# standing in for Go's WroteRequest callback.
|
|
162
|
+
class TimingBody < ::Protocol::HTTP::Body::Buffered
|
|
163
|
+
def initialize(chunks, trace)
|
|
164
|
+
super(chunks)
|
|
165
|
+
@trace = trace
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def read
|
|
169
|
+
chunk = super
|
|
170
|
+
if chunk.nil? && @trace && @trace.req_done_at.nil?
|
|
171
|
+
@trace.req_done_at = Requester.now
|
|
172
|
+
end
|
|
173
|
+
chunk
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Request is the template a benchmark run is built from. It mirrors the
|
|
178
|
+
# parts of net/http.Request that hey actually sets; each request sent is a
|
|
179
|
+
# fresh Protocol::HTTP::Request cloned from it.
|
|
180
|
+
class Request
|
|
181
|
+
attr_accessor :method, :url, :headers, :host, :content_length, :body
|
|
182
|
+
|
|
183
|
+
# Go: http.NewRequest(method, url, body)
|
|
184
|
+
def initialize(method, url, body = nil)
|
|
185
|
+
@method = method
|
|
186
|
+
@url = url.is_a?(::URI::Generic) ? url : ::URI.parse(url.to_s)
|
|
187
|
+
raise ArgumentError, "URL must be absolute (include scheme, host): #{url}" unless @url.absolute?
|
|
188
|
+
|
|
189
|
+
@headers = ::Protocol::HTTP::Headers.new
|
|
190
|
+
@host = nil
|
|
191
|
+
@content_length = 0
|
|
192
|
+
@body = body
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Go: req.SetBasicAuth(username, password)
|
|
196
|
+
def set_basic_auth(username, password)
|
|
197
|
+
credentials = ::Base64.strict_encode64("#{username}:#{password}")
|
|
198
|
+
@headers["authorization"] = "Basic #{credentials}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The authority to send, honouring an explicit Host override.
|
|
202
|
+
def authority
|
|
203
|
+
return @host if @host && !@host.empty?
|
|
204
|
+
|
|
205
|
+
if @url.port && @url.port != @url.default_port
|
|
206
|
+
"#{@url.hostname}:#{@url.port}"
|
|
207
|
+
else
|
|
208
|
+
@url.hostname
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def path
|
|
213
|
+
path = @url.path
|
|
214
|
+
path = "/" if path.nil? || path.empty?
|
|
215
|
+
path += "?#{@url.query}" if @url.query
|
|
216
|
+
path
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# cloneRequest returns a clone of the provided request, as a wire-ready
|
|
221
|
+
# Protocol::HTTP::Request. A fresh one is built per request because a
|
|
222
|
+
# protocol-http body is consumed as it is written.
|
|
223
|
+
def self.clone_request(r, body, trace = nil)
|
|
224
|
+
headers = ::Protocol::HTTP::Headers.new(r.headers.fields.dup)
|
|
225
|
+
|
|
226
|
+
request_body =
|
|
227
|
+
if body && !body.empty?
|
|
228
|
+
TimingBody.new([body.dup], trace)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
::Protocol::HTTP::Request.new(
|
|
232
|
+
r.url.scheme, r.authority, r.method, r.path, nil, headers, request_body
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class Work
|
|
237
|
+
# Request is the request to be made.
|
|
238
|
+
attr_accessor :request
|
|
239
|
+
|
|
240
|
+
attr_accessor :request_body
|
|
241
|
+
|
|
242
|
+
# RequestFunc is a callable to generate requests. If it is nil, then
|
|
243
|
+
# Request and RequestBody are cloned for each request.
|
|
244
|
+
attr_accessor :request_func
|
|
245
|
+
|
|
246
|
+
# N is the total number of requests to make.
|
|
247
|
+
attr_accessor :n
|
|
248
|
+
|
|
249
|
+
# C is the concurrency level, the number of concurrent workers to run.
|
|
250
|
+
attr_accessor :c
|
|
251
|
+
|
|
252
|
+
# H2 is an option to make HTTP/2 requests
|
|
253
|
+
attr_accessor :h2
|
|
254
|
+
|
|
255
|
+
# Timeout in seconds.
|
|
256
|
+
attr_accessor :timeout
|
|
257
|
+
|
|
258
|
+
# QPS is the rate limit in queries per second.
|
|
259
|
+
attr_accessor :qps
|
|
260
|
+
|
|
261
|
+
# DisableCompression is an option to disable compression in response
|
|
262
|
+
attr_accessor :disable_compression
|
|
263
|
+
|
|
264
|
+
# DisableKeepAlives is an option to prevent re-use of TCP connections
|
|
265
|
+
# between different HTTP requests
|
|
266
|
+
attr_accessor :disable_keep_alives
|
|
267
|
+
|
|
268
|
+
# DisableRedirects is an option to prevent the following of HTTP
|
|
269
|
+
# redirects
|
|
270
|
+
attr_accessor :disable_redirects
|
|
271
|
+
|
|
272
|
+
# Output represents the output type. If "csv" is provided, the output
|
|
273
|
+
# will be dumped as a csv stream.
|
|
274
|
+
attr_accessor :output
|
|
275
|
+
|
|
276
|
+
# ProxyAddr is the address of the HTTP proxy server. Optional.
|
|
277
|
+
attr_accessor :proxy_addr
|
|
278
|
+
|
|
279
|
+
# Writer is where results will be written. If nil, results are written
|
|
280
|
+
# to stdout.
|
|
281
|
+
attr_accessor :writer
|
|
282
|
+
|
|
283
|
+
def initialize(request: nil, request_body: nil, request_func: nil,
|
|
284
|
+
n: 0, c: 0, h2: false, timeout: 0, qps: 0.0,
|
|
285
|
+
disable_compression: false, disable_keep_alives: false,
|
|
286
|
+
disable_redirects: false, output: "", proxy_addr: nil,
|
|
287
|
+
writer: nil)
|
|
288
|
+
@request = request
|
|
289
|
+
@request_body = request_body
|
|
290
|
+
@request_func = request_func
|
|
291
|
+
@n = n
|
|
292
|
+
@c = c
|
|
293
|
+
@h2 = h2
|
|
294
|
+
@timeout = timeout
|
|
295
|
+
@qps = qps
|
|
296
|
+
@disable_compression = disable_compression
|
|
297
|
+
@disable_keep_alives = disable_keep_alives
|
|
298
|
+
@disable_redirects = disable_redirects
|
|
299
|
+
@output = output
|
|
300
|
+
@proxy_addr = proxy_addr
|
|
301
|
+
@writer = writer
|
|
302
|
+
|
|
303
|
+
@init_once = false
|
|
304
|
+
@results = nil
|
|
305
|
+
@stop_queue = nil
|
|
306
|
+
@start = 0.0
|
|
307
|
+
@report = nil
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Go: b.writer()
|
|
311
|
+
def writer_io
|
|
312
|
+
return $stdout if @writer.nil?
|
|
313
|
+
|
|
314
|
+
@writer
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Init initializes internal data-structures.
|
|
318
|
+
def init
|
|
319
|
+
return if @init_once
|
|
320
|
+
|
|
321
|
+
@init_once = true
|
|
322
|
+
@results = Async::LimitedQueue.new([@c * 1000, MAX_RESULT].min)
|
|
323
|
+
@stop_queue = Async::LimitedQueue.new(@c)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Run makes all the requests, prints the summary. It blocks until all
|
|
327
|
+
# work is done. It can be called inside or outside a reactor.
|
|
328
|
+
def run
|
|
329
|
+
Sync { perform }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def perform
|
|
333
|
+
init
|
|
334
|
+
@start = Requester.now
|
|
335
|
+
@report = Reporter.new(writer_io, @results, @output, @n)
|
|
336
|
+
# Run the reporter first, it polls the result queue until it is closed.
|
|
337
|
+
reporter = Async { @report.run }
|
|
338
|
+
run_workers
|
|
339
|
+
finish
|
|
340
|
+
reporter.wait
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def stop
|
|
344
|
+
# Send stop signal so that workers can stop gracefully.
|
|
345
|
+
@c.times { @stop_queue.enqueue(nil) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def finish
|
|
349
|
+
@results.close
|
|
350
|
+
total = Requester.now - @start
|
|
351
|
+
# Wait until the reporter is done.
|
|
352
|
+
@report.done.dequeue
|
|
353
|
+
@report.finalize(total)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def make_request(client)
|
|
357
|
+
s = Requester.now
|
|
358
|
+
size = 0
|
|
359
|
+
code = 0
|
|
360
|
+
res_start = 0.0
|
|
361
|
+
err = nil
|
|
362
|
+
|
|
363
|
+
trace = Trace.new
|
|
364
|
+
Requester.current_trace = trace
|
|
365
|
+
|
|
366
|
+
req =
|
|
367
|
+
if @request_func
|
|
368
|
+
@request_func.call
|
|
369
|
+
else
|
|
370
|
+
Requester.clone_request(@request, @request_body, trace)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Go's transport adds this; async-http leaves connection reuse to the
|
|
374
|
+
# pool, so the header is what turns keep-alive off.
|
|
375
|
+
req.headers["connection"] = "close" if @disable_keep_alives
|
|
376
|
+
|
|
377
|
+
call_start = Requester.now
|
|
378
|
+
begin
|
|
379
|
+
with_timeout do
|
|
380
|
+
response = client.call(req)
|
|
381
|
+
begin
|
|
382
|
+
size = response.body&.length || -1
|
|
383
|
+
code = response.status
|
|
384
|
+
res_start = Requester.now
|
|
385
|
+
# Go: io.Copy(io.Discard, resp.Body)
|
|
386
|
+
response.body&.each { |_chunk| }
|
|
387
|
+
ensure
|
|
388
|
+
response.close
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
rescue StandardError, Async::TimeoutError => e
|
|
392
|
+
err = error_message(req, e)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
t = Requester.now
|
|
396
|
+
res_duration = t - res_start
|
|
397
|
+
finish = t - s
|
|
398
|
+
|
|
399
|
+
# Go reads reqStart at GotConn and reqDuration at WroteRequest.
|
|
400
|
+
req_start = trace.conn_done_at || call_start
|
|
401
|
+
req_done = trace.req_done_at || req_start
|
|
402
|
+
req_duration = req_done - req_start
|
|
403
|
+
delay_duration = res_start > 0 ? res_start - req_done : 0.0
|
|
404
|
+
|
|
405
|
+
@results.enqueue(Result.new(
|
|
406
|
+
offset: s,
|
|
407
|
+
status_code: code,
|
|
408
|
+
duration: finish,
|
|
409
|
+
err: err,
|
|
410
|
+
content_length: size,
|
|
411
|
+
conn_duration: trace.conn_duration,
|
|
412
|
+
dns_duration: trace.dns_duration,
|
|
413
|
+
req_duration: req_duration,
|
|
414
|
+
res_duration: res_duration,
|
|
415
|
+
delay_duration: delay_duration
|
|
416
|
+
))
|
|
417
|
+
ensure
|
|
418
|
+
Requester.current_trace = nil
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def run_worker(client, n)
|
|
422
|
+
# Go uses a time.Ticker; the deadline is tracked explicitly here, and
|
|
423
|
+
# a missed tick is dropped rather than queued, as the ticker does.
|
|
424
|
+
interval = nil
|
|
425
|
+
next_tick = nil
|
|
426
|
+
if @qps > 0
|
|
427
|
+
interval = (1e6 / @qps).to_i / 1e6
|
|
428
|
+
next_tick = Requester.now + interval
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
n.times do
|
|
432
|
+
# Check if application is stopped. Do not send into a closed queue.
|
|
433
|
+
unless @stop_queue.empty?
|
|
434
|
+
@stop_queue.dequeue
|
|
435
|
+
return
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if interval
|
|
439
|
+
delay = next_tick - Requester.now
|
|
440
|
+
if delay > 0
|
|
441
|
+
sleep(delay)
|
|
442
|
+
next_tick += interval
|
|
443
|
+
else
|
|
444
|
+
next_tick = Requester.now + interval
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
make_request(client)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def run_workers
|
|
453
|
+
endpoint = build_endpoint
|
|
454
|
+
client = Async::HTTP::Client.new(
|
|
455
|
+
endpoint,
|
|
456
|
+
protocol: endpoint.protocol,
|
|
457
|
+
# Go caps idle connections per host at min(C, maxIdleConn); the pool
|
|
458
|
+
# limit here caps live connections, so it tracks the concurrency.
|
|
459
|
+
limit: [@c, 1].max
|
|
460
|
+
)
|
|
461
|
+
app = wrap(client)
|
|
462
|
+
|
|
463
|
+
barrier = Async::Barrier.new
|
|
464
|
+
# Ignore the case where n % c != 0.
|
|
465
|
+
@c.times do
|
|
466
|
+
barrier.async do
|
|
467
|
+
run_worker(app, @n / @c)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
barrier.wait
|
|
471
|
+
ensure
|
|
472
|
+
barrier&.stop
|
|
473
|
+
client&.close
|
|
474
|
+
@proxy_client&.close
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
private
|
|
478
|
+
|
|
479
|
+
# Go configures http.Transport; the equivalents here are the endpoint
|
|
480
|
+
# (TLS, protocol) and a middleware stack (redirects, compression).
|
|
481
|
+
def build_endpoint
|
|
482
|
+
protocol = @h2 ? Async::HTTP::Protocol::HTTP2 : Async::HTTP::Protocol::HTTP11
|
|
483
|
+
|
|
484
|
+
endpoint = TimingEndpoint.new(
|
|
485
|
+
@request.url,
|
|
486
|
+
protocol: protocol,
|
|
487
|
+
# Go: tls.Config{InsecureSkipVerify: true}
|
|
488
|
+
ssl_context: insecure_ssl_context(protocol)
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if @proxy_addr
|
|
492
|
+
@proxy_client = Async::HTTP::Client.new(Async::HTTP::Endpoint[@proxy_addr])
|
|
493
|
+
# Tunnels via CONNECT; the connection is the proxy's, so the
|
|
494
|
+
# DNS/connect timings above do not apply to it.
|
|
495
|
+
endpoint = Async::HTTP::Proxy.endpoint(@proxy_client, endpoint)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
endpoint
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def insecure_ssl_context(protocol)
|
|
502
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
503
|
+
context.alpn_protocols = protocol.names if protocol.respond_to?(:names)
|
|
504
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
505
|
+
context
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def wrap(client)
|
|
509
|
+
app = client
|
|
510
|
+
# net/http follows up to 10 redirects unless told not to; async-http
|
|
511
|
+
# follows none unless asked, so the sense of the flag is inverted.
|
|
512
|
+
app = Async::HTTP::Middleware::LocationRedirector.new(app, 10) unless @disable_redirects
|
|
513
|
+
# net/http requests gzip by default; async-http does not.
|
|
514
|
+
app = ::Protocol::HTTP::AcceptEncoding.new(app) unless @disable_compression
|
|
515
|
+
app
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Go: http.Client{Timeout: ...}, where 0 means no timeout.
|
|
519
|
+
def with_timeout(&block)
|
|
520
|
+
return block.call unless @timeout.to_i > 0
|
|
521
|
+
|
|
522
|
+
Async::Task.current.with_timeout(@timeout, &block)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Shaped after net/http's error strings, e.g.
|
|
526
|
+
# Get "http://host/": dial tcp ...: connection refused
|
|
527
|
+
def error_message(req, error)
|
|
528
|
+
verb = req.method.to_s.capitalize
|
|
529
|
+
"#{verb} \"#{@request.url}\": #{error.message}"
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
data/lib/hey/version.rb
ADDED
data/lib/hey.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hey.rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Kidd
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.46'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.46'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: async-http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.105'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.105'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: base64
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: erb
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '4.0'
|
|
61
|
+
- - "<"
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '7'
|
|
64
|
+
type: :runtime
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '4.0'
|
|
71
|
+
- - "<"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '7'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: io-endpoint
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0.19'
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0.19'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: protocol-http
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0.72'
|
|
95
|
+
type: :runtime
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0.72'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: gem_kit
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0.2'
|
|
109
|
+
type: :runtime
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0.2'
|
|
116
|
+
description: 'hey sends load to a web application and reports a latency histogram,
|
|
117
|
+
percentile distribution and per-phase timings. A file-by-file Ruby port of rakyll/hey:
|
|
118
|
+
goroutines and channels become Async tasks and queues, and net/http becomes async-http.
|
|
119
|
+
The gem is named hey.rb because hey was taken.'
|
|
120
|
+
email:
|
|
121
|
+
- nathankidd@hey.com
|
|
122
|
+
executables:
|
|
123
|
+
- hey
|
|
124
|
+
extensions: []
|
|
125
|
+
extra_rdoc_files: []
|
|
126
|
+
files:
|
|
127
|
+
- CHANGELOG.md
|
|
128
|
+
- LICENSE
|
|
129
|
+
- NOTICE
|
|
130
|
+
- README.md
|
|
131
|
+
- exe/hey
|
|
132
|
+
- lib/hey.rb
|
|
133
|
+
- lib/hey/requester.rb
|
|
134
|
+
- lib/hey/requester/now_other.rb
|
|
135
|
+
- lib/hey/requester/now_windows.rb
|
|
136
|
+
- lib/hey/requester/print.rb
|
|
137
|
+
- lib/hey/requester/report.rb
|
|
138
|
+
- lib/hey/version.rb
|
|
139
|
+
homepage: https://github.com/n-at-han-k/hey.rb
|
|
140
|
+
licenses:
|
|
141
|
+
- Apache-2.0
|
|
142
|
+
metadata:
|
|
143
|
+
source_code_uri: https://github.com/n-at-han-k/hey.rb
|
|
144
|
+
changelog_uri: https://github.com/n-at-han-k/hey.rb/blob/main/CHANGELOG.md
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '3.2'
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubygems_version: 3.7.2
|
|
160
|
+
specification_version: 4
|
|
161
|
+
summary: An HTTP load generator. A Ruby port of rakyll/hey, built on async-http.
|
|
162
|
+
test_files: []
|