fast_curl 0.3.0 → 0.4.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/README.md +91 -11
- data/ext/fast_curl/extconf.rb +3 -0
- data/ext/fast_curl/fast_curl.c +1151 -359
- data/lib/fast_curl/version.rb +1 -1
- data/lib/fast_curl.rb +114 -6
- metadata +37 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32aca3a9aec734c69bc40aa9b84b519aeddc15defc3ad3a9e8f278f63d9f0ffb
|
|
4
|
+
data.tar.gz: 8037377b3d3e5ab02f4171c3f4ae56e6f42552a6cb5c6d7e2f5fd54921e2cc71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 670eefe8b9fa597e947ae0858bda833d35d15914fbd62e3093483cdf8c2c68560c121586c4b01eec75f81d3a4f6d49d04829d5711180178247f051cb510c8cd6
|
|
7
|
+
data.tar.gz: 709792e880465cc09de983246936ce66a843f5b7899e69ac6f5a390cef35b4cdc8baaf1dbda36abe39627cc2fe457e994076329ab746887d000d1a089e649af7
|
data/README.md
CHANGED
|
@@ -12,9 +12,9 @@ Ultra-fast parallel HTTP client for Ruby. C extension built on libcurl `curl_mul
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
**Requirements**: Ruby >=
|
|
15
|
+
**Requirements**: Ruby >= 2.7, libcurl
|
|
16
16
|
|
|
17
|
-
> **
|
|
17
|
+
> **Fiber Scheduler support requires Ruby >= 3.1.** The C extension uses `rb_fiber_scheduler_current`, `rb_fiber_scheduler_block` and `rb_fiber_scheduler_unblock` to yield control to the Fiber Scheduler during I/O; these APIs are stable from Ruby 3.1. On 2.7 and 3.0 the extension builds and runs correctly, but that code is compiled out — so a request made inside a scheduler blocks the whole thread and **no sibling fiber runs until it finishes**. Other OS threads are unaffected, since the GVL is still released. If you use `async`, use Ruby >= 3.1.
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
20
|
gem 'fast_curl'
|
|
@@ -50,16 +50,37 @@ end
|
|
|
50
50
|
|
|
51
51
|
### POST with body and headers
|
|
52
52
|
|
|
53
|
+
Be explicit about the encoding — `json:` and `form:` set the matching
|
|
54
|
+
`Content-Type` for you:
|
|
55
|
+
|
|
53
56
|
```ruby
|
|
54
57
|
FastCurl.post([
|
|
55
58
|
{
|
|
56
59
|
url: "https://api.example.com/users",
|
|
57
60
|
headers: { "Authorization" => "Bearer token" },
|
|
58
|
-
|
|
61
|
+
json: { name: "John" } # application/json
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
url: "https://api.example.com/login",
|
|
65
|
+
form: { user: "john", pass: "x" } # application/x-www-form-urlencoded
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
url: "https://api.example.com/blob",
|
|
69
|
+
headers: { "Content-Type" => "application/xml" },
|
|
70
|
+
body: "<user/>" # sent as-is
|
|
59
71
|
}
|
|
60
72
|
])
|
|
61
73
|
```
|
|
62
74
|
|
|
75
|
+
A raw String `body:` without an explicit `Content-Type` is sent as
|
|
76
|
+
`application/octet-stream`. A Hash `body:` is still encoded as JSON.
|
|
77
|
+
|
|
78
|
+
Query parameters can be passed separately:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
FastCurl.get([{ url: "https://api.example.com/search", params: { q: "ruby", page: 2 } }])
|
|
82
|
+
```
|
|
83
|
+
|
|
63
84
|
### First N responses (cancel the rest)
|
|
64
85
|
|
|
65
86
|
```ruby
|
|
@@ -78,13 +99,29 @@ FastCurl.stream_get(urls, connections: 50) do |index, response|
|
|
|
78
99
|
end
|
|
79
100
|
```
|
|
80
101
|
|
|
81
|
-
###
|
|
102
|
+
### Retries
|
|
103
|
+
|
|
104
|
+
**Only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) are retried.**
|
|
105
|
+
Several retryable curl errors — `GOT_NOTHING`, `SEND_ERROR`, `RECV_ERROR`,
|
|
106
|
+
`PARTIAL_FILE` — can occur *after* the server has already accepted and processed
|
|
107
|
+
the request, so replaying a `POST` or `PATCH` may duplicate its side effects.
|
|
108
|
+
If you know the endpoint is safe to replay (e.g. it takes an idempotency key),
|
|
109
|
+
opt in with `retry_non_idempotent: true`.
|
|
110
|
+
|
|
111
|
+
`timeout` applies to a single attempt. Use `total_timeout` to bound the whole
|
|
112
|
+
call, including retries and backoff:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
FastCurl.get(urls, timeout: 5, retries: 3, total_timeout: 10_000)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Delays use exponential backoff with full jitter, starting from `retry_delay`.
|
|
82
119
|
|
|
83
120
|
```ruby
|
|
84
121
|
# Automatic retry on network errors (timeout, connection issues)
|
|
85
122
|
results = FastCurl.get([
|
|
86
123
|
{ url: "https://unreliable-api.com/data" }
|
|
87
|
-
], retries: 3, retry_delay: 1000) #
|
|
124
|
+
], retries: 3, retry_delay: 1000) # base delay 1s, doubling with jitter
|
|
88
125
|
|
|
89
126
|
# Retry on specific HTTP status codes
|
|
90
127
|
results = FastCurl.get([
|
|
@@ -109,14 +146,35 @@ end
|
|
|
109
146
|
|
|
110
147
|
## Response format
|
|
111
148
|
|
|
149
|
+
Every response — successful or not — has the same keys:
|
|
150
|
+
|
|
112
151
|
```ruby
|
|
113
152
|
[index, {
|
|
114
|
-
status: 200,
|
|
115
|
-
headers: { "
|
|
116
|
-
body: "response body"
|
|
153
|
+
status: 200, # HTTP status code, 0 on error
|
|
154
|
+
headers: { "content-type" => "application/json" },
|
|
155
|
+
body: "response body",
|
|
156
|
+
error: nil, # nil, or :curl_error / :invalid_request /
|
|
157
|
+
# :not_completed / :deadline_exceeded
|
|
158
|
+
error_code: nil, # CURLcode when error == :curl_error
|
|
159
|
+
effective_url: "https://...", # final URL after redirects
|
|
160
|
+
attempts: 1 # attempts made, including the first
|
|
117
161
|
}]
|
|
118
162
|
```
|
|
119
163
|
|
|
164
|
+
Check `response[:error]` rather than `response[:status] == 200`; a status of `0`
|
|
165
|
+
always means the request never produced an HTTP response.
|
|
166
|
+
|
|
167
|
+
Header names are normalised to lower case (HTTP/2 sends them that way and
|
|
168
|
+
HTTP/1.1 may not), and lookups are case-insensitive:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
response[:headers]["Content-Type"] # => "application/json"
|
|
172
|
+
response[:headers]["content-type"] # => "application/json"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Repeated fields fold into one comma-separated String. `set-cookie` cannot be
|
|
176
|
+
folded and is **always** an Array, even for a single cookie.
|
|
177
|
+
|
|
120
178
|
## Available methods
|
|
121
179
|
|
|
122
180
|
| Method | Description |
|
|
@@ -137,10 +195,32 @@ end
|
|
|
137
195
|
| Option | Default | Description |
|
|
138
196
|
|---|---|---|
|
|
139
197
|
| `connections` | 20 | Max parallel connections |
|
|
140
|
-
| `timeout` | 30 |
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
198
|
+
| `timeout` | 30 | Timeout for a single attempt, in seconds (1-300) |
|
|
199
|
+
| `connect_timeout` | 10000 | Connection phase timeout, in milliseconds |
|
|
200
|
+
| `total_timeout` | none | Wall-clock budget for the whole call, in milliseconds |
|
|
201
|
+
| `retries` | 1 | Retry attempts for idempotent methods (0-10) |
|
|
202
|
+
| `retry_delay` | 100 | Base backoff in milliseconds; doubles with jitter |
|
|
143
203
|
| `retry_codes` | [] | HTTP status codes to retry on |
|
|
204
|
+
| `retry_non_idempotent` | false | Also retry POST and PATCH |
|
|
205
|
+
| `follow_redirects` | true | Follow `Location` headers |
|
|
206
|
+
| `max_redirects` | 5 | Redirect limit (0-100) |
|
|
207
|
+
|
|
208
|
+
DNS results and TLS sessions are cached process-wide, so repeated calls to the
|
|
209
|
+
same host skip resolution and can resume TLS. TCP connections are pooled only
|
|
210
|
+
within a single call — see Known limitations.
|
|
211
|
+
|
|
212
|
+
## Known limitations
|
|
213
|
+
|
|
214
|
+
- TCP connections are not reused across separate calls; each call builds its own
|
|
215
|
+
`curl_multi` handle. Sharing libcurl's connection cache across concurrent
|
|
216
|
+
multi handles deadlocks or crashes, so only the DNS and TLS session caches are
|
|
217
|
+
shared.
|
|
218
|
+
- The whole response body is buffered in memory (100 MB cap per response);
|
|
219
|
+
`stream_execute` streams *responses*, not bodies.
|
|
220
|
+
- HTTP/2 multiplexing is enabled, but `connections` caps in-flight requests and
|
|
221
|
+
TCP connections with the same number, so multiplexing cannot be exploited
|
|
222
|
+
beyond that limit.
|
|
223
|
+
- No multipart, cookie jar, proxy or auth helpers yet.
|
|
144
224
|
|
|
145
225
|
## Performance
|
|
146
226
|
|
data/ext/fast_curl/extconf.rb
CHANGED
|
@@ -8,6 +8,9 @@ have_header("ruby/fiber/scheduler.h")
|
|
|
8
8
|
|
|
9
9
|
have_func("curl_multi_wakeup", "curl/curl.h")
|
|
10
10
|
have_func("rb_fiber_scheduler_current", "ruby.h")
|
|
11
|
+
have_func("rb_fiber_scheduler_block", "ruby.h")
|
|
12
|
+
have_func("rb_fiber_scheduler_unblock", "ruby.h")
|
|
13
|
+
have_func("rb_fiber_current", "ruby.h")
|
|
11
14
|
have_func("rb_io_wait", "ruby.h")
|
|
12
15
|
|
|
13
16
|
$CFLAGS << " -std=c99 -O2 -Wall -Wextra -Wno-unused-parameter"
|