funnel_http 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +23 -0
- data/Rakefile +3 -1
- data/benchmark/benchmark.rb +3 -3
- data/ext/funnel_http/funnel_http.go +17 -0
- data/ext/funnel_http/go.mod +1 -1
- data/ext/funnel_http/go.sum +2 -0
- data/ext/funnel_http/run_requests.go +2 -2
- data/ext/funnel_http/run_requests_test.go +86 -2
- data/lib/funnel_http/client.rb +3 -0
- data/lib/funnel_http/version.rb +1 -1
- data/rbs_collection.lock.yaml +12 -4
- data/sig/funnel_http.rbs +2 -1
- metadata +5 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca358cfbd282a397a1e8e4458f00c842deb22b8c3715a57bc26dfa171c0394f5
|
4
|
+
data.tar.gz: e730cb4d2cc871a82547b6008db1f919243e8102bdd28bfaa95b9838208c38a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b567c0a370a7cdba78948dcb7ab66b97192c9bc227dc2b5da3fc5234fc955cf6e35b22455322a79235fbda3fbe51822765706229c928c5998709aa1808a6bc8
|
7
|
+
data.tar.gz: e3d5993b8445e7385c51ad410aa4d2e703c9f397129ae01551f1cb3d31cf5dcbc94c713009f02bb980c0c8c53702bb3dc31504f9e03736da472a43ea606fde31
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
|
-
[full changelog](http://github.com/sue445/funnel_http/compare/v0.
|
2
|
+
[full changelog](http://github.com/sue445/funnel_http/compare/v0.3.0...main)
|
3
|
+
|
4
|
+
## [0.3.0](https://github.com/sue445/funnel_http/releases/tag/v0.3.0) - 2025-01-12
|
5
|
+
[full changelog](http://github.com/sue445/funnel_http/compare/v0.2.0...v0.3.0)
|
6
|
+
|
7
|
+
* Support request body
|
8
|
+
* https://github.com/sue445/funnel_http/pull/51
|
9
|
+
|
10
|
+
## [0.2.0](https://github.com/sue445/funnel_http/releases/tag/v0.2.0) - 2025-01-07
|
11
|
+
[full changelog](http://github.com/sue445/funnel_http/compare/v0.1.0...v0.2.0)
|
12
|
+
|
13
|
+
* Support Ruby 3.4
|
14
|
+
* https://github.com/sue445/funnel_http/pull/48
|
3
15
|
|
4
16
|
## [0.1.0](https://github.com/sue445/funnel_http/releases/tag/v0.1.0) - 2024-12-18
|
5
17
|
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# FunnelHttp
|
2
2
|
Perform HTTP requests in parallel
|
3
3
|
|
4
|
+
[](https://badge.fury.io/rb/funnel_http)
|
4
5
|
[](https://github.com/sue445/funnel_http/actions/workflows/build.yml)
|
5
6
|
|
6
7
|
## Requirements
|
@@ -44,12 +45,24 @@ requests = [
|
|
44
45
|
"X-Multiple-Values" => ["1st value", "2nd value"],
|
45
46
|
},
|
46
47
|
},
|
48
|
+
|
49
|
+
# with request body
|
50
|
+
{
|
51
|
+
method: :post,
|
52
|
+
uri: "https://example.com/api/user",
|
53
|
+
header: {
|
54
|
+
"Authorization" => "Bearer xxxxxxxx",
|
55
|
+
"Content-Type" => "application/json",
|
56
|
+
},
|
57
|
+
body: '{"name": "sue445"}',
|
58
|
+
},
|
47
59
|
]
|
48
60
|
|
49
61
|
responses = client.perform(requests)
|
50
62
|
# => [
|
51
63
|
# { status_code: 200, body: "Response of /api/user/1", header: { "Content-Type" => ["text/plain;charset=utf-8"]} }
|
52
64
|
# { status_code: 200, body: "Response of /api/user/2", header: { "Content-Type" => ["text/plain;charset=utf-8"]} }
|
65
|
+
# { status_code: 200, body: "Response of /api/user", header: { "Content-Type" => ["text/plain;charset=utf-8"]} }
|
53
66
|
# ]
|
54
67
|
```
|
55
68
|
|
@@ -72,6 +85,16 @@ client.add_default_request_header("Authorization", "Bearer xxxxxx")
|
|
72
85
|
## API Reference
|
73
86
|
https://sue445.github.io/funnel_http/
|
74
87
|
|
88
|
+
## Performance
|
89
|
+
Depending on the case, `funnel_http` runs about 1.2x faster than pure-Ruby `Thread` :dash:
|
90
|
+
|
91
|
+
See [benchmark/](benchmark/)
|
92
|
+
|
93
|
+
### Why?
|
94
|
+
`funnel_http` uses [Go's goroutine](https://go.dev/tour/concurrency) for asynchronous processing of HTTP requests.
|
95
|
+
|
96
|
+
So this is faster than Ruby in many cases.
|
97
|
+
|
75
98
|
## Development
|
76
99
|
|
77
100
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
@@ -26,7 +26,9 @@ namespace :go do
|
|
26
26
|
sh "which golangci-lint" do |ok, _|
|
27
27
|
raise "golangci-lint isn't installed. See. https://golangci-lint.run/welcome/install/" unless ok
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
|
+
build_tag = GoGem::Util.ruby_minor_version_build_tag
|
31
|
+
sh GoGem::RakeTask.build_env_vars, "golangci-lint run --build-tags #{build_tag}"
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
data/benchmark/benchmark.rb
CHANGED
@@ -5,11 +5,11 @@ require "etc"
|
|
5
5
|
|
6
6
|
ROOT_DIR = File.expand_path("..", __dir__)
|
7
7
|
|
8
|
-
TEST_SERVER_URL = "http://localhost:8080/"
|
8
|
+
TEST_SERVER_URL = ENV.fetch("TEST_SERVER_URL") { "http://localhost:8080/" }
|
9
9
|
|
10
10
|
REQUEST_COUNT = 100
|
11
11
|
|
12
|
-
BENCHMARK_CONCURRENCY = ENV.fetch("BENCHMARK_CONCURRENCY") { 4 }
|
12
|
+
BENCHMARK_CONCURRENCY = (ENV.fetch("BENCHMARK_CONCURRENCY") { 4 }).to_i
|
13
13
|
|
14
14
|
# Build native extension before running benchmark
|
15
15
|
Dir.chdir(ROOT_DIR) do
|
@@ -32,7 +32,7 @@ def fetch_server
|
|
32
32
|
end
|
33
33
|
|
34
34
|
Benchmark.ips do |x|
|
35
|
-
x.config(warmup: 1, time: 2)
|
35
|
+
# x.config(warmup: 1, time: 2)
|
36
36
|
|
37
37
|
x.report("FunnelHttp::Client#perform") do
|
38
38
|
FunnelHttp::Client.new.perform(requests)
|
@@ -26,6 +26,7 @@ func rb_funnel_http_run_requests(self C.VALUE, rbAry C.VALUE) C.VALUE {
|
|
26
26
|
Method: getRbHashValueAsString(rbHash, "method"),
|
27
27
|
URL: getRbHashValueAsString(rbHash, "url"),
|
28
28
|
Header: getRbHashValueAsMap(rbHash, "header"),
|
29
|
+
Body: getRbHashValueAsBytes(rbHash, "body"),
|
29
30
|
}
|
30
31
|
requests = append(requests, req)
|
31
32
|
}
|
@@ -111,6 +112,22 @@ func getRbHashValueAsString(rbHash ruby.VALUE, key string) string {
|
|
111
112
|
return ruby.Value2String(value)
|
112
113
|
}
|
113
114
|
|
115
|
+
func getRbHashValueAsBytes(rbHash ruby.VALUE, key string) []byte {
|
116
|
+
value := ruby.RbHashAref(rbHash, ruby.RbToSymbol(ruby.String2Value(key)))
|
117
|
+
|
118
|
+
if value == ruby.Qnil() {
|
119
|
+
return []byte{}
|
120
|
+
}
|
121
|
+
|
122
|
+
length := ruby.RSTRING_LENINT(value)
|
123
|
+
if length == 0 {
|
124
|
+
return []byte{}
|
125
|
+
}
|
126
|
+
|
127
|
+
char := ruby.RSTRING_PTR(value)
|
128
|
+
return C.GoBytes(unsafe.Pointer(char), C.int(length))
|
129
|
+
}
|
130
|
+
|
114
131
|
func getRbHashValueAsMap(rbHash ruby.VALUE, key string) map[string][]string {
|
115
132
|
rbHashValue := ruby.RbHashAref(rbHash, ruby.RbToSymbol(ruby.String2Value(key)))
|
116
133
|
rbKeys := ruby.CallFunction(rbHashValue, "keys")
|
data/ext/funnel_http/go.mod
CHANGED
data/ext/funnel_http/go.sum
CHANGED
@@ -36,6 +36,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
|
36
36
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
37
37
|
github.com/ruby-go-gem/go-gem-wrapper v0.5.1 h1:TFGH/eOJl0uYzYMwrcLbZxXNQiQbQjCq/VLXyRk1HRM=
|
38
38
|
github.com/ruby-go-gem/go-gem-wrapper v0.5.1/go.mod h1:k2k+LziSCMxNYP4J9/9v90xdU6zlU1DJpJDTU6oJhHE=
|
39
|
+
github.com/ruby-go-gem/go-gem-wrapper v0.6.0 h1:WFu2Cj/uzKAOemsrCo4P6vsdOgB5yesrrJtAqvLkAso=
|
40
|
+
github.com/ruby-go-gem/go-gem-wrapper v0.6.0/go.mod h1:k2k+LziSCMxNYP4J9/9v90xdU6zlU1DJpJDTU6oJhHE=
|
39
41
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
40
42
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
41
43
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
@@ -13,6 +13,7 @@ type Request struct {
|
|
13
13
|
Method string
|
14
14
|
URL string
|
15
15
|
Header map[string][]string
|
16
|
+
Body []byte
|
16
17
|
}
|
17
18
|
|
18
19
|
// Response is proxy between CRuby and Go
|
@@ -33,8 +34,7 @@ func RunRequests(httpClient *http.Client, requests []Request) ([]Response, error
|
|
33
34
|
request := request
|
34
35
|
|
35
36
|
g.Go(func() error {
|
36
|
-
|
37
|
-
httpReq, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(body))
|
37
|
+
httpReq, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(request.Body))
|
38
38
|
if err != nil {
|
39
39
|
return errors.WithStack(err)
|
40
40
|
}
|
@@ -1,9 +1,11 @@
|
|
1
1
|
package main_test
|
2
2
|
|
3
3
|
import (
|
4
|
+
"github.com/cockroachdb/errors"
|
4
5
|
"github.com/jarcoal/httpmock"
|
5
6
|
"github.com/stretchr/testify/assert"
|
6
7
|
"github.com/sue445/funnel_http"
|
8
|
+
"io"
|
7
9
|
"net/http"
|
8
10
|
"testing"
|
9
11
|
)
|
@@ -42,13 +44,33 @@ func TestRunRequests(t *testing.T) {
|
|
42
44
|
return resp, nil
|
43
45
|
})
|
44
46
|
|
47
|
+
httpmock.RegisterResponder("POST", "http://example.com/1",
|
48
|
+
func(req *http.Request) (*http.Response, error) {
|
49
|
+
payload, err := io.ReadAll(req.Body)
|
50
|
+
if err != nil {
|
51
|
+
return nil, errors.WithStack(err)
|
52
|
+
}
|
53
|
+
|
54
|
+
resp := httpmock.NewStringResponse(200, string(payload))
|
55
|
+
|
56
|
+
resp.Header.Set("Content-Type", "text/plain")
|
57
|
+
|
58
|
+
for key, values := range req.Header {
|
59
|
+
for _, value := range values {
|
60
|
+
resp.Header.Add(key, value)
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
return resp, nil
|
65
|
+
})
|
66
|
+
|
45
67
|
tests := []struct {
|
46
68
|
name string
|
47
69
|
requests []main.Request
|
48
70
|
expected []main.Response
|
49
71
|
}{
|
50
72
|
{
|
51
|
-
name: "1 request",
|
73
|
+
name: "GET 1 request",
|
52
74
|
requests: []main.Request{
|
53
75
|
{
|
54
76
|
Method: "GET",
|
@@ -70,7 +92,7 @@ func TestRunRequests(t *testing.T) {
|
|
70
92
|
},
|
71
93
|
},
|
72
94
|
{
|
73
|
-
name: "multiple requests",
|
95
|
+
name: "GET multiple requests",
|
74
96
|
requests: []main.Request{
|
75
97
|
{
|
76
98
|
Method: "GET",
|
@@ -106,6 +128,68 @@ func TestRunRequests(t *testing.T) {
|
|
106
128
|
},
|
107
129
|
},
|
108
130
|
},
|
131
|
+
{
|
132
|
+
name: "POST 1 request",
|
133
|
+
requests: []main.Request{
|
134
|
+
{
|
135
|
+
Method: "POST",
|
136
|
+
URL: "http://example.com/1",
|
137
|
+
Header: map[string][]string{
|
138
|
+
"X-My-Request-Header": {"a", "b"},
|
139
|
+
},
|
140
|
+
Body: []byte("111"),
|
141
|
+
},
|
142
|
+
},
|
143
|
+
expected: []main.Response{
|
144
|
+
{
|
145
|
+
StatusCode: 200,
|
146
|
+
Body: []byte("111"),
|
147
|
+
Header: map[string][]string{
|
148
|
+
"Content-Type": {"text/plain"},
|
149
|
+
"X-My-Request-Header": {"a", "b"},
|
150
|
+
},
|
151
|
+
},
|
152
|
+
},
|
153
|
+
},
|
154
|
+
{
|
155
|
+
name: "POST multiple requests",
|
156
|
+
requests: []main.Request{
|
157
|
+
{
|
158
|
+
Method: "POST",
|
159
|
+
URL: "http://example.com/1",
|
160
|
+
Header: map[string][]string{
|
161
|
+
"X-My-Request-Header": {"a", "b"},
|
162
|
+
},
|
163
|
+
Body: []byte("111"),
|
164
|
+
},
|
165
|
+
{
|
166
|
+
Method: "POST",
|
167
|
+
URL: "http://example.com/1",
|
168
|
+
Header: map[string][]string{
|
169
|
+
"X-My-Request-Header": {"c", "d"},
|
170
|
+
},
|
171
|
+
Body: []byte("222"),
|
172
|
+
},
|
173
|
+
},
|
174
|
+
expected: []main.Response{
|
175
|
+
{
|
176
|
+
StatusCode: 200,
|
177
|
+
Body: []byte("111"),
|
178
|
+
Header: map[string][]string{
|
179
|
+
"Content-Type": {"text/plain"},
|
180
|
+
"X-My-Request-Header": {"a", "b"},
|
181
|
+
},
|
182
|
+
},
|
183
|
+
{
|
184
|
+
StatusCode: 200,
|
185
|
+
Body: []byte("222"),
|
186
|
+
Header: map[string][]string{
|
187
|
+
"Content-Type": {"text/plain"},
|
188
|
+
"X-My-Request-Header": {"c", "d"},
|
189
|
+
},
|
190
|
+
},
|
191
|
+
},
|
192
|
+
},
|
109
193
|
}
|
110
194
|
|
111
195
|
httpClient := http.Client{}
|
data/lib/funnel_http/client.rb
CHANGED
@@ -27,12 +27,14 @@ module FunnelHttp
|
|
27
27
|
# @option requests :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
28
28
|
# @option requests :url [String] **[required]** Request url
|
29
29
|
# @option requests :header [Hash{String => String, Array<String>}, nil] Request header
|
30
|
+
# @option requests :body [String, nil] Request body
|
30
31
|
#
|
31
32
|
# @overload perform(request)
|
32
33
|
# @param request [Hash{Symbol => Object}]
|
33
34
|
# @option request :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
34
35
|
# @option request :url [String] **[required]** Request url
|
35
36
|
# @option request :header [Hash{String => String, Array<String>}, nil] Request header
|
37
|
+
# @option request :body [String, nil] Request body
|
36
38
|
#
|
37
39
|
# @return [Array<Hash<Symbol => Object>>] `Array` of following `Hash`
|
38
40
|
# @return [Integer] `:status_code`
|
@@ -78,6 +80,7 @@ module FunnelHttp
|
|
78
80
|
url: request[:url].to_s,
|
79
81
|
method: request[:method].to_s.upcase,
|
80
82
|
header: normalize_header(request[:header]),
|
83
|
+
body: request[:body].freeze,
|
81
84
|
}
|
82
85
|
end
|
83
86
|
end
|
data/lib/funnel_http/version.rb
CHANGED
data/rbs_collection.lock.yaml
CHANGED
@@ -14,7 +14,7 @@ gems:
|
|
14
14
|
source:
|
15
15
|
type: git
|
16
16
|
name: ruby/gem_rbs_collection
|
17
|
-
revision:
|
17
|
+
revision: 7ff8cf6ab2759cb1d0fb2ca8d6c1f8ccab2c605c
|
18
18
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
19
19
|
repo_dir: gems
|
20
20
|
- name: fileutils
|
@@ -29,12 +29,20 @@ gems:
|
|
29
29
|
version: '0'
|
30
30
|
source:
|
31
31
|
type: stdlib
|
32
|
+
- name: parallel
|
33
|
+
version: '1.20'
|
34
|
+
source:
|
35
|
+
type: git
|
36
|
+
name: ruby/gem_rbs_collection
|
37
|
+
revision: 7ff8cf6ab2759cb1d0fb2ca8d6c1f8ccab2c605c
|
38
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
39
|
+
repo_dir: gems
|
32
40
|
- name: rack
|
33
41
|
version: '2.2'
|
34
42
|
source:
|
35
43
|
type: git
|
36
44
|
name: ruby/gem_rbs_collection
|
37
|
-
revision:
|
45
|
+
revision: 7ff8cf6ab2759cb1d0fb2ca8d6c1f8ccab2c605c
|
38
46
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
39
47
|
repo_dir: gems
|
40
48
|
- name: rake
|
@@ -42,7 +50,7 @@ gems:
|
|
42
50
|
source:
|
43
51
|
type: git
|
44
52
|
name: ruby/gem_rbs_collection
|
45
|
-
revision:
|
53
|
+
revision: 7ff8cf6ab2759cb1d0fb2ca8d6c1f8ccab2c605c
|
46
54
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
47
55
|
repo_dir: gems
|
48
56
|
- name: sinatra
|
@@ -50,7 +58,7 @@ gems:
|
|
50
58
|
source:
|
51
59
|
type: git
|
52
60
|
name: ruby/gem_rbs_collection
|
53
|
-
revision:
|
61
|
+
revision: 7ff8cf6ab2759cb1d0fb2ca8d6c1f8ccab2c605c
|
54
62
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
55
63
|
repo_dir: gems
|
56
64
|
- name: stringio
|
data/sig/funnel_http.rbs
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: funnel_http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- sue445
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-12 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: go_gem
|
@@ -16,14 +15,14 @@ dependencies:
|
|
16
15
|
requirements:
|
17
16
|
- - "~>"
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
18
|
+
version: '0.6'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - "~>"
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
25
|
+
version: '0.6'
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
27
|
name: puma
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -239,7 +238,6 @@ metadata:
|
|
239
238
|
changelog_uri: https://github.com/sue445/funnel_http/blob/main/CHANGELOG.md
|
240
239
|
documentation_uri: https://sue445.github.io/funnel_http/
|
241
240
|
rubygems_mfa_required: 'true'
|
242
|
-
post_install_message:
|
243
241
|
rdoc_options: []
|
244
242
|
require_paths:
|
245
243
|
- lib
|
@@ -254,8 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
254
252
|
- !ruby/object:Gem::Version
|
255
253
|
version: '0'
|
256
254
|
requirements: []
|
257
|
-
rubygems_version: 3.
|
258
|
-
signing_key:
|
255
|
+
rubygems_version: 3.6.2
|
259
256
|
specification_version: 4
|
260
257
|
summary: Perform HTTP requests in parallel
|
261
258
|
test_files: []
|