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
data/ext/fast_curl/fast_curl.c
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
#define FAST_CURL_HAVE_FIBER_SCHEDULER 1
|
|
12
12
|
#endif
|
|
13
13
|
#include <curl/curl.h>
|
|
14
|
+
#include <ctype.h>
|
|
14
15
|
#include <limits.h>
|
|
16
|
+
#include <pthread.h>
|
|
17
|
+
#include <stdint.h>
|
|
15
18
|
#include <stdlib.h>
|
|
16
19
|
#include <string.h>
|
|
17
20
|
#include <strings.h>
|
|
@@ -23,20 +26,25 @@
|
|
|
23
26
|
#define FAST_CURL_NORETURN
|
|
24
27
|
#endif
|
|
25
28
|
|
|
26
|
-
#define MAX_RESPONSE_SIZE
|
|
27
|
-
#define MAX_REDIRECTS
|
|
28
|
-
#define MAX_TIMEOUT
|
|
29
|
-
#define MAX_RETRIES
|
|
30
|
-
#define MAX_REQUESTS
|
|
31
|
-
#define MAX_CONNECTIONS
|
|
32
|
-
#define
|
|
33
|
-
#define
|
|
34
|
-
#define
|
|
35
|
-
#define
|
|
36
|
-
#define
|
|
37
|
-
#define
|
|
38
|
-
#define
|
|
39
|
-
#define
|
|
29
|
+
#define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
|
|
30
|
+
#define MAX_REDIRECTS 5
|
|
31
|
+
#define MAX_TIMEOUT 300
|
|
32
|
+
#define MAX_RETRIES 10
|
|
33
|
+
#define MAX_REQUESTS 10000
|
|
34
|
+
#define MAX_CONNECTIONS 100
|
|
35
|
+
#define MAX_BUFFER 10000
|
|
36
|
+
#define MAX_RETRY_DELAY_MS 30000
|
|
37
|
+
#define DEFAULT_RETRIES 1
|
|
38
|
+
#define DEFAULT_RETRY_DELAY 100
|
|
39
|
+
#define DEFAULT_CONNECT_TIMEOUT_MS 10000L
|
|
40
|
+
#define MAX_CONNECT_TIMEOUT_MS 300000L
|
|
41
|
+
#define MAX_TOTAL_TIMEOUT_MS 3600000L
|
|
42
|
+
#define INITIAL_BUF_CAP 8192
|
|
43
|
+
#define INITIAL_HEADER_CAP 16
|
|
44
|
+
#define POLL_TIMEOUT_MS 50
|
|
45
|
+
#define POLL_SLICE_MS 2000
|
|
46
|
+
#define FIBER_POLL_TIMEOUT_MS 10
|
|
47
|
+
#define HEADER_LINE_BUF_SIZE 512
|
|
40
48
|
|
|
41
49
|
static const CURLcode DEFAULT_RETRYABLE_CURLE[] = {
|
|
42
50
|
CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEDOUT, CURLE_SEND_ERROR, CURLE_RECV_ERROR,
|
|
@@ -57,6 +65,16 @@ typedef enum {
|
|
|
57
65
|
KEY_RETRIES,
|
|
58
66
|
KEY_RETRY_DELAY,
|
|
59
67
|
KEY_RETRY_CODES,
|
|
68
|
+
KEY_CONNECT_TIMEOUT,
|
|
69
|
+
KEY_TOTAL_TIMEOUT,
|
|
70
|
+
KEY_RETRY_NON_IDEMPOTENT,
|
|
71
|
+
KEY_FOLLOW_REDIRECTS,
|
|
72
|
+
KEY_MAX_REDIRECTS,
|
|
73
|
+
KEY_EFFECTIVE_URL,
|
|
74
|
+
KEY_ERROR,
|
|
75
|
+
KEY_ATTEMPTS,
|
|
76
|
+
KEY_BUFFER,
|
|
77
|
+
KEY_ACCEPT,
|
|
60
78
|
KEY_LAST
|
|
61
79
|
} key_id_t;
|
|
62
80
|
|
|
@@ -66,10 +84,113 @@ static VALUE fast_syms[KEY_LAST];
|
|
|
66
84
|
#define SYM(key) fast_syms[key]
|
|
67
85
|
|
|
68
86
|
static const char *const KEY_NAMES[KEY_LAST] = {
|
|
69
|
-
"status",
|
|
70
|
-
"
|
|
87
|
+
"status",
|
|
88
|
+
"headers",
|
|
89
|
+
"body",
|
|
90
|
+
"error_code",
|
|
91
|
+
"url",
|
|
92
|
+
"method",
|
|
93
|
+
"timeout",
|
|
94
|
+
"connections",
|
|
95
|
+
"count",
|
|
96
|
+
"retries",
|
|
97
|
+
"retry_delay",
|
|
98
|
+
"retry_codes",
|
|
99
|
+
"connect_timeout",
|
|
100
|
+
"total_timeout",
|
|
101
|
+
"retry_non_idempotent",
|
|
102
|
+
"follow_redirects",
|
|
103
|
+
"max_redirects",
|
|
104
|
+
"effective_url",
|
|
105
|
+
"error",
|
|
106
|
+
"attempts",
|
|
107
|
+
"buffer",
|
|
108
|
+
"accept",
|
|
71
109
|
};
|
|
72
110
|
|
|
111
|
+
typedef enum {
|
|
112
|
+
ERR_CURL,
|
|
113
|
+
ERR_INVALID_REQUEST,
|
|
114
|
+
ERR_NOT_COMPLETED,
|
|
115
|
+
ERR_DEADLINE,
|
|
116
|
+
ERR_LAST
|
|
117
|
+
} error_kind_t;
|
|
118
|
+
|
|
119
|
+
static VALUE error_syms[ERR_LAST];
|
|
120
|
+
|
|
121
|
+
static const char *const ERROR_KIND_NAMES[ERR_LAST] = {
|
|
122
|
+
"curl_error",
|
|
123
|
+
"invalid_request",
|
|
124
|
+
"not_completed",
|
|
125
|
+
"deadline_exceeded",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
static long long fast_now_ms(void) {
|
|
129
|
+
struct timespec ts;
|
|
130
|
+
#ifdef CLOCK_MONOTONIC
|
|
131
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
132
|
+
#else
|
|
133
|
+
clock_gettime(CLOCK_REALTIME, &ts);
|
|
134
|
+
#endif
|
|
135
|
+
return (long long)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static uint64_t fast_rand_state;
|
|
139
|
+
|
|
140
|
+
/* Small xorshift PRNG; only used to jitter retry delays. */
|
|
141
|
+
static uint32_t fast_rand(void) {
|
|
142
|
+
uint64_t x = fast_rand_state;
|
|
143
|
+
if (x == 0)
|
|
144
|
+
x = (uint64_t)fast_now_ms() | 1ULL;
|
|
145
|
+
x ^= x << 13;
|
|
146
|
+
x ^= x >> 7;
|
|
147
|
+
x ^= x << 17;
|
|
148
|
+
fast_rand_state = x;
|
|
149
|
+
return (uint32_t)(x >> 32);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Process-wide DNS and TLS session cache, so repeated calls to the same host
|
|
153
|
+
skip resolution and can resume TLS instead of doing a full handshake. */
|
|
154
|
+
static CURLSH *fast_share = NULL;
|
|
155
|
+
static pthread_mutex_t fast_share_locks[CURL_LOCK_DATA_LAST];
|
|
156
|
+
|
|
157
|
+
static void fast_share_lock(CURL *handle, curl_lock_data data, curl_lock_access access,
|
|
158
|
+
void *userptr) {
|
|
159
|
+
(void)handle;
|
|
160
|
+
(void)access;
|
|
161
|
+
(void)userptr;
|
|
162
|
+
if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
|
|
163
|
+
pthread_mutex_lock(&fast_share_locks[data]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static void fast_share_unlock(CURL *handle, curl_lock_data data, void *userptr) {
|
|
167
|
+
(void)handle;
|
|
168
|
+
(void)userptr;
|
|
169
|
+
if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
|
|
170
|
+
pthread_mutex_unlock(&fast_share_locks[data]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static void fast_share_init(void) {
|
|
174
|
+
for (int i = 0; i < CURL_LOCK_DATA_LAST; i++)
|
|
175
|
+
pthread_mutex_init(&fast_share_locks[i], NULL);
|
|
176
|
+
|
|
177
|
+
fast_share = curl_share_init();
|
|
178
|
+
if (!fast_share)
|
|
179
|
+
return;
|
|
180
|
+
|
|
181
|
+
curl_share_setopt(fast_share, CURLSHOPT_LOCKFUNC, fast_share_lock);
|
|
182
|
+
curl_share_setopt(fast_share, CURLSHOPT_UNLOCKFUNC, fast_share_unlock);
|
|
183
|
+
curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
|
|
184
|
+
curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
|
|
185
|
+
|
|
186
|
+
/* CURL_LOCK_DATA_CONNECT is deliberately NOT shared. Sharing the connection
|
|
187
|
+
cache between multi handles running concurrently in different threads
|
|
188
|
+
deadlocks against CURLMOPT_MAX_TOTAL_CONNECTIONS, and removing that limit
|
|
189
|
+
turns the deadlock into a segfault inside curl_multi_perform (reproduced
|
|
190
|
+
on libcurl 8.5.0 with two threads x 25 requests). Cross-call TCP reuse
|
|
191
|
+
needs a persistent multi handle per thread, not a shared cache. */
|
|
192
|
+
}
|
|
193
|
+
|
|
73
194
|
typedef struct {
|
|
74
195
|
char *data;
|
|
75
196
|
size_t len;
|
|
@@ -204,7 +325,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
|
|
|
204
325
|
|
|
205
326
|
typedef struct {
|
|
206
327
|
CURL *easy;
|
|
207
|
-
|
|
328
|
+
long index;
|
|
208
329
|
buffer_t body;
|
|
209
330
|
header_list_t headers;
|
|
210
331
|
struct curl_slist *req_headers;
|
|
@@ -212,9 +333,20 @@ typedef struct {
|
|
|
212
333
|
int active;
|
|
213
334
|
CURLcode curl_result;
|
|
214
335
|
long http_status;
|
|
336
|
+
int idempotent;
|
|
337
|
+
int attempts;
|
|
338
|
+
const char *setup_error;
|
|
339
|
+
int setup_error_fatal;
|
|
215
340
|
} request_ctx_t;
|
|
216
341
|
|
|
217
|
-
static
|
|
342
|
+
static VALUE fast_validation_error = Qnil;
|
|
343
|
+
#define SET_SETUP_ERROR(ctx, msg, fatal) \
|
|
344
|
+
do { \
|
|
345
|
+
(ctx)->setup_error = (msg); \
|
|
346
|
+
(ctx)->setup_error_fatal = (fatal); \
|
|
347
|
+
} while (0)
|
|
348
|
+
|
|
349
|
+
static inline void request_ctx_init(request_ctx_t *ctx, long index) {
|
|
218
350
|
ctx->easy = NULL;
|
|
219
351
|
ctx->index = index;
|
|
220
352
|
buffer_init(&ctx->body);
|
|
@@ -224,6 +356,10 @@ static inline void request_ctx_init(request_ctx_t *ctx, int index) {
|
|
|
224
356
|
ctx->active = 0;
|
|
225
357
|
ctx->curl_result = CURLE_OK;
|
|
226
358
|
ctx->http_status = 0;
|
|
359
|
+
ctx->idempotent = 1;
|
|
360
|
+
ctx->attempts = 0;
|
|
361
|
+
ctx->setup_error = NULL;
|
|
362
|
+
ctx->setup_error_fatal = 0;
|
|
227
363
|
}
|
|
228
364
|
|
|
229
365
|
static void request_ctx_free(request_ctx_t *ctx) {
|
|
@@ -267,6 +403,8 @@ static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
|
|
|
267
403
|
ctx->active = 0;
|
|
268
404
|
ctx->curl_result = CURLE_OK;
|
|
269
405
|
ctx->http_status = 0;
|
|
406
|
+
ctx->setup_error = NULL;
|
|
407
|
+
ctx->setup_error_fatal = 0;
|
|
270
408
|
return 1;
|
|
271
409
|
}
|
|
272
410
|
|
|
@@ -291,8 +429,18 @@ typedef struct {
|
|
|
291
429
|
long retry_delay_ms;
|
|
292
430
|
int *retry_http_codes;
|
|
293
431
|
int retry_http_count;
|
|
432
|
+
int retry_non_idempotent;
|
|
294
433
|
} retry_config_t;
|
|
295
434
|
|
|
435
|
+
typedef struct {
|
|
436
|
+
long timeout_sec;
|
|
437
|
+
long connect_timeout_ms;
|
|
438
|
+
long total_timeout_ms;
|
|
439
|
+
long follow_redirects;
|
|
440
|
+
long max_redirects;
|
|
441
|
+
long long deadline_ms;
|
|
442
|
+
} request_options_t;
|
|
443
|
+
|
|
296
444
|
static int contains_header_injection(const char *str, long len) {
|
|
297
445
|
for (long i = 0; i < len; i++) {
|
|
298
446
|
if (str[i] == '\r' || str[i] == '\n' || str[i] == '\0')
|
|
@@ -392,24 +540,37 @@ static void run_via_fiber_worker(VALUE scheduler, void *(*func)(void *), void *a
|
|
|
392
540
|
}
|
|
393
541
|
#endif
|
|
394
542
|
|
|
395
|
-
static
|
|
543
|
+
static VALUE fast_cHeaders = Qnil;
|
|
544
|
+
static VALUE new_headers_hash(void) {
|
|
545
|
+
if (!NIL_P(fast_cHeaders))
|
|
546
|
+
return rb_obj_alloc(fast_cHeaders);
|
|
547
|
+
return rb_hash_new();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
static void headers_hash_store(VALUE headers_hash, VALUE key, VALUE val, int always_array) {
|
|
396
551
|
VALUE existing = rb_hash_aref(headers_hash, key);
|
|
397
552
|
|
|
398
553
|
if (NIL_P(existing)) {
|
|
399
|
-
rb_hash_aset(headers_hash, key, val);
|
|
400
|
-
|
|
554
|
+
rb_hash_aset(headers_hash, key, always_array ? rb_ary_new_from_args(1, val) : val);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (RB_TYPE_P(existing, T_ARRAY)) {
|
|
401
559
|
rb_ary_push(existing, val);
|
|
402
|
-
|
|
403
|
-
VALUE values = rb_ary_new_from_args(2, existing, val);
|
|
404
|
-
rb_hash_aset(headers_hash, key, values);
|
|
560
|
+
return;
|
|
405
561
|
}
|
|
562
|
+
|
|
563
|
+
VALUE joined = rb_str_dup(existing);
|
|
564
|
+
rb_str_cat_cstr(joined, ", ");
|
|
565
|
+
rb_str_append(joined, val);
|
|
566
|
+
rb_hash_aset(headers_hash, key, joined);
|
|
406
567
|
}
|
|
407
568
|
|
|
408
569
|
static VALUE build_response(request_ctx_t *ctx) {
|
|
409
570
|
long status = 0;
|
|
410
571
|
curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
|
|
411
572
|
|
|
412
|
-
VALUE headers_hash =
|
|
573
|
+
VALUE headers_hash = new_headers_hash();
|
|
413
574
|
for (int i = 0; i < ctx->headers.count; i++) {
|
|
414
575
|
const char *hdr = ctx->headers.entries[i].str;
|
|
415
576
|
size_t hdr_len = ctx->headers.entries[i].len;
|
|
@@ -418,6 +579,12 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
418
579
|
continue;
|
|
419
580
|
|
|
420
581
|
VALUE key = rb_str_new(hdr, colon - hdr);
|
|
582
|
+
char *kp = RSTRING_PTR(key);
|
|
583
|
+
long klen = RSTRING_LEN(key);
|
|
584
|
+
for (long k = 0; k < klen; k++)
|
|
585
|
+
kp[k] = (char)tolower((unsigned char)kp[k]);
|
|
586
|
+
int always_array = (klen == 10 && memcmp(kp, "set-cookie", 10) == 0);
|
|
587
|
+
|
|
421
588
|
const char *vs = colon + 1;
|
|
422
589
|
const char *ve = hdr + hdr_len;
|
|
423
590
|
|
|
@@ -427,7 +594,7 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
427
594
|
ve--;
|
|
428
595
|
|
|
429
596
|
VALUE val = rb_str_new(vs, ve - vs);
|
|
430
|
-
headers_hash_store(headers_hash, key, val);
|
|
597
|
+
headers_hash_store(headers_hash, key, val, always_array);
|
|
431
598
|
}
|
|
432
599
|
|
|
433
600
|
VALUE body_str =
|
|
@@ -437,19 +604,33 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
437
604
|
rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
|
|
438
605
|
rb_hash_aset(result, SYM(KEY_HEADERS), headers_hash);
|
|
439
606
|
rb_hash_aset(result, SYM(KEY_BODY), body_str);
|
|
607
|
+
rb_hash_aset(result, SYM(KEY_ERROR), Qnil);
|
|
608
|
+
rb_hash_aset(result, SYM(KEY_ERROR_CODE), Qnil);
|
|
609
|
+
rb_hash_aset(result, SYM(KEY_ATTEMPTS), INT2NUM(ctx->attempts));
|
|
610
|
+
|
|
611
|
+
char *effective = NULL;
|
|
612
|
+
if (curl_easy_getinfo(ctx->easy, CURLINFO_EFFECTIVE_URL, &effective) == CURLE_OK && effective)
|
|
613
|
+
rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), rb_str_new_cstr(effective));
|
|
614
|
+
else
|
|
615
|
+
rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), Qnil);
|
|
616
|
+
|
|
440
617
|
return result;
|
|
441
618
|
}
|
|
442
619
|
|
|
443
|
-
static VALUE build_error_response(const char *message) {
|
|
620
|
+
static VALUE build_error_response(const char *message, error_kind_t kind, int attempts) {
|
|
444
621
|
VALUE r = rb_hash_new();
|
|
445
622
|
rb_hash_aset(r, SYM(KEY_STATUS), INT2NUM(0));
|
|
446
623
|
rb_hash_aset(r, SYM(KEY_HEADERS), Qnil);
|
|
447
624
|
rb_hash_aset(r, SYM(KEY_BODY), rb_str_new_cstr(message));
|
|
625
|
+
rb_hash_aset(r, SYM(KEY_ERROR), error_syms[kind]);
|
|
626
|
+
rb_hash_aset(r, SYM(KEY_ERROR_CODE), Qnil);
|
|
627
|
+
rb_hash_aset(r, SYM(KEY_EFFECTIVE_URL), Qnil);
|
|
628
|
+
rb_hash_aset(r, SYM(KEY_ATTEMPTS), INT2NUM(attempts));
|
|
448
629
|
return r;
|
|
449
630
|
}
|
|
450
631
|
|
|
451
|
-
static VALUE build_error_response_with_code(const char *message, int error_code) {
|
|
452
|
-
VALUE r = build_error_response(message);
|
|
632
|
+
static VALUE build_error_response_with_code(const char *message, int error_code, int attempts) {
|
|
633
|
+
VALUE r = build_error_response(message, ERR_CURL, attempts);
|
|
453
634
|
rb_hash_aset(r, SYM(KEY_ERROR_CODE), INT2NUM(error_code));
|
|
454
635
|
return r;
|
|
455
636
|
}
|
|
@@ -474,20 +655,33 @@ static int is_valid_url(const char *url) {
|
|
|
474
655
|
return _r; \
|
|
475
656
|
} while (0)
|
|
476
657
|
|
|
477
|
-
static CURLcode setup_basic_options(CURL *easy, const char *url_str,
|
|
658
|
+
static CURLcode setup_basic_options(CURL *easy, const char *url_str, const request_options_t *opts,
|
|
478
659
|
request_ctx_t *ctx) {
|
|
660
|
+
long timeout_ms = opts->timeout_sec * 1000L;
|
|
661
|
+
|
|
662
|
+
if (opts->deadline_ms > 0) {
|
|
663
|
+
long long remaining = opts->deadline_ms - fast_now_ms();
|
|
664
|
+
if (remaining < 1)
|
|
665
|
+
remaining = 1;
|
|
666
|
+
if (remaining < (long long)timeout_ms)
|
|
667
|
+
timeout_ms = (long)remaining;
|
|
668
|
+
}
|
|
669
|
+
|
|
479
670
|
CURL_SETOPT_CHECK(easy, CURLOPT_URL, url_str);
|
|
480
671
|
CURL_SETOPT_CHECK(easy, CURLOPT_WRITEFUNCTION, write_callback);
|
|
481
672
|
CURL_SETOPT_CHECK(easy, CURLOPT_WRITEDATA, &ctx->body);
|
|
482
673
|
CURL_SETOPT_CHECK(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
|
483
674
|
CURL_SETOPT_CHECK(easy, CURLOPT_HEADERDATA, &ctx->headers);
|
|
484
|
-
CURL_SETOPT_CHECK(easy,
|
|
675
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
|
|
676
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_CONNECTTIMEOUT_MS, opts->connect_timeout_ms);
|
|
485
677
|
CURL_SETOPT_CHECK(easy, CURLOPT_NOSIGNAL, 1L);
|
|
486
|
-
CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION,
|
|
487
|
-
CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS,
|
|
678
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION, opts->follow_redirects);
|
|
679
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS, opts->max_redirects);
|
|
488
680
|
CURL_SETOPT_CHECK(easy, CURLOPT_ACCEPT_ENCODING, "");
|
|
489
681
|
CURL_SETOPT_CHECK(easy, CURLOPT_PRIVATE, (char *)ctx);
|
|
490
682
|
CURL_SETOPT_CHECK(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
|
|
683
|
+
if (fast_share)
|
|
684
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_SHARE, fast_share);
|
|
491
685
|
return CURLE_OK;
|
|
492
686
|
}
|
|
493
687
|
|
|
@@ -518,12 +712,13 @@ typedef struct {
|
|
|
518
712
|
int post;
|
|
519
713
|
int nobody;
|
|
520
714
|
int allows_body;
|
|
715
|
+
int idempotent;
|
|
521
716
|
} http_method_t;
|
|
522
717
|
|
|
523
718
|
static const http_method_t HTTP_METHODS[] = {
|
|
524
|
-
{"GET", NULL, 0, 0, 0}, {"POST", NULL, 1, 0, 1}, {"PUT", "PUT", 0, 0, 1},
|
|
525
|
-
{"DELETE", "DELETE", 0, 0, 1}, {"PATCH", "PATCH", 0, 0, 1}, {"HEAD", NULL, 0, 1, 0},
|
|
526
|
-
{"OPTIONS", "OPTIONS", 0, 0, 0},
|
|
719
|
+
{"GET", NULL, 0, 0, 0, 1}, {"POST", NULL, 1, 0, 1, 0}, {"PUT", "PUT", 0, 0, 1, 1},
|
|
720
|
+
{"DELETE", "DELETE", 0, 0, 1, 1}, {"PATCH", "PATCH", 0, 0, 1, 0}, {"HEAD", NULL, 0, 1, 0, 1},
|
|
721
|
+
{"OPTIONS", "OPTIONS", 0, 0, 0, 1},
|
|
527
722
|
};
|
|
528
723
|
|
|
529
724
|
static const http_method_t *find_http_method(const char *name) {
|
|
@@ -544,29 +739,52 @@ static CURLcode apply_http_method(CURL *easy, const http_method_t *method) {
|
|
|
544
739
|
return CURLE_OK;
|
|
545
740
|
}
|
|
546
741
|
|
|
547
|
-
static
|
|
548
|
-
const
|
|
549
|
-
const http_method_t *method = find_http_method(name);
|
|
742
|
+
static int setup_method_and_body(request_ctx_t *ctx, VALUE method_value, VALUE body) {
|
|
743
|
+
const http_method_t *method;
|
|
550
744
|
int has_body = !NIL_P(body);
|
|
745
|
+
char name[32];
|
|
551
746
|
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
747
|
+
if (NIL_P(method_value)) {
|
|
748
|
+
memcpy(name, "GET", 4);
|
|
749
|
+
} else {
|
|
750
|
+
if (!RB_TYPE_P(method_value, T_STRING)) {
|
|
751
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
long len = RSTRING_LEN(method_value);
|
|
755
|
+
if (len <= 0 || len >= (long)sizeof(name)) {
|
|
756
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
memcpy(name, RSTRING_PTR(method_value), (size_t)len);
|
|
760
|
+
name[len] = '\0';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
method = find_http_method(name);
|
|
764
|
+
if (!method) {
|
|
765
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
766
|
+
return 0;
|
|
767
|
+
}
|
|
768
|
+
if (has_body && !method->allows_body) {
|
|
769
|
+
SET_SETUP_ERROR(ctx, "This HTTP method must not include a body", 1);
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
ctx->idempotent = method->idempotent;
|
|
556
774
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return
|
|
775
|
+
if (apply_http_method(ctx->easy, method) != CURLE_OK) {
|
|
776
|
+
SET_SETUP_ERROR(ctx, "Failed to configure HTTP method", 0);
|
|
777
|
+
return 0;
|
|
778
|
+
}
|
|
560
779
|
|
|
561
|
-
if (has_body) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return res;
|
|
780
|
+
if (has_body && set_body(ctx->easy, body) != CURLE_OK) {
|
|
781
|
+
SET_SETUP_ERROR(ctx, "Failed to configure request body", 0);
|
|
782
|
+
return 0;
|
|
565
783
|
}
|
|
566
784
|
|
|
567
785
|
RB_GC_GUARD(method_value);
|
|
568
786
|
RB_GC_GUARD(body);
|
|
569
|
-
return
|
|
787
|
+
return 1;
|
|
570
788
|
}
|
|
571
789
|
|
|
572
790
|
static void append_request_header(request_ctx_t *ctx, const char *buf) {
|
|
@@ -608,23 +826,42 @@ static void append_formatted_header(request_ctx_t *ctx, const char *key, long ke
|
|
|
608
826
|
free(line);
|
|
609
827
|
}
|
|
610
828
|
|
|
829
|
+
static VALUE header_part_to_str(VALUE v) {
|
|
830
|
+
if (RB_TYPE_P(v, T_STRING))
|
|
831
|
+
return v;
|
|
832
|
+
if (SYMBOL_P(v))
|
|
833
|
+
return rb_sym2str(v);
|
|
834
|
+
return rb_String(v);
|
|
835
|
+
}
|
|
836
|
+
|
|
611
837
|
static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
|
|
612
838
|
request_ctx_t *ctx = (request_ctx_t *)arg;
|
|
613
|
-
VALUE key_str
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
long klen = RSTRING_LEN(key_str);
|
|
839
|
+
VALUE key_str, val_str;
|
|
840
|
+
const char *k;
|
|
841
|
+
long klen;
|
|
617
842
|
const char *v = NULL;
|
|
618
843
|
long vlen = 0;
|
|
619
844
|
|
|
620
|
-
if (
|
|
621
|
-
|
|
845
|
+
if (ctx->setup_error)
|
|
846
|
+
return ST_STOP;
|
|
847
|
+
|
|
848
|
+
key_str = header_part_to_str(key);
|
|
849
|
+
val_str = NIL_P(val) ? Qnil : header_part_to_str(val);
|
|
850
|
+
k = RSTRING_PTR(key_str);
|
|
851
|
+
klen = RSTRING_LEN(key_str);
|
|
852
|
+
|
|
853
|
+
if (!is_valid_header_name(k, klen) || contains_header_injection(k, klen)) {
|
|
854
|
+
SET_SETUP_ERROR(ctx, "Invalid HTTP header name", 1);
|
|
855
|
+
return ST_STOP;
|
|
856
|
+
}
|
|
622
857
|
|
|
623
858
|
if (!NIL_P(val_str) && RSTRING_LEN(val_str) > 0) {
|
|
624
859
|
v = RSTRING_PTR(val_str);
|
|
625
860
|
vlen = RSTRING_LEN(val_str);
|
|
626
|
-
if (contains_header_injection(v, vlen))
|
|
627
|
-
|
|
861
|
+
if (contains_header_injection(v, vlen)) {
|
|
862
|
+
SET_SETUP_ERROR(ctx, "Invalid HTTP header value", 1);
|
|
863
|
+
return ST_STOP;
|
|
864
|
+
}
|
|
628
865
|
}
|
|
629
866
|
|
|
630
867
|
append_formatted_header(ctx, k, klen, v, vlen);
|
|
@@ -634,40 +871,58 @@ static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
|
|
|
634
871
|
return ST_CONTINUE;
|
|
635
872
|
}
|
|
636
873
|
|
|
637
|
-
static int setup_easy_handle(request_ctx_t *ctx, VALUE request,
|
|
638
|
-
|
|
874
|
+
static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_options_t *opts) {
|
|
875
|
+
if (!RB_TYPE_P(request, T_HASH)) {
|
|
876
|
+
SET_SETUP_ERROR(ctx, "Request must be a Hash", 1);
|
|
877
|
+
return 0;
|
|
878
|
+
}
|
|
639
879
|
|
|
640
880
|
VALUE url = hash_aref_key(request, KEY_URL);
|
|
641
881
|
VALUE method = hash_aref_key(request, KEY_METHOD);
|
|
642
882
|
VALUE headers = hash_aref_key(request, KEY_HEADERS);
|
|
643
883
|
VALUE body = hash_aref_key(request, KEY_BODY);
|
|
644
884
|
|
|
645
|
-
if (NIL_P(url))
|
|
885
|
+
if (NIL_P(url)) {
|
|
886
|
+
SET_SETUP_ERROR(ctx, "Missing :url", 0);
|
|
887
|
+
return 0;
|
|
888
|
+
}
|
|
889
|
+
if (!RB_TYPE_P(url, T_STRING) || memchr(RSTRING_PTR(url), '\0', RSTRING_LEN(url))) {
|
|
890
|
+
SET_SETUP_ERROR(ctx, "Invalid URL", 1);
|
|
646
891
|
return 0;
|
|
892
|
+
}
|
|
647
893
|
|
|
648
894
|
const char *url_str = StringValueCStr(url);
|
|
649
|
-
if (!is_valid_url(url_str))
|
|
650
|
-
|
|
895
|
+
if (!is_valid_url(url_str)) {
|
|
896
|
+
SET_SETUP_ERROR(ctx, "Invalid URL (expected http:// or https://, max 2048 bytes)", 1);
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
651
899
|
|
|
652
|
-
|
|
653
|
-
|
|
900
|
+
if (setup_basic_options(ctx->easy, url_str, opts, ctx) != CURLE_OK) {
|
|
901
|
+
SET_SETUP_ERROR(ctx, "Failed to configure request", 0);
|
|
654
902
|
return 0;
|
|
903
|
+
}
|
|
655
904
|
|
|
656
|
-
|
|
657
|
-
|
|
905
|
+
if (setup_security_options(ctx->easy) != CURLE_OK) {
|
|
906
|
+
SET_SETUP_ERROR(ctx, "Failed to configure TLS options", 0);
|
|
658
907
|
return 0;
|
|
908
|
+
}
|
|
659
909
|
|
|
660
|
-
|
|
661
|
-
if (res != CURLE_OK)
|
|
910
|
+
if (!setup_method_and_body(ctx, method, body))
|
|
662
911
|
return 0;
|
|
663
912
|
|
|
664
913
|
if (!NIL_P(headers)) {
|
|
665
|
-
|
|
914
|
+
if (!RB_TYPE_P(headers, T_HASH)) {
|
|
915
|
+
SET_SETUP_ERROR(ctx, ":headers must be a Hash", 1);
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
666
918
|
rb_hash_foreach(headers, header_iter_cb, (VALUE)ctx);
|
|
919
|
+
if (ctx->setup_error)
|
|
920
|
+
return 0;
|
|
667
921
|
if (ctx->req_headers) {
|
|
668
|
-
|
|
669
|
-
|
|
922
|
+
if (curl_easy_setopt(ctx->easy, CURLOPT_HTTPHEADER, ctx->req_headers) != CURLE_OK) {
|
|
923
|
+
SET_SETUP_ERROR(ctx, "Failed to set request headers", 0);
|
|
670
924
|
return 0;
|
|
925
|
+
}
|
|
671
926
|
}
|
|
672
927
|
}
|
|
673
928
|
|
|
@@ -679,14 +934,25 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, long timeout_sec
|
|
|
679
934
|
return 1;
|
|
680
935
|
}
|
|
681
936
|
|
|
937
|
+
/* Poll until something actually completes, or the slice budget expires.
|
|
938
|
+
Previously this returned after every single 50ms poll, which under a fiber
|
|
939
|
+
scheduler meant a fresh OS thread twenty times a second. Cancellation still
|
|
940
|
+
breaks out immediately via curl_multi_wakeup. */
|
|
682
941
|
static void *poll_without_gvl(void *arg) {
|
|
683
942
|
multi_session_t *s = (multi_session_t *)arg;
|
|
684
|
-
|
|
685
|
-
|
|
943
|
+
long long started = fast_now_ms();
|
|
944
|
+
int before = s->still_running;
|
|
945
|
+
|
|
946
|
+
while (!s->cancelled) {
|
|
947
|
+
int numfds = 0;
|
|
948
|
+
curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
|
|
949
|
+
curl_multi_perform(s->multi, &s->still_running);
|
|
686
950
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
951
|
+
if (s->still_running == 0 || s->still_running < before)
|
|
952
|
+
break;
|
|
953
|
+
if (fast_now_ms() - started >= POLL_SLICE_MS)
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
690
956
|
return NULL;
|
|
691
957
|
}
|
|
692
958
|
|
|
@@ -700,28 +966,52 @@ static void unblock_perform(void *arg) {
|
|
|
700
966
|
|
|
701
967
|
typedef struct {
|
|
702
968
|
VALUE results;
|
|
969
|
+
VALUE accept;
|
|
703
970
|
int completed;
|
|
704
971
|
int target;
|
|
705
972
|
int stream;
|
|
706
973
|
} completion_ctx_t;
|
|
707
974
|
|
|
708
|
-
static VALUE build_result_pair(
|
|
709
|
-
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);
|
|
710
977
|
}
|
|
711
978
|
|
|
712
|
-
static int
|
|
713
|
-
if (cctx->
|
|
714
|
-
|
|
979
|
+
static int response_accepted(completion_ctx_t *cctx, VALUE response) {
|
|
980
|
+
if (cctx->target <= 0 || NIL_P(cctx->accept))
|
|
981
|
+
return 1;
|
|
715
982
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
983
|
+
return RTEST(rb_funcall(cctx->accept, rb_intern("call"), 1, response));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
static int record_response(completion_ctx_t *cctx, long index, VALUE response) {
|
|
987
|
+
VALUE pair = build_result_pair(index, response);
|
|
720
988
|
|
|
989
|
+
if (cctx->stream) {
|
|
990
|
+
rb_yield(pair);
|
|
721
991
|
cctx->completed++;
|
|
722
|
-
|
|
723
|
-
|
|
992
|
+
return 0;
|
|
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;
|
|
724
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
|
+
|
|
725
1015
|
return 0;
|
|
726
1016
|
}
|
|
727
1017
|
|
|
@@ -751,21 +1041,16 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
|
|
|
751
1041
|
curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &ctx->http_status);
|
|
752
1042
|
|
|
753
1043
|
if (cctx->stream || cctx->target > 0) {
|
|
754
|
-
VALUE response =
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
rb_ary_push(cctx->results, pair);
|
|
1044
|
+
VALUE response =
|
|
1045
|
+
(msg->data.result == CURLE_OK)
|
|
1046
|
+
? build_response(ctx)
|
|
1047
|
+
: build_error_response_with_code(curl_easy_strerror(msg->data.result),
|
|
1048
|
+
(int)msg->data.result, ctx->attempts);
|
|
1049
|
+
if (record_response(cctx, ctx->index, response))
|
|
1050
|
+
return 1;
|
|
1051
|
+
} else {
|
|
1052
|
+
cctx->completed++;
|
|
764
1053
|
}
|
|
765
|
-
|
|
766
|
-
cctx->completed++;
|
|
767
|
-
if (cctx->target > 0 && cctx->completed >= cctx->target)
|
|
768
|
-
return 1;
|
|
769
1054
|
}
|
|
770
1055
|
|
|
771
1056
|
return 0;
|
|
@@ -782,30 +1067,34 @@ static int next_pending_index(multi_session_t *session) {
|
|
|
782
1067
|
}
|
|
783
1068
|
|
|
784
1069
|
static int activate_request(multi_session_t *session, VALUE requests, int idx, int *invalid,
|
|
785
|
-
|
|
1070
|
+
const request_options_t *opts) {
|
|
786
1071
|
request_ctx_t *ctx = &session->requests[idx];
|
|
787
1072
|
|
|
788
1073
|
if (invalid[idx] || ctx->done)
|
|
789
1074
|
return 0;
|
|
790
1075
|
|
|
791
|
-
if (!request_ctx_prepare_easy(ctx))
|
|
1076
|
+
if (!request_ctx_prepare_easy(ctx)) {
|
|
1077
|
+
SET_SETUP_ERROR(ctx, "Failed to allocate a curl handle", 0);
|
|
792
1078
|
return 0;
|
|
1079
|
+
}
|
|
793
1080
|
|
|
794
|
-
if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx),
|
|
1081
|
+
if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx), opts))
|
|
795
1082
|
return 0;
|
|
796
1083
|
|
|
797
|
-
|
|
798
|
-
|
|
1084
|
+
if (curl_multi_add_handle(session->multi, ctx->easy) != CURLM_OK) {
|
|
1085
|
+
SET_SETUP_ERROR(ctx, "Failed to schedule the request", 0);
|
|
799
1086
|
return 0;
|
|
1087
|
+
}
|
|
800
1088
|
|
|
1089
|
+
ctx->attempts++;
|
|
801
1090
|
ctx->active = 1;
|
|
802
1091
|
ctx->done = 0;
|
|
803
1092
|
session->active_count++;
|
|
804
1093
|
return 1;
|
|
805
1094
|
}
|
|
806
1095
|
|
|
807
|
-
static int fill_slots(multi_session_t *session, VALUE requests, int *invalid,
|
|
808
|
-
completion_ctx_t *cctx) {
|
|
1096
|
+
static int fill_slots(multi_session_t *session, VALUE requests, int *invalid,
|
|
1097
|
+
const request_options_t *opts, completion_ctx_t *cctx) {
|
|
809
1098
|
while (session->active_count < session->max_connections) {
|
|
810
1099
|
int idx = next_pending_index(session);
|
|
811
1100
|
if (idx < 0)
|
|
@@ -813,11 +1102,14 @@ static int fill_slots(multi_session_t *session, VALUE requests, int *invalid, lo
|
|
|
813
1102
|
|
|
814
1103
|
request_ctx_t *ctx = &session->requests[idx];
|
|
815
1104
|
|
|
816
|
-
if (!activate_request(session, requests, idx, invalid,
|
|
1105
|
+
if (!activate_request(session, requests, idx, invalid, opts)) {
|
|
817
1106
|
invalid[idx] = 1;
|
|
818
1107
|
ctx->done = 1;
|
|
819
1108
|
ctx->active = 0;
|
|
820
|
-
if (record_immediate_error(cctx, idx,
|
|
1109
|
+
if (record_immediate_error(cctx, idx,
|
|
1110
|
+
ctx->setup_error ? ctx->setup_error
|
|
1111
|
+
: "Invalid request configuration",
|
|
1112
|
+
ctx->attempts))
|
|
821
1113
|
return 1;
|
|
822
1114
|
}
|
|
823
1115
|
}
|
|
@@ -838,13 +1130,14 @@ static void prepare_pending(multi_session_t *session, int *indices, int count) {
|
|
|
838
1130
|
}
|
|
839
1131
|
|
|
840
1132
|
static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VALUE requests,
|
|
841
|
-
int *invalid,
|
|
1133
|
+
int *invalid, const request_options_t *opts, int *indices,
|
|
1134
|
+
int indices_count) {
|
|
842
1135
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
843
1136
|
VALUE scheduler = current_fiber_scheduler();
|
|
844
1137
|
#endif
|
|
845
1138
|
prepare_pending(session, indices, indices_count);
|
|
846
1139
|
|
|
847
|
-
if (fill_slots(session, requests, invalid,
|
|
1140
|
+
if (fill_slots(session, requests, invalid, opts, cctx))
|
|
848
1141
|
return;
|
|
849
1142
|
|
|
850
1143
|
curl_multi_perform(session->multi, &session->still_running);
|
|
@@ -852,7 +1145,10 @@ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VAL
|
|
|
852
1145
|
return;
|
|
853
1146
|
|
|
854
1147
|
while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
|
|
855
|
-
if (
|
|
1148
|
+
if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
|
|
1149
|
+
break;
|
|
1150
|
+
|
|
1151
|
+
if (fill_slots(session, requests, invalid, opts, cctx))
|
|
856
1152
|
return;
|
|
857
1153
|
|
|
858
1154
|
if (session->active_count == 0)
|
|
@@ -880,6 +1176,9 @@ static int is_default_retryable_curle(CURLcode code) {
|
|
|
880
1176
|
}
|
|
881
1177
|
|
|
882
1178
|
static int should_retry(request_ctx_t *ctx, retry_config_t *rc) {
|
|
1179
|
+
if (!ctx->idempotent && !rc->retry_non_idempotent)
|
|
1180
|
+
return 0;
|
|
1181
|
+
|
|
883
1182
|
if (ctx->curl_result != CURLE_OK)
|
|
884
1183
|
return is_default_retryable_curle(ctx->curl_result);
|
|
885
1184
|
|
|
@@ -930,6 +1229,26 @@ static void retry_config_init(retry_config_t *retry_cfg) {
|
|
|
930
1229
|
retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
|
|
931
1230
|
retry_cfg->retry_http_codes = NULL;
|
|
932
1231
|
retry_cfg->retry_http_count = 0;
|
|
1232
|
+
retry_cfg->retry_non_idempotent = 0;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
static long retry_backoff_ms(const retry_config_t *rc, int attempt) {
|
|
1236
|
+
long delay = rc->retry_delay_ms;
|
|
1237
|
+
long half;
|
|
1238
|
+
|
|
1239
|
+
if (delay <= 0)
|
|
1240
|
+
return 0;
|
|
1241
|
+
|
|
1242
|
+
for (int i = 0; i < attempt && delay < MAX_RETRY_DELAY_MS; i++)
|
|
1243
|
+
delay *= 2;
|
|
1244
|
+
if (delay > MAX_RETRY_DELAY_MS)
|
|
1245
|
+
delay = MAX_RETRY_DELAY_MS;
|
|
1246
|
+
|
|
1247
|
+
half = delay / 2;
|
|
1248
|
+
if (half > 0)
|
|
1249
|
+
delay = half + (long)(fast_rand() % (uint32_t)(half + 1));
|
|
1250
|
+
|
|
1251
|
+
return delay;
|
|
933
1252
|
}
|
|
934
1253
|
|
|
935
1254
|
static long parse_long_option(VALUE options, key_id_t key, const char *name, long min, long max,
|
|
@@ -986,8 +1305,16 @@ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
|
|
|
986
1305
|
retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(codes, i));
|
|
987
1306
|
}
|
|
988
1307
|
|
|
989
|
-
static void parse_options(VALUE options,
|
|
990
|
-
|
|
1308
|
+
static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
|
|
1309
|
+
retry_config_t *retry_cfg) {
|
|
1310
|
+
VALUE flag;
|
|
1311
|
+
|
|
1312
|
+
opts->timeout_sec = 30;
|
|
1313
|
+
opts->connect_timeout_ms = DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1314
|
+
opts->total_timeout_ms = 0;
|
|
1315
|
+
opts->follow_redirects = 1;
|
|
1316
|
+
opts->max_redirects = MAX_REDIRECTS;
|
|
1317
|
+
opts->deadline_ms = 0;
|
|
991
1318
|
*max_conn = 20;
|
|
992
1319
|
retry_config_init(retry_cfg);
|
|
993
1320
|
|
|
@@ -995,7 +1322,24 @@ static void parse_options(VALUE options, long *timeout, int *max_conn, retry_con
|
|
|
995
1322
|
return;
|
|
996
1323
|
|
|
997
1324
|
Check_Type(options, T_HASH);
|
|
998
|
-
|
|
1325
|
+
opts->timeout_sec =
|
|
1326
|
+
parse_long_option(options, KEY_TIMEOUT, "timeout", 1, MAX_TIMEOUT, opts->timeout_sec, NULL);
|
|
1327
|
+
opts->connect_timeout_ms =
|
|
1328
|
+
parse_long_option(options, KEY_CONNECT_TIMEOUT, "connect_timeout", 1,
|
|
1329
|
+
MAX_CONNECT_TIMEOUT_MS, opts->connect_timeout_ms, NULL);
|
|
1330
|
+
opts->total_timeout_ms = parse_long_option(options, KEY_TOTAL_TIMEOUT, "total_timeout", 1,
|
|
1331
|
+
MAX_TOTAL_TIMEOUT_MS, opts->total_timeout_ms, NULL);
|
|
1332
|
+
opts->max_redirects = parse_long_option(options, KEY_MAX_REDIRECTS, "max_redirects", 0, 100,
|
|
1333
|
+
opts->max_redirects, NULL);
|
|
1334
|
+
|
|
1335
|
+
flag = hash_aref_key(options, KEY_FOLLOW_REDIRECTS);
|
|
1336
|
+
if (!NIL_P(flag))
|
|
1337
|
+
opts->follow_redirects = RTEST(flag) ? 1L : 0L;
|
|
1338
|
+
|
|
1339
|
+
flag = hash_aref_key(options, KEY_RETRY_NON_IDEMPOTENT);
|
|
1340
|
+
if (!NIL_P(flag))
|
|
1341
|
+
retry_cfg->retry_non_idempotent = RTEST(flag) ? 1 : 0;
|
|
1342
|
+
|
|
999
1343
|
*max_conn = parse_int_option(options, KEY_CONNECTIONS, "connections", 1, MAX_CONNECTIONS,
|
|
1000
1344
|
*max_conn, NULL);
|
|
1001
1345
|
retry_cfg->max_retries = parse_int_option(options, KEY_RETRIES, "retries", 0, MAX_RETRIES,
|
|
@@ -1074,7 +1418,7 @@ typedef struct {
|
|
|
1074
1418
|
multi_session_t *session;
|
|
1075
1419
|
int *invalid;
|
|
1076
1420
|
retry_config_t *retry_cfg;
|
|
1077
|
-
|
|
1421
|
+
request_options_t *opts;
|
|
1078
1422
|
} execute_args_t;
|
|
1079
1423
|
|
|
1080
1424
|
static VALUE internal_execute_body(VALUE arg) {
|
|
@@ -1083,7 +1427,8 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1083
1427
|
multi_session_t *session = ea->session;
|
|
1084
1428
|
int *invalid = ea->invalid;
|
|
1085
1429
|
retry_config_t *retry_cfg = ea->retry_cfg;
|
|
1086
|
-
|
|
1430
|
+
request_options_t *opts = ea->opts;
|
|
1431
|
+
int deadline_hit = 0;
|
|
1087
1432
|
int count = session->count;
|
|
1088
1433
|
int target = ea->target;
|
|
1089
1434
|
int stream = ea->stream;
|
|
@@ -1093,19 +1438,31 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1093
1438
|
|
|
1094
1439
|
completion_ctx_t cctx;
|
|
1095
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;
|
|
1096
1442
|
cctx.completed = 0;
|
|
1097
1443
|
cctx.target = target;
|
|
1098
1444
|
cctx.stream = stream;
|
|
1099
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
|
+
|
|
1100
1449
|
if (!stream && target <= 0) {
|
|
1101
1450
|
for (int i = 0; i < count; i++)
|
|
1102
1451
|
rb_ary_store(cctx.results, i, Qnil);
|
|
1103
1452
|
}
|
|
1104
1453
|
|
|
1105
|
-
|
|
1454
|
+
if (opts->total_timeout_ms > 0)
|
|
1455
|
+
opts->deadline_ms = fast_now_ms() + opts->total_timeout_ms;
|
|
1456
|
+
|
|
1457
|
+
run_multi_loop(session, &cctx, requests, invalid, opts, NULL, count);
|
|
1106
1458
|
|
|
1107
1459
|
if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
|
|
1108
1460
|
for (int attempt = 0; attempt < retry_cfg->max_retries; attempt++) {
|
|
1461
|
+
if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
|
|
1462
|
+
deadline_hit = 1;
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1109
1466
|
int *retry_indices = malloc(sizeof(int) * (size_t)count);
|
|
1110
1467
|
if (!retry_indices)
|
|
1111
1468
|
rb_raise(rb_eNoMemError, "failed to allocate retry index array");
|
|
@@ -1123,7 +1480,18 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1123
1480
|
break;
|
|
1124
1481
|
}
|
|
1125
1482
|
|
|
1126
|
-
|
|
1483
|
+
long backoff = retry_backoff_ms(retry_cfg, attempt);
|
|
1484
|
+
if (opts->deadline_ms > 0) {
|
|
1485
|
+
long long left = opts->deadline_ms - fast_now_ms();
|
|
1486
|
+
if (left <= 0) {
|
|
1487
|
+
deadline_hit = 1;
|
|
1488
|
+
free(retry_indices);
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
if (backoff > (long)left)
|
|
1492
|
+
backoff = (long)left;
|
|
1493
|
+
}
|
|
1494
|
+
retry_delay_sleep(backoff);
|
|
1127
1495
|
|
|
1128
1496
|
int runnable_count = 0;
|
|
1129
1497
|
for (int r = 0; r < retry_count; r++) {
|
|
@@ -1133,6 +1501,7 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1133
1501
|
if (!request_ctx_reset_for_retry(rc)) {
|
|
1134
1502
|
invalid[idx] = 1;
|
|
1135
1503
|
rc->done = 1;
|
|
1504
|
+
rc->setup_error = "Failed to allocate a curl handle for the retry";
|
|
1136
1505
|
continue;
|
|
1137
1506
|
}
|
|
1138
1507
|
|
|
@@ -1141,7 +1510,7 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1141
1510
|
|
|
1142
1511
|
if (runnable_count > 0) {
|
|
1143
1512
|
cctx.completed = 0;
|
|
1144
|
-
run_multi_loop(session, &cctx, requests, invalid,
|
|
1513
|
+
run_multi_loop(session, &cctx, requests, invalid, opts, retry_indices,
|
|
1145
1514
|
runnable_count);
|
|
1146
1515
|
}
|
|
1147
1516
|
|
|
@@ -1155,14 +1524,20 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1155
1524
|
VALUE response;
|
|
1156
1525
|
|
|
1157
1526
|
if (invalid[i]) {
|
|
1158
|
-
response = build_error_response(
|
|
1527
|
+
response = build_error_response(rc->setup_error ? rc->setup_error
|
|
1528
|
+
: "Invalid request configuration",
|
|
1529
|
+
ERR_INVALID_REQUEST, rc->attempts);
|
|
1159
1530
|
} else if (!rc->done) {
|
|
1160
|
-
response =
|
|
1531
|
+
response =
|
|
1532
|
+
deadline_hit || (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
|
|
1533
|
+
? build_error_response("Total timeout exceeded", ERR_DEADLINE, rc->attempts)
|
|
1534
|
+
: build_error_response("Request was not completed", ERR_NOT_COMPLETED,
|
|
1535
|
+
rc->attempts);
|
|
1161
1536
|
} else if (rc->curl_result == CURLE_OK) {
|
|
1162
1537
|
response = build_response(rc);
|
|
1163
1538
|
} else {
|
|
1164
1539
|
response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
|
|
1165
|
-
(int)rc->curl_result);
|
|
1540
|
+
(int)rc->curl_result, rc->attempts);
|
|
1166
1541
|
}
|
|
1167
1542
|
|
|
1168
1543
|
rb_ary_store(cctx.results, i, build_result_pair(i, response));
|
|
@@ -1172,6 +1547,482 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1172
1547
|
return stream ? Qnil : cctx.results;
|
|
1173
1548
|
}
|
|
1174
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
|
+
|
|
1175
2026
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
1176
2027
|
typedef struct {
|
|
1177
2028
|
execute_args_t *ea;
|
|
@@ -1247,6 +2098,9 @@ static VALUE execute_with_fiber_scheduler(VALUE arg) {
|
|
|
1247
2098
|
#endif
|
|
1248
2099
|
|
|
1249
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
|
+
|
|
1250
2104
|
Check_Type(requests, T_ARRAY);
|
|
1251
2105
|
|
|
1252
2106
|
long count_long = RARRAY_LEN(requests);
|
|
@@ -1262,10 +2116,37 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1262
2116
|
if (target > 0 && target > count)
|
|
1263
2117
|
target = count;
|
|
1264
2118
|
|
|
1265
|
-
|
|
2119
|
+
request_options_t opts;
|
|
1266
2120
|
int max_conn;
|
|
1267
2121
|
retry_config_t retry_cfg;
|
|
1268
|
-
parse_options(options, &
|
|
2122
|
+
parse_options(options, &opts, &max_conn, &retry_cfg);
|
|
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
|
+
|
|
2130
|
+
for (int i = 0; i < count; i++) {
|
|
2131
|
+
request_ctx_t probe;
|
|
2132
|
+
request_ctx_init(&probe, i);
|
|
2133
|
+
if (!request_ctx_prepare_easy(&probe)) {
|
|
2134
|
+
request_ctx_free(&probe);
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
setup_easy_handle(&probe, rb_ary_entry(requests, i), &opts);
|
|
2138
|
+
int fatal = probe.setup_error_fatal;
|
|
2139
|
+
const char *message = probe.setup_error;
|
|
2140
|
+
char buffer[128];
|
|
2141
|
+
if (fatal && message) {
|
|
2142
|
+
snprintf(buffer, sizeof(buffer), "%s (request %d)", message, i);
|
|
2143
|
+
}
|
|
2144
|
+
request_ctx_free(&probe);
|
|
2145
|
+
if (fatal) {
|
|
2146
|
+
VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
|
|
2147
|
+
rb_raise(klass, "%s", buffer);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
1269
2150
|
|
|
1270
2151
|
if (stream || target > 0) {
|
|
1271
2152
|
if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
|
|
@@ -1279,7 +2160,7 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1279
2160
|
|
|
1280
2161
|
multi_session_t session;
|
|
1281
2162
|
int *invalid = NULL;
|
|
1282
|
-
multi_session_init(&session, curl_multi_init(), count, max_conn, timeout_sec);
|
|
2163
|
+
multi_session_init(&session, curl_multi_init(), count, max_conn, opts.timeout_sec);
|
|
1283
2164
|
|
|
1284
2165
|
cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
|
|
1285
2166
|
|
|
@@ -1304,12 +2185,13 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1304
2185
|
.session = &session,
|
|
1305
2186
|
.invalid = invalid,
|
|
1306
2187
|
.retry_cfg = &retry_cfg,
|
|
1307
|
-
.
|
|
2188
|
+
.opts = &opts,
|
|
1308
2189
|
};
|
|
1309
2190
|
|
|
1310
2191
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
1311
2192
|
VALUE scheduler = current_fiber_scheduler();
|
|
1312
|
-
|
|
2193
|
+
VALUE accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
|
|
2194
|
+
if (scheduler != Qnil && !stream && NIL_P(accept)) {
|
|
1313
2195
|
scheduler_execute_ctx_t scheduler_ctx = {
|
|
1314
2196
|
.ea = &ea,
|
|
1315
2197
|
.scheduler = scheduler,
|
|
@@ -1326,7 +2208,6 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1326
2208
|
(VALUE)&cleanup);
|
|
1327
2209
|
}
|
|
1328
2210
|
#endif
|
|
1329
|
-
|
|
1330
2211
|
return rb_ensure(internal_execute_body, (VALUE)&ea, cleanup_session, (VALUE)&cleanup);
|
|
1331
2212
|
}
|
|
1332
2213
|
|
|
@@ -1366,6 +2247,7 @@ static VALUE rb_fast_curl_stream_execute(int argc, VALUE *argv, VALUE self) {
|
|
|
1366
2247
|
|
|
1367
2248
|
void Init_fast_curl(void) {
|
|
1368
2249
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
2250
|
+
fast_share_init();
|
|
1369
2251
|
|
|
1370
2252
|
for (int i = 0; i < KEY_LAST; i++) {
|
|
1371
2253
|
fast_ids[i] = rb_intern(KEY_NAMES[i]);
|
|
@@ -1373,8 +2255,18 @@ void Init_fast_curl(void) {
|
|
|
1373
2255
|
rb_gc_register_address(&fast_syms[i]);
|
|
1374
2256
|
}
|
|
1375
2257
|
|
|
2258
|
+
for (int i = 0; i < ERR_LAST; i++) {
|
|
2259
|
+
error_syms[i] = ID2SYM(rb_intern(ERROR_KIND_NAMES[i]));
|
|
2260
|
+
rb_gc_register_address(&error_syms[i]);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1376
2263
|
VALUE mFastCurl = rb_define_module("FastCurl");
|
|
1377
2264
|
|
|
2265
|
+
if (rb_const_defined_at(mFastCurl, rb_intern("Headers"))) {
|
|
2266
|
+
fast_cHeaders = rb_const_get_at(mFastCurl, rb_intern("Headers"));
|
|
2267
|
+
rb_gc_register_address(&fast_cHeaders);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1378
2270
|
rb_define_module_function(mFastCurl, "execute", rb_fast_curl_execute, -1);
|
|
1379
2271
|
rb_define_module_function(mFastCurl, "first_execute", rb_fast_curl_first_execute, -1);
|
|
1380
2272
|
rb_define_module_function(mFastCurl, "stream_execute", rb_fast_curl_stream_execute, -1);
|