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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc6d1f5ffef3ad3e41de07eb3b94342bd082724aa3d1daad338603fbedbabcdc
4
- data.tar.gz: f3208ba0c9154bd99fd2d373e91d3679690f0f711c74a3f11038c500c08fbd8a
3
+ metadata.gz: ca358cfbd282a397a1e8e4458f00c842deb22b8c3715a57bc26dfa171c0394f5
4
+ data.tar.gz: e730cb4d2cc871a82547b6008db1f919243e8102bdd28bfaa95b9838208c38a1
5
5
  SHA512:
6
- metadata.gz: 87f9ad18e8c0005f2a3520d06381f3e308bbf6372827b631d646546c48f929be5879da1296331d9bab4ec7667786ce353473930b6795b84d0a46c11f9e68ec12
7
- data.tar.gz: c92e03744f09aa670972e188bb0c94db2b2920f028994db5bb0f26d3804d470345b43087d57195666d1236bfc2adcc9c8ee8ccde4c0ca50ae94b54887a0c1dd6
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.1.0...main)
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
+ [![Gem Version](https://badge.fury.io/rb/funnel_http.svg)](https://badge.fury.io/rb/funnel_http)
4
5
  [![build](https://github.com/sue445/funnel_http/actions/workflows/build.yml/badge.svg)](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
- sh GoGem::RakeTask.build_env_vars, "golangci-lint run"
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
 
@@ -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")
@@ -5,7 +5,7 @@ go 1.23
5
5
  require (
6
6
  github.com/cockroachdb/errors v1.11.3
7
7
  github.com/jarcoal/httpmock v1.3.1
8
- github.com/ruby-go-gem/go-gem-wrapper v0.5.1
8
+ github.com/ruby-go-gem/go-gem-wrapper v0.6.0
9
9
  github.com/stretchr/testify v1.10.0
10
10
  golang.org/x/sync v0.10.0
11
11
  )
@@ -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
- var body []byte
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{}
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FunnelHttp
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -14,7 +14,7 @@ gems:
14
14
  source:
15
15
  type: git
16
16
  name: ruby/gem_rbs_collection
17
- revision: 0a6ea105a0afc7eaee4494585a7775f47eea6145
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: 0a6ea105a0afc7eaee4494585a7775f47eea6145
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: 0a6ea105a0afc7eaee4494585a7775f47eea6145
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: 0a6ea105a0afc7eaee4494585a7775f47eea6145
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
@@ -10,7 +10,8 @@ module FunnelHttp
10
10
  type fuzzy_request = {
11
11
  method: String | Symbol,
12
12
  url: String,
13
- header: fuzzy_header?
13
+ header: fuzzy_header?,
14
+ body: String?,
14
15
  }
15
16
 
16
17
  type strict_header = Hash[String, Array[String]]
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.1.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: 2024-12-17 00:00:00.000000000 Z
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.5'
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.5'
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.5.22
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: []