fast_curl 0.3.0 → 0.3.1

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.
@@ -4,15 +4,32 @@
4
4
  #ifdef HAVE_RUBY_FIBER_SCHEDULER_H
5
5
  #include <ruby/fiber/scheduler.h>
6
6
  #endif
7
+
8
+ #if defined(HAVE_RUBY_FIBER_SCHEDULER_H) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT) && \
9
+ defined(HAVE_RB_FIBER_SCHEDULER_BLOCK) && defined(HAVE_RB_FIBER_SCHEDULER_UNBLOCK) && \
10
+ defined(HAVE_RB_FIBER_CURRENT)
11
+ #define FAST_CURL_HAVE_FIBER_SCHEDULER 1
12
+ #endif
7
13
  #include <curl/curl.h>
14
+ #include <limits.h>
8
15
  #include <stdlib.h>
9
16
  #include <string.h>
17
+ #include <strings.h>
18
+ #include <time.h>
19
+
20
+ #if defined(__GNUC__) || defined(__clang__)
21
+ #define FAST_CURL_NORETURN __attribute__((noreturn))
22
+ #else
23
+ #define FAST_CURL_NORETURN
24
+ #endif
10
25
 
11
26
  #define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
12
27
  #define MAX_REDIRECTS 5
13
28
  #define MAX_TIMEOUT 300
14
29
  #define MAX_RETRIES 10
15
30
  #define MAX_REQUESTS 10000
31
+ #define MAX_CONNECTIONS 100
32
+ #define MAX_RETRY_DELAY_MS 30000
16
33
  #define DEFAULT_RETRIES 1
17
34
  #define DEFAULT_RETRY_DELAY 0
18
35
  #define INITIAL_BUF_CAP 8192
@@ -27,12 +44,31 @@ static const CURLcode DEFAULT_RETRYABLE_CURLE[] = {
27
44
  #define DEFAULT_RETRYABLE_CURLE_COUNT \
28
45
  (int)(sizeof(DEFAULT_RETRYABLE_CURLE) / sizeof(DEFAULT_RETRYABLE_CURLE[0]))
29
46
 
30
- static ID id_status, id_headers, id_body, id_error_code, id_url, id_method;
31
- static ID id_timeout, id_connections, id_count, id_keys;
32
- static ID id_retries, id_retry_delay, id_retry_codes;
33
- static VALUE sym_status, sym_headers, sym_body, sym_error_code, sym_url, sym_method;
34
- static VALUE sym_timeout, sym_connections, sym_count;
35
- static VALUE sym_retries, sym_retry_delay, sym_retry_codes;
47
+ typedef enum {
48
+ KEY_STATUS,
49
+ KEY_HEADERS,
50
+ KEY_BODY,
51
+ KEY_ERROR_CODE,
52
+ KEY_URL,
53
+ KEY_METHOD,
54
+ KEY_TIMEOUT,
55
+ KEY_CONNECTIONS,
56
+ KEY_COUNT_OPT,
57
+ KEY_RETRIES,
58
+ KEY_RETRY_DELAY,
59
+ KEY_RETRY_CODES,
60
+ KEY_LAST
61
+ } key_id_t;
62
+
63
+ static ID fast_ids[KEY_LAST];
64
+ static VALUE fast_syms[KEY_LAST];
65
+
66
+ #define SYM(key) fast_syms[key]
67
+
68
+ static const char *const KEY_NAMES[KEY_LAST] = {
69
+ "status", "headers", "body", "error_code", "url", "method",
70
+ "timeout", "connections", "count", "retries", "retry_delay", "retry_codes",
71
+ };
36
72
 
37
73
  typedef struct {
38
74
  char *data;
@@ -64,20 +100,31 @@ static inline void buffer_reset(buffer_t *buf) {
64
100
  static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
65
101
  buffer_t *buf = (buffer_t *)userdata;
66
102
  size_t total = size * nmemb;
67
- if (buf->len + total > buf->max_size)
103
+
104
+ if (nmemb != 0 && total / nmemb != size)
105
+ return 0;
106
+ if (total > buf->max_size || buf->len > buf->max_size - total)
68
107
  return 0;
108
+
69
109
  if (buf->len + total >= buf->cap) {
70
110
  size_t new_cap = (buf->cap == 0) ? INITIAL_BUF_CAP : buf->cap;
71
- while (new_cap <= buf->len + total)
111
+ while (new_cap <= buf->len + total) {
112
+ if (new_cap > buf->max_size / 2) {
113
+ new_cap = buf->max_size;
114
+ break;
115
+ }
72
116
  new_cap *= 2;
73
- if (new_cap > buf->max_size)
74
- new_cap = buf->max_size;
117
+ }
118
+ if (new_cap < buf->len + total)
119
+ return 0;
120
+
75
121
  char *new_data = realloc(buf->data, new_cap);
76
122
  if (!new_data)
77
123
  return 0;
78
124
  buf->data = new_data;
79
125
  buf->cap = new_cap;
80
126
  }
127
+
81
128
  memcpy(buf->data + buf->len, ptr, total);
82
129
  buf->len += total;
83
130
  return total;
@@ -118,8 +165,22 @@ static void header_list_reset(header_list_t *h) {
118
165
  static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
119
166
  header_list_t *h = (header_list_t *)userdata;
120
167
  size_t total = size * nmemb;
121
- if (total <= 2)
168
+
169
+ if (nmemb != 0 && total / nmemb != size)
170
+ return 0;
171
+
172
+ size_t stripped = total;
173
+ while (stripped > 0 && (ptr[stripped - 1] == '\r' || ptr[stripped - 1] == '\n'))
174
+ stripped--;
175
+
176
+ if (stripped >= 5 && memcmp(ptr, "HTTP/", 5) == 0) {
177
+ header_list_reset(h);
178
+ return total;
179
+ }
180
+
181
+ if (stripped == 0)
122
182
  return total;
183
+
123
184
  if (h->count >= h->cap) {
124
185
  int new_cap = (h->cap == 0) ? INITIAL_HEADER_CAP : h->cap * 2;
125
186
  header_entry_t *ne = realloc(h->entries, sizeof(header_entry_t) * new_cap);
@@ -128,18 +189,17 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
128
189
  h->entries = ne;
129
190
  h->cap = new_cap;
130
191
  }
131
- size_t stripped = total;
132
- while (stripped > 0 && (ptr[stripped - 1] == '\r' || ptr[stripped - 1] == '\n'))
133
- stripped--;
192
+
134
193
  char *entry = malloc(stripped + 1);
135
194
  if (!entry)
136
195
  return 0;
137
196
  memcpy(entry, ptr, stripped);
138
197
  entry[stripped] = '\0';
198
+
139
199
  h->entries[h->count].str = entry;
140
200
  h->entries[h->count].len = stripped;
141
201
  h->count++;
142
- return size * nmemb;
202
+ return total;
143
203
  }
144
204
 
145
205
  typedef struct {
@@ -149,17 +209,19 @@ typedef struct {
149
209
  header_list_t headers;
150
210
  struct curl_slist *req_headers;
151
211
  int done;
212
+ int active;
152
213
  CURLcode curl_result;
153
214
  long http_status;
154
215
  } request_ctx_t;
155
216
 
156
217
  static inline void request_ctx_init(request_ctx_t *ctx, int index) {
157
- ctx->easy = curl_easy_init();
218
+ ctx->easy = NULL;
158
219
  ctx->index = index;
159
220
  buffer_init(&ctx->body);
160
221
  header_list_init(&ctx->headers);
161
222
  ctx->req_headers = NULL;
162
223
  ctx->done = 0;
224
+ ctx->active = 0;
163
225
  ctx->curl_result = CURLE_OK;
164
226
  ctx->http_status = 0;
165
227
  }
@@ -175,6 +237,16 @@ static void request_ctx_free(request_ctx_t *ctx) {
175
237
  curl_slist_free_all(ctx->req_headers);
176
238
  ctx->req_headers = NULL;
177
239
  }
240
+ ctx->active = 0;
241
+ }
242
+
243
+ static int request_ctx_prepare_easy(request_ctx_t *ctx) {
244
+ if (!ctx->easy) {
245
+ ctx->easy = curl_easy_init();
246
+ if (!ctx->easy)
247
+ return 0;
248
+ }
249
+ return 1;
178
250
  }
179
251
 
180
252
  static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
@@ -192,6 +264,7 @@ static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
192
264
  if (!ctx->easy)
193
265
  return 0;
194
266
  ctx->done = 0;
267
+ ctx->active = 0;
195
268
  ctx->curl_result = CURLE_OK;
196
269
  ctx->http_status = 0;
197
270
  return 1;
@@ -205,10 +278,16 @@ typedef struct {
205
278
  long timeout_ms;
206
279
  int max_connections;
207
280
  volatile int cancelled;
281
+
282
+ int active_count;
283
+ int pending_pos;
284
+ int pending_count;
285
+ int *pending_indices;
208
286
  } multi_session_t;
209
287
 
210
288
  typedef struct {
211
289
  int max_retries;
290
+ int retries_explicit;
212
291
  long retry_delay_ms;
213
292
  int *retry_http_codes;
214
293
  int retry_http_count;
@@ -222,18 +301,61 @@ static int contains_header_injection(const char *str, long len) {
222
301
  return 0;
223
302
  }
224
303
 
225
- #ifdef HAVE_RB_FIBER_SCHEDULER_CURRENT
304
+ static int is_header_token_char(unsigned char c) {
305
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
306
+ return 1;
307
+ switch (c) {
308
+ case '!':
309
+ case '#':
310
+ case '$':
311
+ case '%':
312
+ case '&':
313
+ case '\'':
314
+ case '*':
315
+ case '+':
316
+ case '-':
317
+ case '.':
318
+ case '^':
319
+ case '_':
320
+ case '`':
321
+ case '|':
322
+ case '~':
323
+ return 1;
324
+ default:
325
+ return 0;
326
+ }
327
+ }
328
+
329
+ static int is_valid_header_name(const char *str, long len) {
330
+ if (len <= 0)
331
+ return 0;
332
+ for (long i = 0; i < len; i++) {
333
+ if (!is_header_token_char((unsigned char)str[i]))
334
+ return 0;
335
+ }
336
+ return 1;
337
+ }
338
+
339
+ static VALUE hash_aref_symbol_or_string(VALUE hash, VALUE sym, ID id) {
340
+ VALUE value = rb_hash_aref(hash, sym);
341
+ if (!NIL_P(value))
342
+ return value;
343
+
344
+ const char *name = rb_id2name(id);
345
+ return name ? rb_hash_aref(hash, rb_str_new_cstr(name)) : Qnil;
346
+ }
347
+
348
+ static inline VALUE hash_aref_key(VALUE hash, key_id_t key) {
349
+ return hash_aref_symbol_or_string(hash, fast_syms[key], fast_ids[key]);
350
+ }
351
+
352
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
226
353
  static VALUE current_fiber_scheduler(void) {
227
354
  VALUE sched = rb_fiber_scheduler_current();
228
355
  if (sched == Qnil || sched == Qfalse)
229
356
  return Qnil;
230
357
  return sched;
231
358
  }
232
- #else
233
- static VALUE current_fiber_scheduler(void) {
234
- return Qnil;
235
- }
236
- #endif
237
359
 
238
360
  typedef struct {
239
361
  void *(*func)(void *);
@@ -268,10 +390,25 @@ static void run_via_fiber_worker(VALUE scheduler, void *(*func)(void *), void *a
268
390
  rb_fiber_scheduler_block(scheduler, ctx.blocker, Qnil);
269
391
  rb_funcall(th, rb_intern("join"), 0);
270
392
  }
393
+ #endif
394
+
395
+ static void headers_hash_store(VALUE headers_hash, VALUE key, VALUE val) {
396
+ VALUE existing = rb_hash_aref(headers_hash, key);
397
+
398
+ if (NIL_P(existing)) {
399
+ rb_hash_aset(headers_hash, key, val);
400
+ } else if (RB_TYPE_P(existing, T_ARRAY)) {
401
+ rb_ary_push(existing, val);
402
+ } else {
403
+ VALUE values = rb_ary_new_from_args(2, existing, val);
404
+ rb_hash_aset(headers_hash, key, values);
405
+ }
406
+ }
271
407
 
272
408
  static VALUE build_response(request_ctx_t *ctx) {
273
409
  long status = 0;
274
410
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
411
+
275
412
  VALUE headers_hash = rb_hash_new();
276
413
  for (int i = 0; i < ctx->headers.count; i++) {
277
414
  const char *hdr = ctx->headers.entries[i].str;
@@ -279,38 +416,41 @@ static VALUE build_response(request_ctx_t *ctx) {
279
416
  const char *colon = memchr(hdr, ':', hdr_len);
280
417
  if (!colon)
281
418
  continue;
419
+
282
420
  VALUE key = rb_str_new(hdr, colon - hdr);
283
- const char *vs = colon + 1, *ve = hdr + hdr_len;
421
+ const char *vs = colon + 1;
422
+ const char *ve = hdr + hdr_len;
423
+
284
424
  while (vs < ve && (*vs == ' ' || *vs == '\t'))
285
425
  vs++;
286
426
  while (ve > vs && (*(ve - 1) == ' ' || *(ve - 1) == '\t'))
287
427
  ve--;
428
+
288
429
  VALUE val = rb_str_new(vs, ve - vs);
289
- rb_hash_aset(headers_hash, key, val);
430
+ headers_hash_store(headers_hash, key, val);
290
431
  }
432
+
291
433
  VALUE body_str =
292
434
  ctx->body.data ? rb_str_new(ctx->body.data, ctx->body.len) : rb_str_new_cstr("");
435
+
293
436
  VALUE result = rb_hash_new();
294
- rb_hash_aset(result, sym_status, LONG2NUM(status));
295
- rb_hash_aset(result, sym_headers, headers_hash);
296
- rb_hash_aset(result, sym_body, body_str);
437
+ rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
438
+ rb_hash_aset(result, SYM(KEY_HEADERS), headers_hash);
439
+ rb_hash_aset(result, SYM(KEY_BODY), body_str);
297
440
  return result;
298
441
  }
299
442
 
300
443
  static VALUE build_error_response(const char *message) {
301
444
  VALUE r = rb_hash_new();
302
- rb_hash_aset(r, sym_status, INT2NUM(0));
303
- rb_hash_aset(r, sym_headers, Qnil);
304
- rb_hash_aset(r, sym_body, rb_str_new_cstr(message));
445
+ rb_hash_aset(r, SYM(KEY_STATUS), INT2NUM(0));
446
+ rb_hash_aset(r, SYM(KEY_HEADERS), Qnil);
447
+ rb_hash_aset(r, SYM(KEY_BODY), rb_str_new_cstr(message));
305
448
  return r;
306
449
  }
307
450
 
308
451
  static VALUE build_error_response_with_code(const char *message, int error_code) {
309
- VALUE r = rb_hash_new();
310
- rb_hash_aset(r, sym_status, INT2NUM(0));
311
- rb_hash_aset(r, sym_headers, Qnil);
312
- rb_hash_aset(r, sym_body, rb_str_new_cstr(message));
313
- rb_hash_aset(r, sym_error_code, INT2NUM(error_code));
452
+ VALUE r = build_error_response(message);
453
+ rb_hash_aset(r, SYM(KEY_ERROR_CODE), INT2NUM(error_code));
314
454
  return r;
315
455
  }
316
456
 
@@ -354,7 +494,7 @@ static CURLcode setup_basic_options(CURL *easy, const char *url_str, long timeou
354
494
  static CURLcode setup_security_options(CURL *easy) {
355
495
  CURL_SETOPT_CHECK(easy, CURLOPT_SSL_VERIFYPEER, 1L);
356
496
  CURL_SETOPT_CHECK(easy, CURLOPT_SSL_VERIFYHOST, 2L);
357
- #ifdef CURLOPT_PROTOCOLS_STR
497
+ #if LIBCURL_VERSION_NUM >= 0x075500
358
498
  CURL_SETOPT_CHECK(easy, CURLOPT_PROTOCOLS_STR, "http,https");
359
499
  CURL_SETOPT_CHECK(easy, CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
360
500
  #else
@@ -364,102 +504,165 @@ static CURLcode setup_security_options(CURL *easy) {
364
504
  return CURLE_OK;
365
505
  }
366
506
 
367
- static CURLcode setup_method_and_body(CURL *easy, VALUE method, VALUE body) {
368
- if (!NIL_P(method)) {
369
- const char *m = StringValueCStr(method);
370
- if (strcmp(m, "POST") == 0) {
371
- CURL_SETOPT_CHECK(easy, CURLOPT_POST, 1L);
372
- } else if (strcmp(m, "PUT") == 0) {
373
- CURL_SETOPT_CHECK(easy, CURLOPT_CUSTOMREQUEST, "PUT");
374
- } else if (strcmp(m, "DELETE") == 0) {
375
- CURL_SETOPT_CHECK(easy, CURLOPT_CUSTOMREQUEST, "DELETE");
376
- } else if (strcmp(m, "PATCH") == 0) {
377
- CURL_SETOPT_CHECK(easy, CURLOPT_CUSTOMREQUEST, "PATCH");
378
- } else if (strcmp(m, "GET") != 0) {
379
- CURL_SETOPT_CHECK(easy, CURLOPT_CUSTOMREQUEST, m);
380
- }
507
+ static CURLcode set_body(CURL *easy, VALUE body) {
508
+ VALUE body_str = rb_String(body);
509
+ CURL_SETOPT_CHECK(easy, CURLOPT_POSTFIELDSIZE, (long)RSTRING_LEN(body_str));
510
+ CURL_SETOPT_CHECK(easy, CURLOPT_COPYPOSTFIELDS, StringValuePtr(body_str));
511
+ RB_GC_GUARD(body_str);
512
+ return CURLE_OK;
513
+ }
514
+
515
+ typedef struct {
516
+ const char *name;
517
+ const char *custom;
518
+ int post;
519
+ int nobody;
520
+ int allows_body;
521
+ } http_method_t;
522
+
523
+ 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},
527
+ };
528
+
529
+ static const http_method_t *find_http_method(const char *name) {
530
+ for (size_t i = 0; i < sizeof(HTTP_METHODS) / sizeof(HTTP_METHODS[0]); i++) {
531
+ if (strcasecmp(HTTP_METHODS[i].name, name) == 0)
532
+ return &HTTP_METHODS[i];
381
533
  }
534
+ return NULL;
535
+ }
382
536
 
383
- if (!NIL_P(body)) {
384
- CURL_SETOPT_CHECK(easy, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(body));
385
- CURL_SETOPT_CHECK(easy, CURLOPT_COPYPOSTFIELDS, StringValuePtr(body));
537
+ static CURLcode apply_http_method(CURL *easy, const http_method_t *method) {
538
+ if (method->post)
539
+ CURL_SETOPT_CHECK(easy, CURLOPT_POST, 1L);
540
+ if (method->custom)
541
+ CURL_SETOPT_CHECK(easy, CURLOPT_CUSTOMREQUEST, method->custom);
542
+ if (method->nobody)
543
+ CURL_SETOPT_CHECK(easy, CURLOPT_NOBODY, 1L);
544
+ return CURLE_OK;
545
+ }
546
+
547
+ static CURLcode setup_method_and_body(CURL *easy, VALUE method_value, VALUE body) {
548
+ const char *name = NIL_P(method_value) ? "GET" : StringValueCStr(method_value);
549
+ const http_method_t *method = find_http_method(name);
550
+ int has_body = !NIL_P(body);
551
+
552
+ if (!method)
553
+ rb_raise(rb_eArgError, "Unsupported HTTP method: %s", name);
554
+ if (has_body && !method->allows_body)
555
+ rb_raise(rb_eArgError, "%s requests must not include a body", method->name);
556
+
557
+ CURLcode res = apply_http_method(easy, method);
558
+ if (res != CURLE_OK)
559
+ return res;
560
+
561
+ if (has_body) {
562
+ res = set_body(easy, body);
563
+ if (res != CURLE_OK)
564
+ return res;
386
565
  }
566
+
567
+ RB_GC_GUARD(method_value);
568
+ RB_GC_GUARD(body);
387
569
  return CURLE_OK;
388
570
  }
389
571
 
572
+ static void append_request_header(request_ctx_t *ctx, const char *buf) {
573
+ struct curl_slist *new_headers = curl_slist_append(ctx->req_headers, buf);
574
+ if (!new_headers)
575
+ rb_raise(rb_eNoMemError, "failed to allocate request header");
576
+ ctx->req_headers = new_headers;
577
+ }
578
+
579
+ static char *alloc_header_line(const char *key, long key_len, const char *value, long value_len,
580
+ char stack_buf[HEADER_LINE_BUF_SIZE]) {
581
+ int has_value = value && value_len > 0;
582
+ long need = key_len + (has_value ? 2 + value_len : 1) + 1;
583
+ char *buf = need > HEADER_LINE_BUF_SIZE ? malloc((size_t)need) : stack_buf;
584
+
585
+ if (!buf)
586
+ rb_raise(rb_eNoMemError, "failed to allocate request header");
587
+
588
+ memcpy(buf, key, (size_t)key_len);
589
+ if (has_value) {
590
+ buf[key_len] = ':';
591
+ buf[key_len + 1] = ' ';
592
+ memcpy(buf + key_len + 2, value, (size_t)value_len);
593
+ buf[key_len + 2 + value_len] = '\0';
594
+ } else {
595
+ buf[key_len] = ';';
596
+ buf[key_len + 1] = '\0';
597
+ }
598
+
599
+ return buf;
600
+ }
601
+
602
+ static void append_formatted_header(request_ctx_t *ctx, const char *key, long key_len,
603
+ const char *value, long value_len) {
604
+ char stack_buf[HEADER_LINE_BUF_SIZE];
605
+ char *line = alloc_header_line(key, key_len, value, value_len, stack_buf);
606
+ append_request_header(ctx, line);
607
+ if (line != stack_buf)
608
+ free(line);
609
+ }
610
+
390
611
  static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
391
612
  request_ctx_t *ctx = (request_ctx_t *)arg;
392
613
  VALUE key_str = rb_String(key);
614
+ VALUE val_str = NIL_P(val) ? Qnil : rb_String(val);
393
615
  const char *k = RSTRING_PTR(key_str);
394
616
  long klen = RSTRING_LEN(key_str);
617
+ const char *v = NULL;
618
+ long vlen = 0;
395
619
 
396
- if (contains_header_injection(k, klen))
397
- return ST_CONTINUE;
398
-
399
- if (NIL_P(val) || RSTRING_LEN(rb_String(val)) == 0) {
400
- char sbuf[HEADER_LINE_BUF_SIZE];
401
- char *buf = sbuf;
402
- long need = klen + 2;
403
- if (need > HEADER_LINE_BUF_SIZE)
404
- buf = malloc(need);
405
- if (!buf)
406
- return ST_CONTINUE;
407
- memcpy(buf, k, klen);
408
- buf[klen] = ';';
409
- buf[klen + 1] = '\0';
410
- ctx->req_headers = curl_slist_append(ctx->req_headers, buf);
411
- if (buf != sbuf)
412
- free(buf);
413
- } else {
414
- VALUE val_str = rb_String(val);
415
- const char *v = RSTRING_PTR(val_str);
416
- long vlen = RSTRING_LEN(val_str);
620
+ if (!is_valid_header_name(k, klen) || contains_header_injection(k, klen))
621
+ rb_raise(rb_eArgError, "Invalid HTTP header name");
417
622
 
623
+ if (!NIL_P(val_str) && RSTRING_LEN(val_str) > 0) {
624
+ v = RSTRING_PTR(val_str);
625
+ vlen = RSTRING_LEN(val_str);
418
626
  if (contains_header_injection(v, vlen))
419
- return ST_CONTINUE;
420
- char sbuf[HEADER_LINE_BUF_SIZE];
421
- char *buf = sbuf;
422
- long need = klen + 2 + vlen + 1;
423
- if (need > HEADER_LINE_BUF_SIZE)
424
- buf = malloc(need);
425
- if (!buf)
426
- return ST_CONTINUE;
427
-
428
- memcpy(buf, k, klen);
429
- buf[klen] = ':';
430
- buf[klen + 1] = ' ';
431
- memcpy(buf + klen + 2, v, vlen);
432
- buf[klen + 2 + vlen] = '\0';
433
- ctx->req_headers = curl_slist_append(ctx->req_headers, buf);
434
- if (buf != sbuf)
435
- free(buf);
627
+ rb_raise(rb_eArgError, "Invalid HTTP header value");
436
628
  }
629
+
630
+ append_formatted_header(ctx, k, klen, v, vlen);
631
+
632
+ RB_GC_GUARD(key_str);
633
+ RB_GC_GUARD(val_str);
437
634
  return ST_CONTINUE;
438
635
  }
439
636
 
440
637
  static int setup_easy_handle(request_ctx_t *ctx, VALUE request, long timeout_sec) {
441
- VALUE url = rb_hash_aref(request, sym_url);
442
- VALUE method = rb_hash_aref(request, sym_method);
443
- VALUE headers = rb_hash_aref(request, sym_headers);
444
- VALUE body = rb_hash_aref(request, sym_body);
638
+ Check_Type(request, T_HASH);
639
+
640
+ VALUE url = hash_aref_key(request, KEY_URL);
641
+ VALUE method = hash_aref_key(request, KEY_METHOD);
642
+ VALUE headers = hash_aref_key(request, KEY_HEADERS);
643
+ VALUE body = hash_aref_key(request, KEY_BODY);
644
+
445
645
  if (NIL_P(url))
446
646
  return 0;
647
+
447
648
  const char *url_str = StringValueCStr(url);
448
649
  if (!is_valid_url(url_str))
449
650
  rb_raise(rb_eArgError, "Invalid URL: %s", url_str);
450
651
 
451
- CURLcode res;
452
- res = setup_basic_options(ctx->easy, url_str, timeout_sec, ctx);
652
+ CURLcode res = setup_basic_options(ctx->easy, url_str, timeout_sec, ctx);
453
653
  if (res != CURLE_OK)
454
654
  return 0;
655
+
455
656
  res = setup_security_options(ctx->easy);
456
657
  if (res != CURLE_OK)
457
658
  return 0;
659
+
458
660
  res = setup_method_and_body(ctx->easy, method, body);
459
661
  if (res != CURLE_OK)
460
662
  return 0;
461
663
 
462
- if (!NIL_P(headers) && rb_obj_is_kind_of(headers, rb_cHash)) {
664
+ if (!NIL_P(headers)) {
665
+ Check_Type(headers, T_HASH);
463
666
  rb_hash_foreach(headers, header_iter_cb, (VALUE)ctx);
464
667
  if (ctx->req_headers) {
465
668
  res = curl_easy_setopt(ctx->easy, CURLOPT_HTTPHEADER, ctx->req_headers);
@@ -480,6 +683,7 @@ static void *poll_without_gvl(void *arg) {
480
683
  multi_session_t *s = (multi_session_t *)arg;
481
684
  if (s->cancelled)
482
685
  return NULL;
686
+
483
687
  int numfds = 0;
484
688
  curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
485
689
  curl_multi_perform(s->multi, &s->still_running);
@@ -501,61 +705,171 @@ typedef struct {
501
705
  int stream;
502
706
  } completion_ctx_t;
503
707
 
708
+ static VALUE build_result_pair(int index, VALUE response) {
709
+ return rb_ary_new_from_args(2, INT2NUM(index), response);
710
+ }
711
+
712
+ static int record_immediate_error(completion_ctx_t *cctx, int index, const char *message) {
713
+ if (cctx->stream || cctx->target > 0) {
714
+ VALUE pair = build_result_pair(index, build_error_response(message));
715
+
716
+ if (cctx->stream)
717
+ rb_yield(pair);
718
+ else
719
+ rb_ary_push(cctx->results, pair);
720
+
721
+ cctx->completed++;
722
+ if (cctx->target > 0 && cctx->completed >= cctx->target)
723
+ return 1;
724
+ }
725
+ return 0;
726
+ }
727
+
504
728
  static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
505
729
  CURLMsg *msg;
506
730
  int msgs_left;
731
+
507
732
  while ((msg = curl_multi_info_read(session->multi, &msgs_left))) {
508
733
  if (msg->msg != CURLMSG_DONE)
509
734
  continue;
735
+
510
736
  request_ctx_t *ctx = NULL;
511
737
  curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&ctx);
512
738
  if (!ctx || ctx->done)
513
739
  continue;
740
+
741
+ if (ctx->active) {
742
+ curl_multi_remove_handle(session->multi, ctx->easy);
743
+ ctx->active = 0;
744
+ if (session->active_count > 0)
745
+ session->active_count--;
746
+ }
747
+
514
748
  ctx->done = 1;
515
749
  ctx->curl_result = msg->data.result;
516
750
  if (msg->data.result == CURLE_OK)
517
751
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &ctx->http_status);
518
- if (cctx->stream) {
752
+
753
+ if (cctx->stream || cctx->target > 0) {
519
754
  VALUE response = (msg->data.result == CURLE_OK)
520
755
  ? build_response(ctx)
521
756
  : build_error_response_with_code(
522
757
  curl_easy_strerror(msg->data.result), (int)msg->data.result);
523
- VALUE pair = rb_ary_new_from_args(2, INT2NUM(ctx->index), response);
524
- rb_yield(pair);
525
- cctx->completed++;
526
- } else {
527
- cctx->completed++;
758
+ VALUE pair = build_result_pair(ctx->index, response);
759
+
760
+ if (cctx->stream)
761
+ rb_yield(pair);
762
+ else
763
+ rb_ary_push(cctx->results, pair);
528
764
  }
765
+
766
+ cctx->completed++;
529
767
  if (cctx->target > 0 && cctx->completed >= cctx->target)
530
768
  return 1;
531
769
  }
770
+
771
+ return 0;
772
+ }
773
+
774
+ static int next_pending_index(multi_session_t *session) {
775
+ if (session->pending_pos >= session->pending_count)
776
+ return -1;
777
+
778
+ if (session->pending_indices)
779
+ return session->pending_indices[session->pending_pos++];
780
+
781
+ return session->pending_pos++;
782
+ }
783
+
784
+ static int activate_request(multi_session_t *session, VALUE requests, int idx, int *invalid,
785
+ long timeout_sec) {
786
+ request_ctx_t *ctx = &session->requests[idx];
787
+
788
+ if (invalid[idx] || ctx->done)
789
+ return 0;
790
+
791
+ if (!request_ctx_prepare_easy(ctx))
792
+ return 0;
793
+
794
+ if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx), timeout_sec))
795
+ return 0;
796
+
797
+ CURLMcode mc = curl_multi_add_handle(session->multi, ctx->easy);
798
+ if (mc != CURLM_OK)
799
+ return 0;
800
+
801
+ ctx->active = 1;
802
+ ctx->done = 0;
803
+ session->active_count++;
804
+ return 1;
805
+ }
806
+
807
+ static int fill_slots(multi_session_t *session, VALUE requests, int *invalid, long timeout_sec,
808
+ completion_ctx_t *cctx) {
809
+ while (session->active_count < session->max_connections) {
810
+ int idx = next_pending_index(session);
811
+ if (idx < 0)
812
+ break;
813
+
814
+ request_ctx_t *ctx = &session->requests[idx];
815
+
816
+ if (!activate_request(session, requests, idx, invalid, timeout_sec)) {
817
+ invalid[idx] = 1;
818
+ ctx->done = 1;
819
+ ctx->active = 0;
820
+ if (record_immediate_error(cctx, idx, "Invalid request configuration"))
821
+ return 1;
822
+ }
823
+ }
824
+
532
825
  return 0;
533
826
  }
534
827
 
535
- static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx) {
828
+ static int pending_remaining(multi_session_t *session) {
829
+ return session->pending_pos < session->pending_count;
830
+ }
831
+
832
+ static void prepare_pending(multi_session_t *session, int *indices, int count) {
833
+ session->pending_indices = indices;
834
+ session->pending_count = count;
835
+ session->pending_pos = 0;
836
+ session->active_count = 0;
837
+ session->still_running = 0;
838
+ }
839
+
840
+ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VALUE requests,
841
+ int *invalid, long timeout_sec, int *indices, int indices_count) {
842
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
536
843
  VALUE scheduler = current_fiber_scheduler();
844
+ #endif
845
+ prepare_pending(session, indices, indices_count);
537
846
 
538
- if (scheduler != Qnil) {
539
- curl_multi_perform(session->multi, &session->still_running);
540
- while (session->still_running > 0) {
541
- if (session->cancelled)
542
- break;
847
+ if (fill_slots(session, requests, invalid, timeout_sec, cctx))
848
+ return;
849
+
850
+ curl_multi_perform(session->multi, &session->still_running);
851
+ if (process_completed(session, cctx))
852
+ return;
853
+
854
+ while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
855
+ if (fill_slots(session, requests, invalid, timeout_sec, cctx))
856
+ return;
857
+
858
+ if (session->active_count == 0)
859
+ break;
860
+
861
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
862
+ if (scheduler != Qnil)
543
863
  run_via_fiber_worker(scheduler, poll_without_gvl, session);
544
- if (process_completed(session, cctx))
545
- break;
546
- }
547
- process_completed(session, cctx);
548
- } else {
549
- curl_multi_perform(session->multi, &session->still_running);
550
- while (session->still_running > 0) {
551
- if (session->cancelled)
552
- break;
864
+ else
865
+ #endif
553
866
  rb_thread_call_without_gvl(poll_without_gvl, session, unblock_perform, session);
554
- if (process_completed(session, cctx))
555
- break;
556
- }
557
- process_completed(session, cctx);
867
+
868
+ if (process_completed(session, cctx))
869
+ return;
558
870
  }
871
+
872
+ process_completed(session, cctx);
559
873
  }
560
874
 
561
875
  static int is_default_retryable_curle(CURLcode code) {
@@ -568,9 +882,11 @@ static int is_default_retryable_curle(CURLcode code) {
568
882
  static int should_retry(request_ctx_t *ctx, retry_config_t *rc) {
569
883
  if (ctx->curl_result != CURLE_OK)
570
884
  return is_default_retryable_curle(ctx->curl_result);
885
+
571
886
  for (int i = 0; i < rc->retry_http_count; i++)
572
887
  if (rc->retry_http_codes[i] == (int)ctx->http_status)
573
888
  return 1;
889
+
574
890
  return 0;
575
891
  }
576
892
 
@@ -586,10 +902,11 @@ static void *sleep_without_gvl(void *arg) {
586
902
  return NULL;
587
903
  }
588
904
 
589
- /* FIX #2: Fiber path releases GVL via run_via_fiber_worker */
590
905
  static void retry_delay_sleep(long delay_ms) {
591
906
  if (delay_ms <= 0)
592
907
  return;
908
+
909
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
593
910
  VALUE scheduler = current_fiber_scheduler();
594
911
  if (scheduler != Qnil) {
595
912
  long remaining = delay_ms;
@@ -599,70 +916,111 @@ static void retry_delay_sleep(long delay_ms) {
599
916
  run_via_fiber_worker(scheduler, sleep_without_gvl, &sa);
600
917
  remaining -= chunk;
601
918
  }
602
- } else {
919
+ } else
920
+ #endif
921
+ {
603
922
  sleep_arg_t sa = {.delay_ms = delay_ms};
604
923
  rb_thread_call_without_gvl(sleep_without_gvl, &sa, (rb_unblock_function_t *)0, NULL);
605
924
  }
606
925
  }
607
926
 
608
- static void parse_options(VALUE options, long *timeout, int *max_conn, retry_config_t *retry_cfg) {
609
- *timeout = 30;
610
- *max_conn = 20;
927
+ static void retry_config_init(retry_config_t *retry_cfg) {
611
928
  retry_cfg->max_retries = DEFAULT_RETRIES;
929
+ retry_cfg->retries_explicit = 0;
612
930
  retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
613
931
  retry_cfg->retry_http_codes = NULL;
614
932
  retry_cfg->retry_http_count = 0;
615
- if (NIL_P(options) || !rb_obj_is_kind_of(options, rb_cHash))
933
+ }
934
+
935
+ static long parse_long_option(VALUE options, key_id_t key, const char *name, long min, long max,
936
+ long default_value, int *present) {
937
+ VALUE raw = hash_aref_key(options, key);
938
+ long value;
939
+
940
+ if (present)
941
+ *present = !NIL_P(raw);
942
+ if (NIL_P(raw))
943
+ return default_value;
944
+
945
+ value = NUM2LONG(raw);
946
+ if (value < min || value > max)
947
+ rb_raise(rb_eArgError, "%s must be between %ld and %ld", name, min, max);
948
+
949
+ return value;
950
+ }
951
+
952
+ static int parse_int_option(VALUE options, key_id_t key, const char *name, int min, int max,
953
+ int default_value, int *present) {
954
+ return (int)parse_long_option(options, key, name, min, max, default_value, present);
955
+ }
956
+
957
+ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
958
+ VALUE codes = hash_aref_key(options, KEY_RETRY_CODES);
959
+ long len_long;
960
+ int len;
961
+
962
+ if (NIL_P(codes))
616
963
  return;
617
964
 
618
- VALUE t = rb_hash_aref(options, sym_timeout);
619
- if (!NIL_P(t)) {
620
- long v = NUM2LONG(t);
621
- if (v > MAX_TIMEOUT)
622
- v = MAX_TIMEOUT;
623
- else if (v <= 0)
624
- v = 30;
625
- *timeout = v;
626
- }
627
- VALUE c = rb_hash_aref(options, sym_connections);
628
- if (!NIL_P(c)) {
629
- int v = NUM2INT(c);
630
- if (v > 100)
631
- v = 100;
632
- else if (v <= 0)
633
- v = 20;
634
- *max_conn = v;
635
- }
636
- VALUE r = rb_hash_aref(options, sym_retries);
637
- if (!NIL_P(r)) {
638
- int v = NUM2INT(r);
639
- if (v < 0)
640
- v = 0;
641
- if (v > MAX_RETRIES)
642
- v = MAX_RETRIES;
643
- retry_cfg->max_retries = v;
644
- }
645
- VALUE rd = rb_hash_aref(options, sym_retry_delay);
646
- if (!NIL_P(rd)) {
647
- long v = NUM2LONG(rd);
648
- if (v < 0)
649
- v = 0;
650
- if (v > 30000)
651
- v = 30000;
652
- retry_cfg->retry_delay_ms = v;
653
- }
654
- VALUE rc = rb_hash_aref(options, sym_retry_codes);
655
- if (!NIL_P(rc) && rb_obj_is_kind_of(rc, rb_cArray)) {
656
- int len = (int)RARRAY_LEN(rc);
657
- if (len > 0) {
658
- retry_cfg->retry_http_codes = malloc(sizeof(int) * len);
659
- if (retry_cfg->retry_http_codes) {
660
- retry_cfg->retry_http_count = len;
661
- for (int i = 0; i < len; i++)
662
- retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(rc, i));
663
- }
664
- }
965
+ Check_Type(codes, T_ARRAY);
966
+ len_long = RARRAY_LEN(codes);
967
+ if (len_long > INT_MAX)
968
+ rb_raise(rb_eArgError, "retry_codes is too large");
969
+
970
+ len = (int)len_long;
971
+ for (int i = 0; i < len; i++) {
972
+ int code = NUM2INT(rb_ary_entry(codes, i));
973
+ if (code < 100 || code > 599)
974
+ rb_raise(rb_eArgError, "retry_codes must contain valid HTTP status codes");
665
975
  }
976
+
977
+ if (len == 0)
978
+ return;
979
+
980
+ retry_cfg->retry_http_codes = malloc(sizeof(int) * (size_t)len);
981
+ if (!retry_cfg->retry_http_codes)
982
+ rb_raise(rb_eNoMemError, "failed to allocate retry codes");
983
+
984
+ retry_cfg->retry_http_count = len;
985
+ for (int i = 0; i < len; i++)
986
+ retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(codes, i));
987
+ }
988
+
989
+ static void parse_options(VALUE options, long *timeout, int *max_conn, retry_config_t *retry_cfg) {
990
+ *timeout = 30;
991
+ *max_conn = 20;
992
+ retry_config_init(retry_cfg);
993
+
994
+ if (NIL_P(options))
995
+ return;
996
+
997
+ Check_Type(options, T_HASH);
998
+ *timeout = parse_long_option(options, KEY_TIMEOUT, "timeout", 1, MAX_TIMEOUT, *timeout, NULL);
999
+ *max_conn = parse_int_option(options, KEY_CONNECTIONS, "connections", 1, MAX_CONNECTIONS,
1000
+ *max_conn, NULL);
1001
+ retry_cfg->max_retries = parse_int_option(options, KEY_RETRIES, "retries", 0, MAX_RETRIES,
1002
+ retry_cfg->max_retries, &retry_cfg->retries_explicit);
1003
+ retry_cfg->retry_delay_ms =
1004
+ parse_long_option(options, KEY_RETRY_DELAY, "retry_delay", 0, MAX_RETRY_DELAY_MS,
1005
+ retry_cfg->retry_delay_ms, NULL);
1006
+ parse_retry_codes(options, retry_cfg);
1007
+ }
1008
+
1009
+ static void multi_session_init(multi_session_t *session, CURLM *multi, int count, int max_conn,
1010
+ long timeout_sec) {
1011
+ memset(session, 0, sizeof(*session));
1012
+ session->multi = multi;
1013
+ session->count = count;
1014
+ session->timeout_ms = timeout_sec * 1000;
1015
+ session->max_connections = max_conn;
1016
+ }
1017
+
1018
+ static void multi_session_configure(CURLM *multi, int max_conn) {
1019
+ curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, (long)max_conn);
1020
+ curl_multi_setopt(multi, CURLMOPT_MAX_TOTAL_CONNECTIONS, (long)max_conn);
1021
+ #ifdef CURLPIPE_MULTIPLEX
1022
+ curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
1023
+ #endif
666
1024
  }
667
1025
 
668
1026
  typedef struct {
@@ -673,30 +1031,41 @@ typedef struct {
673
1031
 
674
1032
  static VALUE cleanup_session(VALUE arg) {
675
1033
  cleanup_ctx_t *ctx = (cleanup_ctx_t *)arg;
1034
+
676
1035
  if (ctx->session->requests) {
677
1036
  for (int i = 0; i < ctx->session->count; i++) {
678
- if (ctx->session->requests[i].easy)
1037
+ if (ctx->session->requests[i].easy && ctx->session->requests[i].active)
679
1038
  curl_multi_remove_handle(ctx->session->multi, ctx->session->requests[i].easy);
680
1039
  request_ctx_free(&ctx->session->requests[i]);
681
1040
  }
682
1041
  free(ctx->session->requests);
683
1042
  ctx->session->requests = NULL;
684
1043
  }
1044
+
685
1045
  if (ctx->invalid) {
686
1046
  free(ctx->invalid);
687
1047
  ctx->invalid = NULL;
688
1048
  }
1049
+
689
1050
  if (ctx->session->multi) {
690
1051
  curl_multi_cleanup(ctx->session->multi);
691
1052
  ctx->session->multi = NULL;
692
1053
  }
1054
+
693
1055
  if (ctx->retry_cfg && ctx->retry_cfg->retry_http_codes) {
694
1056
  free(ctx->retry_cfg->retry_http_codes);
695
1057
  ctx->retry_cfg->retry_http_codes = NULL;
696
1058
  }
1059
+
697
1060
  return Qnil;
698
1061
  }
699
1062
 
1063
+ static FAST_CURL_NORETURN void cleanup_and_raise(cleanup_ctx_t *cleanup, VALUE exception,
1064
+ const char *message) {
1065
+ cleanup_session((VALUE)cleanup);
1066
+ rb_raise(exception, "%s", message);
1067
+ }
1068
+
700
1069
  typedef struct {
701
1070
  VALUE requests;
702
1071
  VALUE options;
@@ -715,113 +1084,168 @@ static VALUE internal_execute_body(VALUE arg) {
715
1084
  int *invalid = ea->invalid;
716
1085
  retry_config_t *retry_cfg = ea->retry_cfg;
717
1086
  long timeout_sec = ea->timeout_sec;
718
- int count = session->count, target = ea->target, stream = ea->stream;
1087
+ int count = session->count;
1088
+ int target = ea->target;
1089
+ int stream = ea->stream;
719
1090
 
720
- int valid_requests = 0;
721
- for (int i = 0; i < count; i++) {
722
- VALUE req = rb_ary_entry(requests, i);
1091
+ for (int i = 0; i < count; i++)
723
1092
  request_ctx_init(&session->requests[i], i);
724
- if (!setup_easy_handle(&session->requests[i], req, timeout_sec)) {
725
- session->requests[i].done = 1;
726
- invalid[i] = 1;
727
- continue;
728
- }
729
- CURLMcode mc = curl_multi_add_handle(session->multi, session->requests[i].easy);
730
- if (mc != CURLM_OK) {
731
- session->requests[i].done = 1;
732
- invalid[i] = 1;
733
- continue;
734
- }
735
- valid_requests++;
736
- }
737
- if (valid_requests == 0)
738
- session->still_running = 0;
739
1093
 
740
1094
  completion_ctx_t cctx;
741
- cctx.results = stream ? Qnil : rb_ary_new2(count);
1095
+ cctx.results = stream ? Qnil : ((target > 0) ? rb_ary_new2(target) : rb_ary_new2(count));
742
1096
  cctx.completed = 0;
743
1097
  cctx.target = target;
744
1098
  cctx.stream = stream;
745
- if (!stream) {
1099
+
1100
+ if (!stream && target <= 0) {
746
1101
  for (int i = 0; i < count; i++)
747
1102
  rb_ary_store(cctx.results, i, Qnil);
748
1103
  }
749
1104
 
750
- run_multi_loop(session, &cctx);
1105
+ run_multi_loop(session, &cctx, requests, invalid, timeout_sec, NULL, count);
751
1106
 
752
- if (!stream && retry_cfg->max_retries > 0) {
753
- int prev_all_failed = 0;
1107
+ if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
754
1108
  for (int attempt = 0; attempt < retry_cfg->max_retries; attempt++) {
1109
+ int *retry_indices = malloc(sizeof(int) * (size_t)count);
1110
+ if (!retry_indices)
1111
+ rb_raise(rb_eNoMemError, "failed to allocate retry index array");
1112
+
755
1113
  int retry_count = 0;
756
- int *ri = malloc(sizeof(int) * count);
757
- if (!ri)
758
- break;
759
1114
  for (int i = 0; i < count; i++) {
760
1115
  if (invalid[i] || !session->requests[i].done)
761
1116
  continue;
762
1117
  if (should_retry(&session->requests[i], retry_cfg))
763
- ri[retry_count++] = i;
1118
+ retry_indices[retry_count++] = i;
764
1119
  }
1120
+
765
1121
  if (retry_count == 0) {
766
- free(ri);
767
- break;
768
- }
769
- int done_count = 0;
770
- for (int i = 0; i < count; i++)
771
- if (!invalid[i] && session->requests[i].done)
772
- done_count++;
773
- int all_failed = (retry_count == done_count);
774
- if (all_failed && prev_all_failed) {
775
- free(ri);
1122
+ free(retry_indices);
776
1123
  break;
777
1124
  }
778
- prev_all_failed = all_failed;
1125
+
779
1126
  retry_delay_sleep(retry_cfg->retry_delay_ms);
1127
+
1128
+ int runnable_count = 0;
780
1129
  for (int r = 0; r < retry_count; r++) {
781
- int idx = ri[r];
1130
+ int idx = retry_indices[r];
782
1131
  request_ctx_t *rc = &session->requests[idx];
783
- curl_multi_remove_handle(session->multi, rc->easy);
1132
+
784
1133
  if (!request_ctx_reset_for_retry(rc)) {
785
- rc->done = 1;
786
1134
  invalid[idx] = 1;
787
- continue;
788
- }
789
- VALUE req = rb_ary_entry(requests, idx);
790
- if (!setup_easy_handle(rc, req, timeout_sec)) {
791
1135
  rc->done = 1;
792
- invalid[idx] = 1;
793
1136
  continue;
794
1137
  }
795
- CURLMcode mc = curl_multi_add_handle(session->multi, rc->easy);
796
- if (mc != CURLM_OK) {
797
- rc->done = 1;
798
- invalid[idx] = 1;
799
- }
1138
+
1139
+ retry_indices[runnable_count++] = idx;
800
1140
  }
801
- free(ri);
802
- cctx.completed = 0;
803
- run_multi_loop(session, &cctx);
1141
+
1142
+ if (runnable_count > 0) {
1143
+ cctx.completed = 0;
1144
+ run_multi_loop(session, &cctx, requests, invalid, timeout_sec, retry_indices,
1145
+ runnable_count);
1146
+ }
1147
+
1148
+ free(retry_indices);
804
1149
  }
805
1150
  }
806
1151
 
807
- if (!stream) {
1152
+ if (!stream && target <= 0) {
808
1153
  for (int i = 0; i < count; i++) {
809
1154
  request_ctx_t *rc = &session->requests[i];
810
1155
  VALUE response;
1156
+
811
1157
  if (invalid[i]) {
812
1158
  response = build_error_response("Invalid request configuration");
1159
+ } else if (!rc->done) {
1160
+ response = build_error_response("Request was not completed");
813
1161
  } else if (rc->curl_result == CURLE_OK) {
814
1162
  response = build_response(rc);
815
1163
  } else {
816
1164
  response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
817
1165
  (int)rc->curl_result);
818
1166
  }
819
- rb_ary_store(cctx.results, i, rb_ary_new_from_args(2, INT2NUM(i), response));
1167
+
1168
+ rb_ary_store(cctx.results, i, build_result_pair(i, response));
820
1169
  }
821
1170
  }
1171
+
822
1172
  return stream ? Qnil : cctx.results;
823
1173
  }
824
1174
 
1175
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1176
+ typedef struct {
1177
+ execute_args_t *ea;
1178
+ VALUE scheduler;
1179
+ VALUE blocker;
1180
+ VALUE fiber;
1181
+ VALUE thread;
1182
+ VALUE result;
1183
+ VALUE exception;
1184
+ int state;
1185
+ int finished;
1186
+ } scheduler_execute_ctx_t;
1187
+
1188
+ static VALUE scheduler_execute_thread(void *arg) {
1189
+ scheduler_execute_ctx_t *ctx = (scheduler_execute_ctx_t *)arg;
1190
+
1191
+ ctx->result = rb_protect(internal_execute_body, (VALUE)ctx->ea, &ctx->state);
1192
+ if (ctx->state) {
1193
+ ctx->exception = rb_errinfo();
1194
+ rb_set_errinfo(Qnil);
1195
+ }
1196
+
1197
+ ctx->finished = 1;
1198
+ rb_fiber_scheduler_unblock(ctx->scheduler, ctx->blocker, ctx->fiber);
1199
+ return Qnil;
1200
+ }
1201
+
1202
+ static void cancel_scheduler_execute(scheduler_execute_ctx_t *ctx) {
1203
+ multi_session_t *session = ctx->ea->session;
1204
+
1205
+ session->cancelled = 1;
1206
+ #ifdef HAVE_CURL_MULTI_WAKEUP
1207
+ if (session->multi)
1208
+ curl_multi_wakeup(session->multi);
1209
+ #endif
1210
+ }
1211
+
1212
+ static VALUE scheduler_execute_wait(VALUE arg) {
1213
+ scheduler_execute_ctx_t *ctx = (scheduler_execute_ctx_t *)arg;
1214
+
1215
+ if (!ctx->finished)
1216
+ rb_fiber_scheduler_block(ctx->scheduler, ctx->blocker, Qnil);
1217
+
1218
+ rb_funcall(ctx->thread, rb_intern("join"), 0);
1219
+
1220
+ if (ctx->state)
1221
+ rb_exc_raise(ctx->exception);
1222
+
1223
+ return ctx->result;
1224
+ }
1225
+
1226
+ static VALUE scheduler_execute_ensure(VALUE arg) {
1227
+ scheduler_execute_ctx_t *ctx = (scheduler_execute_ctx_t *)arg;
1228
+
1229
+ if (!NIL_P(ctx->thread)) {
1230
+ if (!ctx->finished)
1231
+ cancel_scheduler_execute(ctx);
1232
+ rb_funcall(ctx->thread, rb_intern("join"), 0);
1233
+ }
1234
+
1235
+ return Qnil;
1236
+ }
1237
+
1238
+ static VALUE execute_with_fiber_scheduler(VALUE arg) {
1239
+ scheduler_execute_ctx_t *ctx = (scheduler_execute_ctx_t *)arg;
1240
+
1241
+ ctx->blocker = rb_obj_alloc(rb_cObject);
1242
+ ctx->fiber = rb_fiber_current();
1243
+ ctx->thread = rb_thread_create(scheduler_execute_thread, ctx);
1244
+
1245
+ return rb_ensure(scheduler_execute_wait, arg, scheduler_execute_ensure, arg);
1246
+ }
1247
+ #endif
1248
+
825
1249
  static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
826
1250
  Check_Type(requests, T_ARRAY);
827
1251
 
@@ -832,55 +1256,46 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
832
1256
  rb_raise(rb_eArgError, "too many requests (%ld), maximum is %d", count_long, MAX_REQUESTS);
833
1257
  if (count_long > INT_MAX)
834
1258
  rb_raise(rb_eArgError, "request count overflows int");
1259
+
835
1260
  int count = (int)count_long;
836
1261
 
1262
+ if (target > 0 && target > count)
1263
+ target = count;
1264
+
837
1265
  long timeout_sec;
838
1266
  int max_conn;
839
1267
  retry_config_t retry_cfg;
840
1268
  parse_options(options, &timeout_sec, &max_conn, &retry_cfg);
841
1269
 
842
1270
  if (stream || target > 0) {
843
- if (retry_cfg.max_retries > 0 && stream)
844
- rb_warn("FastCurl: retries are not supported in stream_execute, ignoring "
845
- "retries option");
846
- if (retry_cfg.max_retries > 0 && target > 0)
847
- rb_warn("FastCurl: retries are not supported in first_execute, ignoring "
848
- "retries option");
1271
+ if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
1272
+ rb_warn(
1273
+ "FastCurl: retries are not supported in stream_execute, ignoring retries option");
1274
+ if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && target > 0)
1275
+ rb_warn(
1276
+ "FastCurl: retries are not supported in first_execute, ignoring retries option");
849
1277
  retry_cfg.max_retries = 0;
850
1278
  }
851
1279
 
852
1280
  multi_session_t session;
853
- session.multi = curl_multi_init();
854
- session.count = count;
855
- session.timeout_ms = timeout_sec * 1000;
856
- session.max_connections = max_conn;
857
- session.cancelled = 0;
858
- session.requests = NULL;
859
-
860
- curl_multi_setopt(session.multi, CURLMOPT_MAXCONNECTS, (long)max_conn);
861
- curl_multi_setopt(session.multi, CURLMOPT_MAX_TOTAL_CONNECTIONS, (long)max_conn);
862
- #ifdef CURLPIPE_MULTIPLEX
863
- curl_multi_setopt(session.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
864
- #endif
1281
+ int *invalid = NULL;
1282
+ multi_session_init(&session, curl_multi_init(), count, max_conn, timeout_sec);
865
1283
 
866
- session.requests = calloc(count, sizeof(request_ctx_t));
867
- if (!session.requests) {
868
- curl_multi_cleanup(session.multi);
869
- if (retry_cfg.retry_http_codes)
870
- free(retry_cfg.retry_http_codes);
871
- rb_raise(rb_eNoMemError, "failed to allocate request contexts");
872
- }
1284
+ cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
873
1285
 
874
- int *invalid = calloc(count, sizeof(int));
875
- if (!invalid) {
876
- free(session.requests);
877
- curl_multi_cleanup(session.multi);
878
- if (retry_cfg.retry_http_codes)
879
- free(retry_cfg.retry_http_codes);
880
- rb_raise(rb_eNoMemError, "failed to allocate tracking array");
881
- }
1286
+ if (!session.multi)
1287
+ cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to initialize curl multi handle");
882
1288
 
883
- cleanup_ctx_t cleanup = {.session = &session, .invalid = invalid, .retry_cfg = &retry_cfg};
1289
+ multi_session_configure(session.multi, max_conn);
1290
+
1291
+ session.requests = calloc((size_t)count, sizeof(request_ctx_t));
1292
+ if (!session.requests)
1293
+ cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to allocate request contexts");
1294
+
1295
+ invalid = calloc((size_t)count, sizeof(int));
1296
+ cleanup.invalid = invalid;
1297
+ if (!invalid)
1298
+ cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to allocate tracking array");
884
1299
  execute_args_t ea = {
885
1300
  .requests = requests,
886
1301
  .options = options,
@@ -891,6 +1306,27 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
891
1306
  .retry_cfg = &retry_cfg,
892
1307
  .timeout_sec = timeout_sec,
893
1308
  };
1309
+
1310
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1311
+ VALUE scheduler = current_fiber_scheduler();
1312
+ if (scheduler != Qnil && !stream) {
1313
+ scheduler_execute_ctx_t scheduler_ctx = {
1314
+ .ea = &ea,
1315
+ .scheduler = scheduler,
1316
+ .blocker = Qnil,
1317
+ .fiber = Qnil,
1318
+ .thread = Qnil,
1319
+ .result = Qnil,
1320
+ .exception = Qnil,
1321
+ .state = 0,
1322
+ .finished = 0,
1323
+ };
1324
+
1325
+ return rb_ensure(execute_with_fiber_scheduler, (VALUE)&scheduler_ctx, cleanup_session,
1326
+ (VALUE)&cleanup);
1327
+ }
1328
+ #endif
1329
+
894
1330
  return rb_ensure(internal_execute_body, (VALUE)&ea, cleanup_session, (VALUE)&cleanup);
895
1331
  }
896
1332
 
@@ -903,64 +1339,39 @@ static VALUE rb_fast_curl_execute(int argc, VALUE *argv, VALUE self) {
903
1339
  static VALUE rb_fast_curl_first_execute(int argc, VALUE *argv, VALUE self) {
904
1340
  VALUE requests, options;
905
1341
  rb_scan_args(argc, argv, "1:", &requests, &options);
1342
+
906
1343
  int count = 1;
907
1344
  if (!NIL_P(options)) {
908
- VALUE c = rb_hash_aref(options, sym_count);
1345
+ Check_Type(options, T_HASH);
1346
+ VALUE c = hash_aref_key(options, KEY_COUNT_OPT);
909
1347
  if (!NIL_P(c))
910
1348
  count = NUM2INT(c);
911
1349
  }
1350
+
1351
+ if (count <= 0)
1352
+ rb_raise(rb_eArgError, "count must be positive");
1353
+
912
1354
  return internal_execute(requests, options, count, 0);
913
1355
  }
914
1356
 
915
1357
  static VALUE rb_fast_curl_stream_execute(int argc, VALUE *argv, VALUE self) {
916
1358
  VALUE requests, options;
917
1359
  rb_scan_args(argc, argv, "1:", &requests, &options);
1360
+
918
1361
  if (!rb_block_given_p())
919
1362
  rb_raise(rb_eArgError, "stream_execute requires a block");
1363
+
920
1364
  return internal_execute(requests, options, -1, 1);
921
1365
  }
922
1366
 
923
1367
  void Init_fast_curl(void) {
924
1368
  curl_global_init(CURL_GLOBAL_ALL);
925
1369
 
926
- id_status = rb_intern("status");
927
- id_headers = rb_intern("headers");
928
- id_body = rb_intern("body");
929
- id_error_code = rb_intern("error_code");
930
- id_url = rb_intern("url");
931
- id_method = rb_intern("method");
932
- id_timeout = rb_intern("timeout");
933
- id_connections = rb_intern("connections");
934
- id_count = rb_intern("count");
935
- id_keys = rb_intern("keys");
936
- id_retries = rb_intern("retries");
937
- id_retry_delay = rb_intern("retry_delay");
938
- id_retry_codes = rb_intern("retry_codes");
939
-
940
- sym_status = ID2SYM(id_status);
941
- rb_gc_register_address(&sym_status);
942
- sym_headers = ID2SYM(id_headers);
943
- rb_gc_register_address(&sym_headers);
944
- sym_body = ID2SYM(id_body);
945
- rb_gc_register_address(&sym_body);
946
- sym_error_code = ID2SYM(id_error_code);
947
- rb_gc_register_address(&sym_error_code);
948
- sym_url = ID2SYM(id_url);
949
- rb_gc_register_address(&sym_url);
950
- sym_method = ID2SYM(id_method);
951
- rb_gc_register_address(&sym_method);
952
- sym_timeout = ID2SYM(id_timeout);
953
- rb_gc_register_address(&sym_timeout);
954
- sym_connections = ID2SYM(id_connections);
955
- rb_gc_register_address(&sym_connections);
956
- sym_count = ID2SYM(id_count);
957
- rb_gc_register_address(&sym_count);
958
- sym_retries = ID2SYM(id_retries);
959
- rb_gc_register_address(&sym_retries);
960
- sym_retry_delay = ID2SYM(id_retry_delay);
961
- rb_gc_register_address(&sym_retry_delay);
962
- sym_retry_codes = ID2SYM(id_retry_codes);
963
- rb_gc_register_address(&sym_retry_codes);
1370
+ for (int i = 0; i < KEY_LAST; i++) {
1371
+ fast_ids[i] = rb_intern(KEY_NAMES[i]);
1372
+ fast_syms[i] = ID2SYM(fast_ids[i]);
1373
+ rb_gc_register_address(&fast_syms[i]);
1374
+ }
964
1375
 
965
1376
  VALUE mFastCurl = rb_define_module("FastCurl");
966
1377