fast_curl 0.4.0 → 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 +44 -0
- data/ext/fast_curl/fast_curl.c +537 -26
- data/lib/fast_curl/version.rb +1 -1
- data/lib/fast_curl.rb +31 -16
- 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,6 +8,7 @@ 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
|
|
@@ -81,6 +82,34 @@ Query parameters can be passed separately:
|
|
|
81
82
|
FastCurl.get([{ url: "https://api.example.com/search", params: { q: "ruby", page: 2 } }])
|
|
82
83
|
```
|
|
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
|
+
|
|
84
113
|
### First N responses (cancel the rest)
|
|
85
114
|
|
|
86
115
|
```ruby
|
|
@@ -91,6 +120,19 @@ result = FastCurl.first_get([
|
|
|
91
120
|
], count: 1)
|
|
92
121
|
```
|
|
93
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
|
+
|
|
94
136
|
### Stream responses as they arrive
|
|
95
137
|
|
|
96
138
|
```ruby
|
|
@@ -195,6 +237,7 @@ folded and is **always** an Array, even for a single cookie.
|
|
|
195
237
|
| Option | Default | Description |
|
|
196
238
|
|---|---|---|
|
|
197
239
|
| `connections` | 20 | Max parallel connections |
|
|
240
|
+
| `buffer` | `connections` | Lazy-source prefetch window; ignored for already-materialized Arrays |
|
|
198
241
|
| `timeout` | 30 | Timeout for a single attempt, in seconds (1-300) |
|
|
199
242
|
| `connect_timeout` | 10000 | Connection phase timeout, in milliseconds |
|
|
200
243
|
| `total_timeout` | none | Wall-clock budget for the whole call, in milliseconds |
|
|
@@ -204,6 +247,7 @@ folded and is **always** an Array, even for a single cookie.
|
|
|
204
247
|
| `retry_non_idempotent` | false | Also retry POST and PATCH |
|
|
205
248
|
| `follow_redirects` | true | Follow `Location` headers |
|
|
206
249
|
| `max_redirects` | 5 | Redirect limit (0-100) |
|
|
250
|
+
| `accept` | none | `first_*` predicate receiving the response Hash |
|
|
207
251
|
|
|
208
252
|
DNS results and TLS sessions are cached process-wide, so repeated calls to the
|
|
209
253
|
same host skip resolution and can resume TLS. TCP connections are pooled only
|
data/ext/fast_curl/fast_curl.c
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
#define MAX_RETRIES 10
|
|
33
33
|
#define MAX_REQUESTS 10000
|
|
34
34
|
#define MAX_CONNECTIONS 100
|
|
35
|
+
#define MAX_BUFFER 10000
|
|
35
36
|
#define MAX_RETRY_DELAY_MS 30000
|
|
36
37
|
#define DEFAULT_RETRIES 1
|
|
37
38
|
#define DEFAULT_RETRY_DELAY 100
|
|
@@ -72,6 +73,8 @@ typedef enum {
|
|
|
72
73
|
KEY_EFFECTIVE_URL,
|
|
73
74
|
KEY_ERROR,
|
|
74
75
|
KEY_ATTEMPTS,
|
|
76
|
+
KEY_BUFFER,
|
|
77
|
+
KEY_ACCEPT,
|
|
75
78
|
KEY_LAST
|
|
76
79
|
} key_id_t;
|
|
77
80
|
|
|
@@ -101,6 +104,8 @@ static const char *const KEY_NAMES[KEY_LAST] = {
|
|
|
101
104
|
"effective_url",
|
|
102
105
|
"error",
|
|
103
106
|
"attempts",
|
|
107
|
+
"buffer",
|
|
108
|
+
"accept",
|
|
104
109
|
};
|
|
105
110
|
|
|
106
111
|
typedef enum {
|
|
@@ -320,7 +325,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
|
|
|
320
325
|
|
|
321
326
|
typedef struct {
|
|
322
327
|
CURL *easy;
|
|
323
|
-
|
|
328
|
+
long index;
|
|
324
329
|
buffer_t body;
|
|
325
330
|
header_list_t headers;
|
|
326
331
|
struct curl_slist *req_headers;
|
|
@@ -341,7 +346,7 @@ static VALUE fast_validation_error = Qnil;
|
|
|
341
346
|
(ctx)->setup_error_fatal = (fatal); \
|
|
342
347
|
} while (0)
|
|
343
348
|
|
|
344
|
-
static inline void request_ctx_init(request_ctx_t *ctx,
|
|
349
|
+
static inline void request_ctx_init(request_ctx_t *ctx, long index) {
|
|
345
350
|
ctx->easy = NULL;
|
|
346
351
|
ctx->index = index;
|
|
347
352
|
buffer_init(&ctx->body);
|
|
@@ -961,30 +966,52 @@ static void unblock_perform(void *arg) {
|
|
|
961
966
|
|
|
962
967
|
typedef struct {
|
|
963
968
|
VALUE results;
|
|
969
|
+
VALUE accept;
|
|
964
970
|
int completed;
|
|
965
971
|
int target;
|
|
966
972
|
int stream;
|
|
967
973
|
} completion_ctx_t;
|
|
968
974
|
|
|
969
|
-
static VALUE build_result_pair(
|
|
970
|
-
return rb_ary_new_from_args(2,
|
|
975
|
+
static VALUE build_result_pair(long index, VALUE response) {
|
|
976
|
+
return rb_ary_new_from_args(2, LONG2NUM(index), response);
|
|
971
977
|
}
|
|
972
978
|
|
|
973
|
-
static int
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
VALUE pair =
|
|
977
|
-
build_result_pair(index, build_error_response(message, ERR_INVALID_REQUEST, attempts));
|
|
979
|
+
static int response_accepted(completion_ctx_t *cctx, VALUE response) {
|
|
980
|
+
if (cctx->target <= 0 || NIL_P(cctx->accept))
|
|
981
|
+
return 1;
|
|
978
982
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
else
|
|
982
|
-
rb_ary_push(cctx->results, pair);
|
|
983
|
+
return RTEST(rb_funcall(cctx->accept, rb_intern("call"), 1, response));
|
|
984
|
+
}
|
|
983
985
|
|
|
986
|
+
static int record_response(completion_ctx_t *cctx, long index, VALUE response) {
|
|
987
|
+
VALUE pair = build_result_pair(index, response);
|
|
988
|
+
|
|
989
|
+
if (cctx->stream) {
|
|
990
|
+
rb_yield(pair);
|
|
984
991
|
cctx->completed++;
|
|
985
|
-
|
|
986
|
-
return 1;
|
|
992
|
+
return 0;
|
|
987
993
|
}
|
|
994
|
+
|
|
995
|
+
if (cctx->target > 0) {
|
|
996
|
+
if (!response_accepted(cctx, response))
|
|
997
|
+
return 0;
|
|
998
|
+
|
|
999
|
+
rb_ary_push(cctx->results, pair);
|
|
1000
|
+
cctx->completed++;
|
|
1001
|
+
return cctx->completed >= cctx->target;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
rb_ary_store(cctx->results, index, pair);
|
|
1005
|
+
cctx->completed++;
|
|
1006
|
+
return 0;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
static int record_immediate_error(completion_ctx_t *cctx, long index, const char *message,
|
|
1010
|
+
int attempts) {
|
|
1011
|
+
if (cctx->stream || cctx->target > 0)
|
|
1012
|
+
return record_response(cctx, index,
|
|
1013
|
+
build_error_response(message, ERR_INVALID_REQUEST, attempts));
|
|
1014
|
+
|
|
988
1015
|
return 0;
|
|
989
1016
|
}
|
|
990
1017
|
|
|
@@ -1019,17 +1046,11 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
|
|
|
1019
1046
|
? build_response(ctx)
|
|
1020
1047
|
: build_error_response_with_code(curl_easy_strerror(msg->data.result),
|
|
1021
1048
|
(int)msg->data.result, ctx->attempts);
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
else
|
|
1027
|
-
rb_ary_push(cctx->results, pair);
|
|
1049
|
+
if (record_response(cctx, ctx->index, response))
|
|
1050
|
+
return 1;
|
|
1051
|
+
} else {
|
|
1052
|
+
cctx->completed++;
|
|
1028
1053
|
}
|
|
1029
|
-
|
|
1030
|
-
cctx->completed++;
|
|
1031
|
-
if (cctx->target > 0 && cctx->completed >= cctx->target)
|
|
1032
|
-
return 1;
|
|
1033
1054
|
}
|
|
1034
1055
|
|
|
1035
1056
|
return 0;
|
|
@@ -1417,10 +1438,14 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1417
1438
|
|
|
1418
1439
|
completion_ctx_t cctx;
|
|
1419
1440
|
cctx.results = stream ? Qnil : ((target > 0) ? rb_ary_new2(target) : rb_ary_new2(count));
|
|
1441
|
+
cctx.accept = target > 0 && !NIL_P(ea->options) ? hash_aref_key(ea->options, KEY_ACCEPT) : Qnil;
|
|
1420
1442
|
cctx.completed = 0;
|
|
1421
1443
|
cctx.target = target;
|
|
1422
1444
|
cctx.stream = stream;
|
|
1423
1445
|
|
|
1446
|
+
if (!NIL_P(cctx.accept) && !rb_respond_to(cctx.accept, rb_intern("call")))
|
|
1447
|
+
rb_raise(rb_eArgError, "accept must respond to #call");
|
|
1448
|
+
|
|
1424
1449
|
if (!stream && target <= 0) {
|
|
1425
1450
|
for (int i = 0; i < count; i++)
|
|
1426
1451
|
rb_ary_store(cctx.results, i, Qnil);
|
|
@@ -1522,6 +1547,482 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1522
1547
|
return stream ? Qnil : cctx.results;
|
|
1523
1548
|
}
|
|
1524
1549
|
|
|
1550
|
+
typedef enum {
|
|
1551
|
+
LAZY_SLOT_FREE,
|
|
1552
|
+
LAZY_SLOT_PENDING,
|
|
1553
|
+
LAZY_SLOT_ACTIVE,
|
|
1554
|
+
LAZY_SLOT_RETRY_WAIT
|
|
1555
|
+
} lazy_slot_state_t;
|
|
1556
|
+
|
|
1557
|
+
typedef struct {
|
|
1558
|
+
multi_session_t session;
|
|
1559
|
+
request_options_t opts;
|
|
1560
|
+
retry_config_t retry_cfg;
|
|
1561
|
+
completion_ctx_t cctx;
|
|
1562
|
+
VALUE source;
|
|
1563
|
+
VALUE anchors;
|
|
1564
|
+
int *states;
|
|
1565
|
+
int *free_slots;
|
|
1566
|
+
int free_count;
|
|
1567
|
+
int *pending_slots;
|
|
1568
|
+
int pending_head;
|
|
1569
|
+
int pending_tail;
|
|
1570
|
+
int pending_count;
|
|
1571
|
+
long long *retry_at_ms;
|
|
1572
|
+
int capacity;
|
|
1573
|
+
int buffer_size;
|
|
1574
|
+
int occupied_count;
|
|
1575
|
+
long next_index;
|
|
1576
|
+
int deadline_hit;
|
|
1577
|
+
int stop;
|
|
1578
|
+
} lazy_execute_ctx_t;
|
|
1579
|
+
|
|
1580
|
+
static int lazy_pending_push(lazy_execute_ctx_t *ctx, int slot) {
|
|
1581
|
+
if (ctx->pending_count >= ctx->capacity)
|
|
1582
|
+
return 0;
|
|
1583
|
+
|
|
1584
|
+
ctx->pending_slots[ctx->pending_tail] = slot;
|
|
1585
|
+
ctx->pending_tail = (ctx->pending_tail + 1) % ctx->capacity;
|
|
1586
|
+
ctx->pending_count++;
|
|
1587
|
+
return 1;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
static int lazy_pending_pop(lazy_execute_ctx_t *ctx) {
|
|
1591
|
+
if (ctx->pending_count == 0)
|
|
1592
|
+
return -1;
|
|
1593
|
+
|
|
1594
|
+
int slot = ctx->pending_slots[ctx->pending_head];
|
|
1595
|
+
ctx->pending_head = (ctx->pending_head + 1) % ctx->capacity;
|
|
1596
|
+
ctx->pending_count--;
|
|
1597
|
+
return slot;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
static void lazy_release_slot(lazy_execute_ctx_t *ctx, int slot) {
|
|
1601
|
+
request_ctx_free(&ctx->session.requests[slot]);
|
|
1602
|
+
rb_ary_store(ctx->anchors, slot, Qnil);
|
|
1603
|
+
ctx->states[slot] = LAZY_SLOT_FREE;
|
|
1604
|
+
ctx->retry_at_ms[slot] = 0;
|
|
1605
|
+
ctx->free_slots[ctx->free_count++] = slot;
|
|
1606
|
+
if (ctx->occupied_count > 0)
|
|
1607
|
+
ctx->occupied_count--;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
static FAST_CURL_NORETURN void lazy_raise_setup_error(request_ctx_t *request) {
|
|
1611
|
+
VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
|
|
1612
|
+
rb_raise(klass, "%s (request %ld)",
|
|
1613
|
+
request->setup_error ? request->setup_error : "Invalid request configuration",
|
|
1614
|
+
request->index);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
static int lazy_finalize_error(lazy_execute_ctx_t *ctx, int slot, const char *message) {
|
|
1618
|
+
request_ctx_t *request = &ctx->session.requests[slot];
|
|
1619
|
+
VALUE response = build_error_response(message, ERR_INVALID_REQUEST, request->attempts);
|
|
1620
|
+
int stop = record_response(&ctx->cctx, request->index, response);
|
|
1621
|
+
lazy_release_slot(ctx, slot);
|
|
1622
|
+
return stop;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
static int lazy_activate_pending(lazy_execute_ctx_t *ctx) {
|
|
1626
|
+
while (ctx->session.active_count < ctx->session.max_connections && ctx->pending_count > 0) {
|
|
1627
|
+
int slot = lazy_pending_pop(ctx);
|
|
1628
|
+
request_ctx_t *request = &ctx->session.requests[slot];
|
|
1629
|
+
VALUE request_value = rb_ary_entry(ctx->anchors, slot);
|
|
1630
|
+
|
|
1631
|
+
if (!request_ctx_prepare_easy(request)) {
|
|
1632
|
+
if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle"))
|
|
1633
|
+
return 1;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (!setup_easy_handle(request, request_value, &ctx->opts)) {
|
|
1638
|
+
if (request->setup_error_fatal)
|
|
1639
|
+
lazy_raise_setup_error(request);
|
|
1640
|
+
if (lazy_finalize_error(ctx, slot,
|
|
1641
|
+
request->setup_error ? request->setup_error
|
|
1642
|
+
: "Invalid request configuration"))
|
|
1643
|
+
return 1;
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
|
|
1648
|
+
if (lazy_finalize_error(ctx, slot, "Failed to schedule the request"))
|
|
1649
|
+
return 1;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
request->attempts++;
|
|
1654
|
+
request->active = 1;
|
|
1655
|
+
request->done = 0;
|
|
1656
|
+
ctx->states[slot] = LAZY_SLOT_ACTIVE;
|
|
1657
|
+
ctx->session.active_count++;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return 0;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
static int lazy_activate_retries(lazy_execute_ctx_t *ctx) {
|
|
1664
|
+
long long now = fast_now_ms();
|
|
1665
|
+
|
|
1666
|
+
for (int slot = 0;
|
|
1667
|
+
slot < ctx->capacity && ctx->session.active_count < ctx->session.max_connections; slot++) {
|
|
1668
|
+
if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT || ctx->retry_at_ms[slot] > now)
|
|
1669
|
+
continue;
|
|
1670
|
+
|
|
1671
|
+
request_ctx_t *request = &ctx->session.requests[slot];
|
|
1672
|
+
VALUE request_value = rb_ary_entry(ctx->anchors, slot);
|
|
1673
|
+
|
|
1674
|
+
if (!request_ctx_reset_for_retry(request)) {
|
|
1675
|
+
if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle for the retry"))
|
|
1676
|
+
return 1;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (!setup_easy_handle(request, request_value, &ctx->opts)) {
|
|
1681
|
+
if (request->setup_error_fatal)
|
|
1682
|
+
lazy_raise_setup_error(request);
|
|
1683
|
+
if (lazy_finalize_error(ctx, slot,
|
|
1684
|
+
request->setup_error ? request->setup_error
|
|
1685
|
+
: "Invalid request configuration"))
|
|
1686
|
+
return 1;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
|
|
1691
|
+
if (lazy_finalize_error(ctx, slot, "Failed to schedule the retry"))
|
|
1692
|
+
return 1;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
request->attempts++;
|
|
1697
|
+
request->active = 1;
|
|
1698
|
+
request->done = 0;
|
|
1699
|
+
ctx->states[slot] = LAZY_SLOT_ACTIVE;
|
|
1700
|
+
ctx->retry_at_ms[slot] = 0;
|
|
1701
|
+
ctx->session.active_count++;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return 0;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
static long long lazy_next_retry_at(const lazy_execute_ctx_t *ctx) {
|
|
1708
|
+
long long next = 0;
|
|
1709
|
+
for (int slot = 0; slot < ctx->capacity; slot++) {
|
|
1710
|
+
if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT)
|
|
1711
|
+
continue;
|
|
1712
|
+
if (next == 0 || ctx->retry_at_ms[slot] < next)
|
|
1713
|
+
next = ctx->retry_at_ms[slot];
|
|
1714
|
+
}
|
|
1715
|
+
return next;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
static int lazy_process_completed(lazy_execute_ctx_t *ctx) {
|
|
1719
|
+
CURLMsg *msg;
|
|
1720
|
+
int msgs_left;
|
|
1721
|
+
|
|
1722
|
+
while ((msg = curl_multi_info_read(ctx->session.multi, &msgs_left))) {
|
|
1723
|
+
if (msg->msg != CURLMSG_DONE)
|
|
1724
|
+
continue;
|
|
1725
|
+
|
|
1726
|
+
request_ctx_t *request = NULL;
|
|
1727
|
+
curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&request);
|
|
1728
|
+
if (!request || request->done)
|
|
1729
|
+
continue;
|
|
1730
|
+
|
|
1731
|
+
int slot = (int)(request - ctx->session.requests);
|
|
1732
|
+
if (slot < 0 || slot >= ctx->capacity)
|
|
1733
|
+
continue;
|
|
1734
|
+
|
|
1735
|
+
if (request->active) {
|
|
1736
|
+
curl_multi_remove_handle(ctx->session.multi, request->easy);
|
|
1737
|
+
request->active = 0;
|
|
1738
|
+
if (ctx->session.active_count > 0)
|
|
1739
|
+
ctx->session.active_count--;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
request->done = 1;
|
|
1743
|
+
request->curl_result = msg->data.result;
|
|
1744
|
+
if (msg->data.result == CURLE_OK)
|
|
1745
|
+
curl_easy_getinfo(request->easy, CURLINFO_RESPONSE_CODE, &request->http_status);
|
|
1746
|
+
|
|
1747
|
+
if (request->attempts <= ctx->retry_cfg.max_retries &&
|
|
1748
|
+
should_retry(request, &ctx->retry_cfg)) {
|
|
1749
|
+
ctx->states[slot] = LAZY_SLOT_RETRY_WAIT;
|
|
1750
|
+
ctx->retry_at_ms[slot] =
|
|
1751
|
+
fast_now_ms() + retry_backoff_ms(&ctx->retry_cfg, request->attempts - 1);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
VALUE response =
|
|
1756
|
+
(msg->data.result == CURLE_OK)
|
|
1757
|
+
? build_response(request)
|
|
1758
|
+
: build_error_response_with_code(curl_easy_strerror(msg->data.result),
|
|
1759
|
+
(int)msg->data.result, request->attempts);
|
|
1760
|
+
int stop = record_response(&ctx->cctx, request->index, response);
|
|
1761
|
+
lazy_release_slot(ctx, slot);
|
|
1762
|
+
if (stop)
|
|
1763
|
+
return 1;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return 0;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
typedef struct {
|
|
1770
|
+
multi_session_t *session;
|
|
1771
|
+
long long wake_at_ms;
|
|
1772
|
+
} lazy_poll_args_t;
|
|
1773
|
+
|
|
1774
|
+
static void *lazy_poll_without_gvl(void *arg) {
|
|
1775
|
+
lazy_poll_args_t *poll = (lazy_poll_args_t *)arg;
|
|
1776
|
+
multi_session_t *session = poll->session;
|
|
1777
|
+
long long started = fast_now_ms();
|
|
1778
|
+
int before = session->still_running;
|
|
1779
|
+
|
|
1780
|
+
while (!session->cancelled) {
|
|
1781
|
+
int numfds = 0;
|
|
1782
|
+
curl_multi_poll(session->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
|
|
1783
|
+
curl_multi_perform(session->multi, &session->still_running);
|
|
1784
|
+
|
|
1785
|
+
if (session->still_running == 0 || session->still_running < before)
|
|
1786
|
+
break;
|
|
1787
|
+
if (poll->wake_at_ms > 0 && fast_now_ms() >= poll->wake_at_ms)
|
|
1788
|
+
break;
|
|
1789
|
+
if (fast_now_ms() - started >= POLL_SLICE_MS)
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
return NULL;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
static void lazy_wait(lazy_execute_ctx_t *ctx) {
|
|
1797
|
+
long long next_retry = lazy_next_retry_at(ctx);
|
|
1798
|
+
|
|
1799
|
+
if (ctx->session.active_count > 0) {
|
|
1800
|
+
lazy_poll_args_t poll = {.session = &ctx->session, .wake_at_ms = next_retry};
|
|
1801
|
+
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
1802
|
+
VALUE scheduler = current_fiber_scheduler();
|
|
1803
|
+
if (scheduler != Qnil)
|
|
1804
|
+
run_via_fiber_worker(scheduler, lazy_poll_without_gvl, &poll);
|
|
1805
|
+
else
|
|
1806
|
+
#endif
|
|
1807
|
+
rb_thread_call_without_gvl(lazy_poll_without_gvl, &poll, unblock_perform,
|
|
1808
|
+
&ctx->session);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (next_retry > 0) {
|
|
1813
|
+
long long delay = next_retry - fast_now_ms();
|
|
1814
|
+
if (delay > 0)
|
|
1815
|
+
retry_delay_sleep((long)delay);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
static int lazy_deadline_reached(lazy_execute_ctx_t *ctx) {
|
|
1820
|
+
if (ctx->opts.deadline_ms <= 0 || fast_now_ms() < ctx->opts.deadline_ms)
|
|
1821
|
+
return 0;
|
|
1822
|
+
|
|
1823
|
+
ctx->deadline_hit = 1;
|
|
1824
|
+
ctx->stop = 1;
|
|
1825
|
+
return 1;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
static void lazy_progress(lazy_execute_ctx_t *ctx) {
|
|
1829
|
+
if (ctx->stop || lazy_deadline_reached(ctx))
|
|
1830
|
+
return;
|
|
1831
|
+
|
|
1832
|
+
if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
|
|
1833
|
+
ctx->stop = 1;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
|
|
1838
|
+
if (lazy_process_completed(ctx)) {
|
|
1839
|
+
ctx->stop = 1;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (ctx->occupied_count == 0)
|
|
1844
|
+
return;
|
|
1845
|
+
|
|
1846
|
+
lazy_wait(ctx);
|
|
1847
|
+
if (lazy_process_completed(ctx))
|
|
1848
|
+
ctx->stop = 1;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
static VALUE lazy_each_request(VALUE yielded, VALUE arg, int argc, const VALUE *argv,
|
|
1852
|
+
VALUE blockarg) {
|
|
1853
|
+
(void)blockarg;
|
|
1854
|
+
lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
|
|
1855
|
+
VALUE request = argc == 1 ? argv[0] : yielded;
|
|
1856
|
+
|
|
1857
|
+
while (!ctx->stop && ctx->occupied_count >= ctx->capacity)
|
|
1858
|
+
lazy_progress(ctx);
|
|
1859
|
+
|
|
1860
|
+
if (ctx->stop || lazy_deadline_reached(ctx))
|
|
1861
|
+
rb_iter_break();
|
|
1862
|
+
|
|
1863
|
+
if (ctx->next_index > INT_MAX)
|
|
1864
|
+
rb_raise(rb_eArgError, "lazy request index exceeds supported range");
|
|
1865
|
+
|
|
1866
|
+
int slot = ctx->free_slots[--ctx->free_count];
|
|
1867
|
+
request_ctx_init(&ctx->session.requests[slot], ctx->next_index++);
|
|
1868
|
+
rb_ary_store(ctx->anchors, slot, request);
|
|
1869
|
+
ctx->states[slot] = LAZY_SLOT_PENDING;
|
|
1870
|
+
ctx->occupied_count++;
|
|
1871
|
+
lazy_pending_push(ctx, slot);
|
|
1872
|
+
|
|
1873
|
+
if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
|
|
1874
|
+
ctx->stop = 1;
|
|
1875
|
+
rb_iter_break();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
|
|
1879
|
+
if (lazy_process_completed(ctx)) {
|
|
1880
|
+
ctx->stop = 1;
|
|
1881
|
+
rb_iter_break();
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return Qnil;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
static void lazy_finalize_deadline(lazy_execute_ctx_t *ctx) {
|
|
1888
|
+
if (ctx->cctx.stream || ctx->cctx.target > 0)
|
|
1889
|
+
return;
|
|
1890
|
+
|
|
1891
|
+
for (int slot = 0; slot < ctx->capacity; slot++) {
|
|
1892
|
+
if (ctx->states[slot] == LAZY_SLOT_FREE)
|
|
1893
|
+
continue;
|
|
1894
|
+
|
|
1895
|
+
request_ctx_t *request = &ctx->session.requests[slot];
|
|
1896
|
+
if (request->active) {
|
|
1897
|
+
curl_multi_remove_handle(ctx->session.multi, request->easy);
|
|
1898
|
+
request->active = 0;
|
|
1899
|
+
if (ctx->session.active_count > 0)
|
|
1900
|
+
ctx->session.active_count--;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
VALUE response =
|
|
1904
|
+
build_error_response("Total timeout exceeded", ERR_DEADLINE, request->attempts);
|
|
1905
|
+
record_response(&ctx->cctx, request->index, response);
|
|
1906
|
+
lazy_release_slot(ctx, slot);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
static VALUE lazy_execute_body(VALUE arg) {
|
|
1911
|
+
lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
|
|
1912
|
+
|
|
1913
|
+
if (ctx->opts.total_timeout_ms > 0)
|
|
1914
|
+
ctx->opts.deadline_ms = fast_now_ms() + ctx->opts.total_timeout_ms;
|
|
1915
|
+
|
|
1916
|
+
rb_block_call(ctx->source, rb_intern("each"), 0, NULL, lazy_each_request, (VALUE)ctx);
|
|
1917
|
+
|
|
1918
|
+
while (!ctx->stop && ctx->occupied_count > 0)
|
|
1919
|
+
lazy_progress(ctx);
|
|
1920
|
+
|
|
1921
|
+
if (ctx->deadline_hit)
|
|
1922
|
+
lazy_finalize_deadline(ctx);
|
|
1923
|
+
|
|
1924
|
+
RB_GC_GUARD(ctx->source);
|
|
1925
|
+
RB_GC_GUARD(ctx->anchors);
|
|
1926
|
+
return ctx->cctx.stream ? Qnil : ctx->cctx.results;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
static VALUE cleanup_lazy_execute(VALUE arg) {
|
|
1930
|
+
lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
|
|
1931
|
+
|
|
1932
|
+
if (ctx->session.requests) {
|
|
1933
|
+
for (int slot = 0; slot < ctx->capacity; slot++) {
|
|
1934
|
+
request_ctx_t *request = &ctx->session.requests[slot];
|
|
1935
|
+
if (request->easy && request->active && ctx->session.multi)
|
|
1936
|
+
curl_multi_remove_handle(ctx->session.multi, request->easy);
|
|
1937
|
+
request_ctx_free(request);
|
|
1938
|
+
}
|
|
1939
|
+
free(ctx->session.requests);
|
|
1940
|
+
ctx->session.requests = NULL;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
free(ctx->states);
|
|
1944
|
+
ctx->states = NULL;
|
|
1945
|
+
free(ctx->free_slots);
|
|
1946
|
+
ctx->free_slots = NULL;
|
|
1947
|
+
free(ctx->pending_slots);
|
|
1948
|
+
ctx->pending_slots = NULL;
|
|
1949
|
+
free(ctx->retry_at_ms);
|
|
1950
|
+
ctx->retry_at_ms = NULL;
|
|
1951
|
+
|
|
1952
|
+
if (ctx->session.multi) {
|
|
1953
|
+
curl_multi_cleanup(ctx->session.multi);
|
|
1954
|
+
ctx->session.multi = NULL;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (ctx->retry_cfg.retry_http_codes) {
|
|
1958
|
+
free(ctx->retry_cfg.retry_http_codes);
|
|
1959
|
+
ctx->retry_cfg.retry_http_codes = NULL;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
return Qnil;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, int stream) {
|
|
1966
|
+
if (!rb_respond_to(requests, rb_intern("each")))
|
|
1967
|
+
rb_raise(rb_eArgError, "requests must be an Array or respond to #each");
|
|
1968
|
+
|
|
1969
|
+
lazy_execute_ctx_t ctx;
|
|
1970
|
+
memset(&ctx, 0, sizeof(ctx));
|
|
1971
|
+
|
|
1972
|
+
int max_conn;
|
|
1973
|
+
parse_options(options, &ctx.opts, &max_conn, &ctx.retry_cfg);
|
|
1974
|
+
ctx.buffer_size = NIL_P(options) ? max_conn
|
|
1975
|
+
: parse_int_option(options, KEY_BUFFER, "buffer", 1,
|
|
1976
|
+
MAX_BUFFER, max_conn, NULL);
|
|
1977
|
+
|
|
1978
|
+
if (stream || target > 0) {
|
|
1979
|
+
if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && stream)
|
|
1980
|
+
rb_warn(
|
|
1981
|
+
"FastCurl: retries are not supported in stream_execute, ignoring retries option");
|
|
1982
|
+
if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && target > 0)
|
|
1983
|
+
rb_warn(
|
|
1984
|
+
"FastCurl: retries are not supported in first_execute, ignoring retries option");
|
|
1985
|
+
ctx.retry_cfg.max_retries = 0;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
ctx.capacity = max_conn + ctx.buffer_size;
|
|
1989
|
+
ctx.source = requests;
|
|
1990
|
+
ctx.anchors = rb_ary_new2(ctx.capacity);
|
|
1991
|
+
ctx.next_index = 0;
|
|
1992
|
+
|
|
1993
|
+
ctx.cctx.results = stream ? Qnil : rb_ary_new();
|
|
1994
|
+
ctx.cctx.accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
|
|
1995
|
+
ctx.cctx.completed = 0;
|
|
1996
|
+
ctx.cctx.target = target;
|
|
1997
|
+
ctx.cctx.stream = stream;
|
|
1998
|
+
|
|
1999
|
+
if (!NIL_P(ctx.cctx.accept) && !rb_respond_to(ctx.cctx.accept, rb_intern("call")))
|
|
2000
|
+
rb_raise(rb_eArgError, "accept must respond to #call");
|
|
2001
|
+
|
|
2002
|
+
multi_session_init(&ctx.session, curl_multi_init(), ctx.capacity, max_conn,
|
|
2003
|
+
ctx.opts.timeout_sec);
|
|
2004
|
+
if (!ctx.session.multi)
|
|
2005
|
+
rb_raise(rb_eNoMemError, "failed to initialize curl multi handle");
|
|
2006
|
+
multi_session_configure(ctx.session.multi, max_conn);
|
|
2007
|
+
|
|
2008
|
+
ctx.session.requests = calloc((size_t)ctx.capacity, sizeof(request_ctx_t));
|
|
2009
|
+
ctx.states = calloc((size_t)ctx.capacity, sizeof(int));
|
|
2010
|
+
ctx.free_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
|
|
2011
|
+
ctx.pending_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
|
|
2012
|
+
ctx.retry_at_ms = calloc((size_t)ctx.capacity, sizeof(long long));
|
|
2013
|
+
|
|
2014
|
+
if (!ctx.session.requests || !ctx.states || !ctx.free_slots || !ctx.pending_slots ||
|
|
2015
|
+
!ctx.retry_at_ms) {
|
|
2016
|
+
cleanup_lazy_execute((VALUE)&ctx);
|
|
2017
|
+
rb_raise(rb_eNoMemError, "failed to allocate lazy request state");
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
for (int slot = 0; slot < ctx.capacity; slot++)
|
|
2021
|
+
ctx.free_slots[ctx.free_count++] = slot;
|
|
2022
|
+
|
|
2023
|
+
return rb_ensure(lazy_execute_body, (VALUE)&ctx, cleanup_lazy_execute, (VALUE)&ctx);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1525
2026
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
1526
2027
|
typedef struct {
|
|
1527
2028
|
execute_args_t *ea;
|
|
@@ -1597,6 +2098,9 @@ static VALUE execute_with_fiber_scheduler(VALUE arg) {
|
|
|
1597
2098
|
#endif
|
|
1598
2099
|
|
|
1599
2100
|
static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
|
|
2101
|
+
if (!RB_TYPE_P(requests, T_ARRAY))
|
|
2102
|
+
return internal_lazy_execute(requests, options, target, stream);
|
|
2103
|
+
|
|
1600
2104
|
Check_Type(requests, T_ARRAY);
|
|
1601
2105
|
|
|
1602
2106
|
long count_long = RARRAY_LEN(requests);
|
|
@@ -1617,6 +2121,12 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1617
2121
|
retry_config_t retry_cfg;
|
|
1618
2122
|
parse_options(options, &opts, &max_conn, &retry_cfg);
|
|
1619
2123
|
|
|
2124
|
+
if (target > 0 && !NIL_P(options)) {
|
|
2125
|
+
VALUE accept = hash_aref_key(options, KEY_ACCEPT);
|
|
2126
|
+
if (!NIL_P(accept) && !rb_respond_to(accept, rb_intern("call")))
|
|
2127
|
+
rb_raise(rb_eArgError, "accept must respond to #call");
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1620
2130
|
for (int i = 0; i < count; i++) {
|
|
1621
2131
|
request_ctx_t probe;
|
|
1622
2132
|
request_ctx_init(&probe, i);
|
|
@@ -1680,7 +2190,8 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1680
2190
|
|
|
1681
2191
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
1682
2192
|
VALUE scheduler = current_fiber_scheduler();
|
|
1683
|
-
|
|
2193
|
+
VALUE accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
|
|
2194
|
+
if (scheduler != Qnil && !stream && NIL_P(accept)) {
|
|
1684
2195
|
scheduler_execute_ctx_t scheduler_ctx = {
|
|
1685
2196
|
.ea = &ea,
|
|
1686
2197
|
.scheduler = scheduler,
|
data/lib/fast_curl/version.rb
CHANGED
data/lib/fast_curl.rb
CHANGED
|
@@ -88,7 +88,8 @@ module FastCurl
|
|
|
88
88
|
execute(build_requests(requests, method), **DEFAULT_OPTIONS.merge(options))
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
define_method(:"first_#{method}") do |requests, count: 1, **options|
|
|
91
|
+
define_method(:"first_#{method}") do |requests, count: 1, accept: nil, **options|
|
|
92
|
+
options[:accept] = accept unless accept.nil?
|
|
92
93
|
first_execute(build_requests(requests, method), count: count, **DEFAULT_OPTIONS.merge(options))
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -100,24 +101,38 @@ module FastCurl
|
|
|
100
101
|
private
|
|
101
102
|
|
|
102
103
|
def build_requests(requests, method)
|
|
103
|
-
requests.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
r[:body] = body
|
|
111
|
-
if content_type && !content_type?(headers)
|
|
112
|
-
headers ||= {}
|
|
113
|
-
headers["Content-Type"] = content_type
|
|
114
|
-
end
|
|
115
|
-
end
|
|
104
|
+
source = requests.is_a?(Hash) ? [requests] : requests
|
|
105
|
+
|
|
106
|
+
if source.is_a?(Array)
|
|
107
|
+
source.map { |request| build_request(request, method) }
|
|
108
|
+
elsif source.respond_to?(:each)
|
|
109
|
+
Enumerator.new do |yielder|
|
|
110
|
+
source.each { |request| yielder << build_request(request, method) }
|
|
116
111
|
end
|
|
112
|
+
else
|
|
113
|
+
raise ArgumentError, "requests must be a Hash, Array or respond to #each"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_request(req, method)
|
|
118
|
+
raise ArgumentError, "request must be a Hash" unless req.is_a?(Hash)
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
r = { url: build_url(req), method: method.to_s.upcase }
|
|
121
|
+
headers = req[:headers] ? req[:headers].dup : nil
|
|
122
|
+
|
|
123
|
+
if BODY_METHODS.include?(method)
|
|
124
|
+
body, content_type = build_body(req)
|
|
125
|
+
if body
|
|
126
|
+
r[:body] = body
|
|
127
|
+
if content_type && !content_type?(headers)
|
|
128
|
+
headers ||= {}
|
|
129
|
+
headers["Content-Type"] = content_type
|
|
130
|
+
end
|
|
131
|
+
end
|
|
120
132
|
end
|
|
133
|
+
|
|
134
|
+
r[:headers] = headers if headers && !headers.empty?
|
|
135
|
+
r
|
|
121
136
|
end
|
|
122
137
|
|
|
123
138
|
def build_url(req)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_curl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- roman-haidarov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: json
|
|
@@ -125,7 +124,6 @@ homepage: https://github.com/roman-haidarov/fast_curl
|
|
|
125
124
|
licenses:
|
|
126
125
|
- MIT
|
|
127
126
|
metadata: {}
|
|
128
|
-
post_install_message:
|
|
129
127
|
rdoc_options: []
|
|
130
128
|
require_paths:
|
|
131
129
|
- lib
|
|
@@ -140,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
138
|
- !ruby/object:Gem::Version
|
|
141
139
|
version: '0'
|
|
142
140
|
requirements: []
|
|
143
|
-
rubygems_version: 3.
|
|
144
|
-
signing_key:
|
|
141
|
+
rubygems_version: 3.6.7
|
|
145
142
|
specification_version: 4
|
|
146
143
|
summary: Ultra-fast parallel HTTP client as Ruby C extension on libcurl multi
|
|
147
144
|
test_files: []
|