fast_curl 0.3.1 → 0.5.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 +135 -11
- data/ext/fast_curl/fast_curl.c +1024 -132
- data/lib/fast_curl/version.rb +1 -1
- data/lib/fast_curl.rb +132 -9
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 576cbc25d05c1a0857f8f762c3ed3744ad84c65457669c63bd5da62d904e72a0
|
|
4
|
+
data.tar.gz: b13fcb38e809a48ac3c85deb14a5ea56dbedb37803cad3cb3c2218a7bf88d483
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3fed870f48939b5d016c9951d1c2503941496706af5fa0d157c964269a925190187e1b3378dcd8a43dea5c7a7d4c56bbf8edf0923f325fd74323b0166d97361
|
|
7
|
+
data.tar.gz: 6f13f0839354222ed5ff8ede5acf39137904226ddad7f860b8b08e3f2b3dcff6fda6b65d21ed8324673f4fda1cc24e8cf51b13a32a6681740bc012507e36072c
|
data/README.md
CHANGED
|
@@ -8,13 +8,14 @@ Ultra-fast parallel HTTP client for Ruby. C extension built on libcurl `curl_mul
|
|
|
8
8
|
- **GVL release** — `rb_thread_call_without_gvl` during I/O, other Ruby threads keep running
|
|
9
9
|
- **Fiber scheduler compatible** — works inside `Async do ... end` without blocking other fibers
|
|
10
10
|
- **Three modes**: execute (all), first_execute (first N), stream_execute (yield as ready)
|
|
11
|
+
- **Lazy Enumerable sources** — bounded request preparation for large or infinite streams
|
|
11
12
|
- **Zero dependencies** — only libcurl (available everywhere)
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
15
|
-
**Requirements**: Ruby >=
|
|
16
|
+
**Requirements**: Ruby >= 2.7, libcurl
|
|
16
17
|
|
|
17
|
-
> **
|
|
18
|
+
> **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
19
|
|
|
19
20
|
```ruby
|
|
20
21
|
gem 'fast_curl'
|
|
@@ -50,16 +51,65 @@ end
|
|
|
50
51
|
|
|
51
52
|
### POST with body and headers
|
|
52
53
|
|
|
54
|
+
Be explicit about the encoding — `json:` and `form:` set the matching
|
|
55
|
+
`Content-Type` for you:
|
|
56
|
+
|
|
53
57
|
```ruby
|
|
54
58
|
FastCurl.post([
|
|
55
59
|
{
|
|
56
60
|
url: "https://api.example.com/users",
|
|
57
61
|
headers: { "Authorization" => "Bearer token" },
|
|
58
|
-
|
|
62
|
+
json: { name: "John" } # application/json
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
url: "https://api.example.com/login",
|
|
66
|
+
form: { user: "john", pass: "x" } # application/x-www-form-urlencoded
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
url: "https://api.example.com/blob",
|
|
70
|
+
headers: { "Content-Type" => "application/xml" },
|
|
71
|
+
body: "<user/>" # sent as-is
|
|
59
72
|
}
|
|
60
73
|
])
|
|
61
74
|
```
|
|
62
75
|
|
|
76
|
+
A raw String `body:` without an explicit `Content-Type` is sent as
|
|
77
|
+
`application/octet-stream`. A Hash `body:` is still encoded as JSON.
|
|
78
|
+
|
|
79
|
+
Query parameters can be passed separately:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
FastCurl.get([{ url: "https://api.example.com/search", params: { q: "ruby", page: 2 } }])
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Lazy / bounded Enumerable sources
|
|
86
|
+
|
|
87
|
+
`Array` keeps the existing fast path. Any other object responding to `#each` is
|
|
88
|
+
consumed lazily, so large request sets do not need to be materialized first:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
requests = Enumerator.new do |y|
|
|
92
|
+
1_000_000.times do |i|
|
|
93
|
+
y << { url: "https://api.example.com/items/#{i}" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
FastCurl.stream_get(requests, connections: 20, buffer: 20) do |index, response|
|
|
98
|
+
puts "#{index}: #{response[:status]}"
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For lazy sources, at most `connections + buffer` requests are retained by
|
|
103
|
+
`fast_curl`. The source itself may have produced one additional item before
|
|
104
|
+
backpressure is applied, so a generator can observe a maximum look-ahead of
|
|
105
|
+
`connections + buffer + 1`. `buffer` defaults to `connections`.
|
|
106
|
+
|
|
107
|
+
`FastCurl.get` still returns all results in input order, so its result array is
|
|
108
|
+
necessarily O(N). Use `stream_get` when the whole pipeline must stay bounded.
|
|
109
|
+
Source exceptions and stream callback exceptions unwind the native multi loop
|
|
110
|
+
and release active curl handles; an `ensure` in the source is also unwound on
|
|
111
|
+
early completed `first_*` calls.
|
|
112
|
+
|
|
63
113
|
### First N responses (cancel the rest)
|
|
64
114
|
|
|
65
115
|
```ruby
|
|
@@ -70,6 +120,19 @@ result = FastCurl.first_get([
|
|
|
70
120
|
], count: 1)
|
|
71
121
|
```
|
|
72
122
|
|
|
123
|
+
`accept:` can keep the race running until a response satisfies a predicate:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
result = FastCurl.first_get(
|
|
127
|
+
mirrors,
|
|
128
|
+
connections: 3,
|
|
129
|
+
accept: ->(response) { response[:status].between?(200, 299) }
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The predicate receives the response Hash. Rejected responses do not count
|
|
134
|
+
toward `count`.
|
|
135
|
+
|
|
73
136
|
### Stream responses as they arrive
|
|
74
137
|
|
|
75
138
|
```ruby
|
|
@@ -78,13 +141,29 @@ FastCurl.stream_get(urls, connections: 50) do |index, response|
|
|
|
78
141
|
end
|
|
79
142
|
```
|
|
80
143
|
|
|
81
|
-
###
|
|
144
|
+
### Retries
|
|
145
|
+
|
|
146
|
+
**Only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) are retried.**
|
|
147
|
+
Several retryable curl errors — `GOT_NOTHING`, `SEND_ERROR`, `RECV_ERROR`,
|
|
148
|
+
`PARTIAL_FILE` — can occur *after* the server has already accepted and processed
|
|
149
|
+
the request, so replaying a `POST` or `PATCH` may duplicate its side effects.
|
|
150
|
+
If you know the endpoint is safe to replay (e.g. it takes an idempotency key),
|
|
151
|
+
opt in with `retry_non_idempotent: true`.
|
|
152
|
+
|
|
153
|
+
`timeout` applies to a single attempt. Use `total_timeout` to bound the whole
|
|
154
|
+
call, including retries and backoff:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
FastCurl.get(urls, timeout: 5, retries: 3, total_timeout: 10_000)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Delays use exponential backoff with full jitter, starting from `retry_delay`.
|
|
82
161
|
|
|
83
162
|
```ruby
|
|
84
163
|
# Automatic retry on network errors (timeout, connection issues)
|
|
85
164
|
results = FastCurl.get([
|
|
86
165
|
{ url: "https://unreliable-api.com/data" }
|
|
87
|
-
], retries: 3, retry_delay: 1000) #
|
|
166
|
+
], retries: 3, retry_delay: 1000) # base delay 1s, doubling with jitter
|
|
88
167
|
|
|
89
168
|
# Retry on specific HTTP status codes
|
|
90
169
|
results = FastCurl.get([
|
|
@@ -109,14 +188,35 @@ end
|
|
|
109
188
|
|
|
110
189
|
## Response format
|
|
111
190
|
|
|
191
|
+
Every response — successful or not — has the same keys:
|
|
192
|
+
|
|
112
193
|
```ruby
|
|
113
194
|
[index, {
|
|
114
|
-
status: 200,
|
|
115
|
-
headers: { "
|
|
116
|
-
body: "response body"
|
|
195
|
+
status: 200, # HTTP status code, 0 on error
|
|
196
|
+
headers: { "content-type" => "application/json" },
|
|
197
|
+
body: "response body",
|
|
198
|
+
error: nil, # nil, or :curl_error / :invalid_request /
|
|
199
|
+
# :not_completed / :deadline_exceeded
|
|
200
|
+
error_code: nil, # CURLcode when error == :curl_error
|
|
201
|
+
effective_url: "https://...", # final URL after redirects
|
|
202
|
+
attempts: 1 # attempts made, including the first
|
|
117
203
|
}]
|
|
118
204
|
```
|
|
119
205
|
|
|
206
|
+
Check `response[:error]` rather than `response[:status] == 200`; a status of `0`
|
|
207
|
+
always means the request never produced an HTTP response.
|
|
208
|
+
|
|
209
|
+
Header names are normalised to lower case (HTTP/2 sends them that way and
|
|
210
|
+
HTTP/1.1 may not), and lookups are case-insensitive:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
response[:headers]["Content-Type"] # => "application/json"
|
|
214
|
+
response[:headers]["content-type"] # => "application/json"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Repeated fields fold into one comma-separated String. `set-cookie` cannot be
|
|
218
|
+
folded and is **always** an Array, even for a single cookie.
|
|
219
|
+
|
|
120
220
|
## Available methods
|
|
121
221
|
|
|
122
222
|
| Method | Description |
|
|
@@ -137,10 +237,34 @@ end
|
|
|
137
237
|
| Option | Default | Description |
|
|
138
238
|
|---|---|---|
|
|
139
239
|
| `connections` | 20 | Max parallel connections |
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
240
|
+
| `buffer` | `connections` | Lazy-source prefetch window; ignored for already-materialized Arrays |
|
|
241
|
+
| `timeout` | 30 | Timeout for a single attempt, in seconds (1-300) |
|
|
242
|
+
| `connect_timeout` | 10000 | Connection phase timeout, in milliseconds |
|
|
243
|
+
| `total_timeout` | none | Wall-clock budget for the whole call, in milliseconds |
|
|
244
|
+
| `retries` | 1 | Retry attempts for idempotent methods (0-10) |
|
|
245
|
+
| `retry_delay` | 100 | Base backoff in milliseconds; doubles with jitter |
|
|
143
246
|
| `retry_codes` | [] | HTTP status codes to retry on |
|
|
247
|
+
| `retry_non_idempotent` | false | Also retry POST and PATCH |
|
|
248
|
+
| `follow_redirects` | true | Follow `Location` headers |
|
|
249
|
+
| `max_redirects` | 5 | Redirect limit (0-100) |
|
|
250
|
+
| `accept` | none | `first_*` predicate receiving the response Hash |
|
|
251
|
+
|
|
252
|
+
DNS results and TLS sessions are cached process-wide, so repeated calls to the
|
|
253
|
+
same host skip resolution and can resume TLS. TCP connections are pooled only
|
|
254
|
+
within a single call — see Known limitations.
|
|
255
|
+
|
|
256
|
+
## Known limitations
|
|
257
|
+
|
|
258
|
+
- TCP connections are not reused across separate calls; each call builds its own
|
|
259
|
+
`curl_multi` handle. Sharing libcurl's connection cache across concurrent
|
|
260
|
+
multi handles deadlocks or crashes, so only the DNS and TLS session caches are
|
|
261
|
+
shared.
|
|
262
|
+
- The whole response body is buffered in memory (100 MB cap per response);
|
|
263
|
+
`stream_execute` streams *responses*, not bodies.
|
|
264
|
+
- HTTP/2 multiplexing is enabled, but `connections` caps in-flight requests and
|
|
265
|
+
TCP connections with the same number, so multiplexing cannot be exploited
|
|
266
|
+
beyond that limit.
|
|
267
|
+
- No multipart, cookie jar, proxy or auth helpers yet.
|
|
144
268
|
|
|
145
269
|
## Performance
|
|
146
270
|
|