funnel_http 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []