fast_curl 0.5.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 576cbc25d05c1a0857f8f762c3ed3744ad84c65457669c63bd5da62d904e72a0
4
- data.tar.gz: b13fcb38e809a48ac3c85deb14a5ea56dbedb37803cad3cb3c2218a7bf88d483
3
+ metadata.gz: e0c5dac84b62da99fa420dc28437ca3284ddd4f73fd970c7730df3c8a2a7ba5b
4
+ data.tar.gz: 7455385e6a1b1c76467e67909bb252a70a6eac465846aacdf988d9e934fb7d79
5
5
  SHA512:
6
- metadata.gz: c3fed870f48939b5d016c9951d1c2503941496706af5fa0d157c964269a925190187e1b3378dcd8a43dea5c7a7d4c56bbf8edf0923f325fd74323b0166d97361
7
- data.tar.gz: 6f13f0839354222ed5ff8ede5acf39137904226ddad7f860b8b08e3f2b3dcff6fda6b65d21ed8324673f4fda1cc24e8cf51b13a32a6681740bc012507e36072c
6
+ metadata.gz: 5f5b3c2c3797436d23ec37e9f2997c340a5e50b05a4ef9dc82a699093b01fc7a3097c720e14adba1fe6ad3e132e29d90893e01c4bd8a81fbdde3043b9067a727
7
+ data.tar.gz: 5a990531123a9bd72ccc082a5076a5dd1f6eed90f455e92c77c336be33ed964f3ad9a6cef3d59e6322cf378223399505edcaa6d27dd7a0619d71f5c600870bba
data/README.md CHANGED
@@ -278,27 +278,27 @@ grows linearly while parallel clients stay near ~1s plus network overhead.
278
278
 
279
279
  | Scenario | Net::HTTP sequential | fast_curl (thread) | fast_curl (fiber/Async) | Async::HTTP::Internet |
280
280
  |---------------------------------|---------------------:|-------------------:|------------------------:|----------------------:|
281
- | 4 requests × 1s, conn=4 | 8.27s | 2.36s | 2.13s | 2.56s |
282
- | 10 requests × 1s, conn=10 | 20.92s | 3.49s | 5.23s | 3.83s |
283
- | 20 requests × 1s, conn=5 | 42.56s | 2.94s | 2.90s | 12.14s |
284
- | 200 requests × 1s, conn=20 | — | 22.19s | 21.77s | 23.59s |
281
+ | 4 requests × 1s, conn=4 | 5.82s | 1.68s | 1.65s | 1.93s |
282
+ | 10 requests × 1s, conn=10 | 17.51s | 1.70s | 1.68s | 1.89s |
283
+ | 20 requests × 1s, conn=5 | 34.66s | 5.69s | 6.71s | 5.79s |
284
+ | 200 requests × 1s, conn=20 | — | 14.29s | 14.29s | 7.27s |
285
285
 
286
286
  ### Speedup vs Net::HTTP (median)
287
287
 
288
288
  | Scenario | fast_curl (thread) | fast_curl (fiber) | Async::HTTP |
289
289
  |-----------------------------------|-------------------:|------------------:|------------:|
290
- | 4 requests × 1s | **3.5x** | 3.9x | 3.2x |
291
- | 10 requests × 1s | **6.0x** | 4.0x | 5.5x |
292
- | 20 requests × 1s (queued, conn=5) | **14.5x** | 14.7x | 3.5x |
290
+ | 4 requests × 1s | **3.5x** | 3.5x | 3.0x |
291
+ | 10 requests × 1s | **10.3x** | 10.4x | 9.2x |
292
+ | 20 requests × 1s (queued, conn=5) | **6.1x** | 5.2x | 6.0x |
293
293
 
294
294
  ### Memory & allocations per request batch (lower is better)
295
295
 
296
296
  | Scenario | fast_curl (thread) allocated | fast_curl (fiber) allocated | Async::HTTP allocated |
297
297
  |---------------------------------|-----------------------------:|----------------------------:|----------------------:|
298
- | 4 requests × 1s | **278 obj** | 350 obj | 2,433 obj |
299
- | 10 requests × 1s | **490 obj** | 756 obj | 4,763 obj |
300
- | 20 requests × 1s, conn=5 | **621 obj** | 750 obj | 8,536 obj |
301
- | 200 requests × 1s, conn=20 | **5,188 obj** | 5,642 obj | 78,203 obj |
298
+ | 4 requests × 1s | **125 obj** | 179 obj | 1,921 obj |
299
+ | 10 requests × 1s | **281 obj** | 335 obj | 3,344 obj |
300
+ | 20 requests × 1s, conn=5 | **541 obj** | 595 obj | 5,898 obj |
301
+ | 200 requests × 1s, conn=20 | **5,222 obj** | 5,275 obj | 50,534 obj |
302
302
 
303
303
  Ruby heap delta stays near zero across all scenarios for fast_curl — most allocation
304
304
  happens in C, not on the Ruby heap.
@@ -307,7 +307,7 @@ happens in C, not on the Ruby heap.
307
307
 
308
308
  | Scenario | Time |
309
309
  |--------------------------------------------------------------|------:|
310
- | 4 mixed requests (404, 500, DNS fail, 30s delay), timeout=2s | 4.02s |
310
+ | 4 mixed requests (404, 500, DNS fail, 30s delay), timeout=2s | 0.68s |
311
311
 
312
312
  Bounded by `timeout=2s` rather than by the slow request.
313
313
 
@@ -20,13 +20,16 @@
20
20
  #include <strings.h>
21
21
  #include <time.h>
22
22
 
23
+ void Init_fast_curl(void);
24
+
23
25
  #if defined(__GNUC__) || defined(__clang__)
24
26
  #define FAST_CURL_NORETURN __attribute__((noreturn))
25
27
  #else
26
28
  #define FAST_CURL_NORETURN
27
29
  #endif
28
30
 
29
- #define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
31
+ #define MAX_RESPONSE_SIZE ((size_t)100 * 1024 * 1024)
32
+ #define MAX_RESPONSE_HEADER_SIZE ((size_t)1024 * 1024)
30
33
  #define MAX_REDIRECTS 5
31
34
  #define MAX_TIMEOUT 300
32
35
  #define MAX_RETRIES 10
@@ -258,29 +261,63 @@ typedef struct {
258
261
 
259
262
  typedef struct {
260
263
  header_entry_t *entries;
261
- int count;
262
- int cap;
264
+ size_t count;
265
+ size_t cap;
266
+ size_t total_bytes;
263
267
  } header_list_t;
264
268
 
265
269
  static inline void header_list_init(header_list_t *h) {
266
270
  h->entries = NULL;
267
271
  h->count = 0;
268
272
  h->cap = 0;
273
+ h->total_bytes = 0;
269
274
  }
270
275
 
271
276
  static void header_list_free(header_list_t *h) {
272
- for (int i = 0; i < h->count; i++)
277
+ for (size_t i = 0; i < h->count; i++)
273
278
  free(h->entries[i].str);
274
279
  free(h->entries);
275
280
  h->entries = NULL;
276
281
  h->count = 0;
277
282
  h->cap = 0;
283
+ h->total_bytes = 0;
278
284
  }
279
285
 
280
286
  static void header_list_reset(header_list_t *h) {
281
- for (int i = 0; i < h->count; i++)
287
+ for (size_t i = 0; i < h->count; i++)
282
288
  free(h->entries[i].str);
283
289
  h->count = 0;
290
+ h->total_bytes = 0;
291
+ }
292
+
293
+ static int header_list_add_bytes(header_list_t *h, size_t total) {
294
+ if (h->total_bytes > MAX_RESPONSE_HEADER_SIZE ||
295
+ total > MAX_RESPONSE_HEADER_SIZE - h->total_bytes)
296
+ return 0;
297
+
298
+ h->total_bytes += total;
299
+ return 1;
300
+ }
301
+
302
+ static int header_list_reserve(header_list_t *h) {
303
+ if (h->count < h->cap)
304
+ return 1;
305
+
306
+ size_t new_cap = h->cap ? h->cap : INITIAL_HEADER_CAP;
307
+ if (h->cap && h->cap > SIZE_MAX / 2)
308
+ return 0;
309
+ if (h->cap)
310
+ new_cap = h->cap * 2;
311
+ if (new_cap > SIZE_MAX / sizeof(*h->entries))
312
+ return 0;
313
+
314
+ header_entry_t *entries = realloc(h->entries, sizeof(*h->entries) * new_cap);
315
+ if (!entries)
316
+ return 0;
317
+
318
+ h->entries = entries;
319
+ h->cap = new_cap;
320
+ return 1;
284
321
  }
285
322
 
286
323
  static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
@@ -294,22 +331,18 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
294
331
  while (stripped > 0 && (ptr[stripped - 1] == '\r' || ptr[stripped - 1] == '\n'))
295
332
  stripped--;
296
333
 
297
- if (stripped >= 5 && memcmp(ptr, "HTTP/", 5) == 0) {
334
+ int status_line = stripped >= 5 && memcmp(ptr, "HTTP/", 5) == 0;
335
+ if (status_line)
298
336
  header_list_reset(h);
299
- return total;
300
- }
301
337
 
302
- if (stripped == 0)
338
+ if (!header_list_add_bytes(h, total))
339
+ return 0;
340
+
341
+ if (status_line || stripped == 0)
303
342
  return total;
304
343
 
305
- if (h->count >= h->cap) {
306
- int new_cap = (h->cap == 0) ? INITIAL_HEADER_CAP : h->cap * 2;
307
- header_entry_t *ne = realloc(h->entries, sizeof(header_entry_t) * new_cap);
308
- if (!ne)
309
- return 0;
310
- h->entries = ne;
311
- h->cap = new_cap;
312
- }
344
+ if (!header_list_reserve(h))
345
+ return 0;
313
346
 
314
347
  char *entry = malloc(stripped + 1);
315
348
  if (!entry)
@@ -415,7 +448,7 @@ typedef struct {
415
448
  int still_running;
416
449
  long timeout_ms;
417
450
  int max_connections;
418
- volatile int cancelled;
451
+ int cancelled;
419
452
 
420
453
  int active_count;
421
454
  int pending_pos;
@@ -423,12 +456,21 @@ typedef struct {
423
456
  int *pending_indices;
424
457
  } multi_session_t;
425
458
 
459
+ static int session_cancelled(const multi_session_t *session) {
460
+ return __atomic_load_n(&session->cancelled, __ATOMIC_ACQUIRE);
461
+ }
462
+
463
+ static void cancel_session(multi_session_t *session) {
464
+ __atomic_store_n(&session->cancelled, 1, __ATOMIC_RELEASE);
465
+ }
466
+
426
467
  typedef struct {
427
468
  int max_retries;
428
469
  int retries_explicit;
429
470
  long retry_delay_ms;
430
471
  int *retry_http_codes;
431
472
  int retry_http_count;
473
+ VALUE retry_code_values;
432
474
  int retry_non_idempotent;
433
475
  } retry_config_t;
434
476
 
@@ -571,7 +613,7 @@ static VALUE build_response(request_ctx_t *ctx) {
571
613
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
572
614
 
573
615
  VALUE headers_hash = new_headers_hash();
574
- for (int i = 0; i < ctx->headers.count; i++) {
616
+ for (size_t i = 0; i < ctx->headers.count; i++) {
575
617
  const char *hdr = ctx->headers.entries[i].str;
576
618
  size_t hdr_len = ctx->headers.entries[i].len;
577
619
  const char *colon = memchr(hdr, ':', hdr_len);
@@ -598,7 +640,7 @@ static VALUE build_response(request_ctx_t *ctx) {
598
640
  }
599
641
 
600
642
  VALUE body_str =
601
- ctx->body.data ? rb_str_new(ctx->body.data, ctx->body.len) : rb_str_new_cstr("");
643
+ ctx->body.data ? rb_str_new(ctx->body.data, (long)ctx->body.len) : rb_str_new_cstr("");
602
644
 
603
645
  VALUE result = rb_hash_new();
604
646
  rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
@@ -643,7 +685,7 @@ static int is_valid_url(const char *url) {
643
685
  return 0;
644
686
  if (strncmp(url, "https://", 8) == 0)
645
687
  return 1;
646
- if (len >= 7 && strncmp(url, "http://", 7) == 0)
688
+ if (strncmp(url, "http://", 7) == 0)
647
689
  return 1;
648
690
  return 0;
649
691
  }
@@ -886,7 +928,7 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_op
886
928
  SET_SETUP_ERROR(ctx, "Missing :url", 0);
887
929
  return 0;
888
930
  }
889
- if (!RB_TYPE_P(url, T_STRING) || memchr(RSTRING_PTR(url), '\0', RSTRING_LEN(url))) {
931
+ if (!RB_TYPE_P(url, T_STRING) || memchr(RSTRING_PTR(url), '\0', (size_t)RSTRING_LEN(url))) {
890
932
  SET_SETUP_ERROR(ctx, "Invalid URL", 1);
891
933
  return 0;
892
934
  }
@@ -934,6 +976,67 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_op
934
976
  return 1;
935
977
  }
936
978
 
979
+ typedef struct {
980
+ request_ctx_t request;
981
+ VALUE value;
982
+ const request_options_t *options;
983
+ long index;
984
+ int fatal;
985
+ char message[128];
986
+ } request_probe_t;
987
+
988
+ static VALUE request_probe_body(VALUE arg) {
989
+ request_probe_t *probe = (request_probe_t *)(uintptr_t)arg;
990
+ request_ctx_init(&probe->request, probe->index);
991
+ if (!request_ctx_prepare_easy(&probe->request))
992
+ return Qnil;
993
+
994
+ setup_easy_handle(&probe->request, probe->value, probe->options);
995
+ probe->fatal = probe->request.setup_error_fatal;
996
+ if (probe->fatal && probe->request.setup_error)
997
+ snprintf(probe->message, sizeof(probe->message), "%s (request %ld)",
998
+ probe->request.setup_error, probe->index);
999
+ return Qnil;
1000
+ }
1001
+
1002
+ static VALUE request_probe_ensure(VALUE arg) {
1003
+ request_probe_t *probe = (request_probe_t *)(uintptr_t)arg;
1004
+ request_ctx_free(&probe->request);
1005
+ return Qnil;
1006
+ }
1007
+
1008
+ static int validate_request_probe(VALUE request, long index, const request_options_t *options,
1009
+ char message[128]) {
1010
+ request_probe_t probe;
1011
+ memset(&probe, 0, sizeof(probe));
1012
+ probe.value = request;
1013
+ probe.options = options;
1014
+ probe.index = index;
1015
+ rb_ensure(request_probe_body, (VALUE)(uintptr_t)&probe, request_probe_ensure,
1016
+ (VALUE)(uintptr_t)&probe);
1017
+ if (probe.fatal)
1018
+ memcpy(message, probe.message, sizeof(probe.message));
1019
+ RB_GC_GUARD(request);
1020
+ return probe.fatal;
1021
+ }
1022
+
1023
+ static void validate_execute_requests(VALUE requests, int count, int target, VALUE options,
1024
+ const request_options_t *opts) {
1025
+ if (target > 0 && !NIL_P(options)) {
1026
+ VALUE accept = hash_aref_key(options, KEY_ACCEPT);
1027
+ if (!NIL_P(accept) && !rb_respond_to(accept, rb_intern("call")))
1028
+ rb_raise(rb_eArgError, "accept must respond to #call");
1029
+ }
1030
+
1031
+ for (int i = 0; i < count; i++) {
1032
+ char message[128];
1033
+ if (validate_request_probe(rb_ary_entry(requests, i), i, opts, message)) {
1034
+ VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
1035
+ rb_raise(klass, "%s", message);
1036
+ }
1037
+ }
1038
+ }
1039
+
937
1040
  /* Poll until something actually completes, or the slice budget expires.
938
1041
  Previously this returned after every single 50ms poll, which under a fiber
939
1042
  scheduler meant a fresh OS thread twenty times a second. Cancellation still
@@ -943,7 +1046,7 @@ static void *poll_without_gvl(void *arg) {
943
1046
  long long started = fast_now_ms();
944
1047
  int before = s->still_running;
945
1048
 
946
- while (!s->cancelled) {
1049
+ while (!session_cancelled(s)) {
947
1050
  int numfds = 0;
948
1051
  curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
949
1052
  curl_multi_perform(s->multi, &s->still_running);
@@ -958,7 +1061,7 @@ static void *poll_without_gvl(void *arg) {
958
1061
 
959
1062
  static void unblock_perform(void *arg) {
960
1063
  multi_session_t *s = (multi_session_t *)arg;
961
- s->cancelled = 1;
1064
+ cancel_session(s);
962
1065
  #ifdef HAVE_CURL_MULTI_WAKEUP
963
1066
  curl_multi_wakeup(s->multi);
964
1067
  #endif
@@ -1144,7 +1247,8 @@ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VAL
1144
1247
  if (process_completed(session, cctx))
1145
1248
  return;
1146
1249
 
1147
- while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
1250
+ while (!session_cancelled(session) &&
1251
+ (session->active_count > 0 || pending_remaining(session))) {
1148
1252
  if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1149
1253
  break;
1150
1254
 
@@ -1229,6 +1333,7 @@ static void retry_config_init(retry_config_t *retry_cfg) {
1229
1333
  retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
1230
1334
  retry_cfg->retry_http_codes = NULL;
1231
1335
  retry_cfg->retry_http_count = 0;
1336
+ retry_cfg->retry_code_values = Qnil;
1232
1337
  retry_cfg->retry_non_idempotent = 0;
1233
1338
  }
1234
1339
 
@@ -1287,22 +1392,33 @@ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
1287
1392
  rb_raise(rb_eArgError, "retry_codes is too large");
1288
1393
 
1289
1394
  len = (int)len_long;
1395
+ VALUE normalized = rb_ary_new_capa(len);
1290
1396
  for (int i = 0; i < len; i++) {
1291
1397
  int code = NUM2INT(rb_ary_entry(codes, i));
1292
1398
  if (code < 100 || code > 599)
1293
1399
  rb_raise(rb_eArgError, "retry_codes must contain valid HTTP status codes");
1400
+ rb_ary_push(normalized, INT2NUM(code));
1294
1401
  }
1295
1402
 
1296
- if (len == 0)
1403
+ retry_cfg->retry_code_values = normalized;
1404
+ retry_cfg->retry_http_count = len;
1405
+ RB_GC_GUARD(codes);
1406
+ }
1407
+
1408
+ static void materialize_retry_codes(retry_config_t *retry_cfg) {
1409
+ if (retry_cfg->retry_http_count == 0)
1297
1410
  return;
1298
1411
 
1299
- retry_cfg->retry_http_codes = malloc(sizeof(int) * (size_t)len);
1412
+ size_t count = (size_t)retry_cfg->retry_http_count;
1413
+ if (count > SIZE_MAX / sizeof(*retry_cfg->retry_http_codes))
1414
+ rb_raise(rb_eArgError, "retry_codes is too large");
1415
+
1416
+ retry_cfg->retry_http_codes = malloc(sizeof(*retry_cfg->retry_http_codes) * count);
1300
1417
  if (!retry_cfg->retry_http_codes)
1301
1418
  rb_raise(rb_eNoMemError, "failed to allocate retry codes");
1302
1419
 
1303
- retry_cfg->retry_http_count = len;
1304
- for (int i = 0; i < len; i++)
1305
- retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(codes, i));
1420
+ for (int i = 0; i < retry_cfg->retry_http_count; i++)
1421
+ retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(retry_cfg->retry_code_values, i));
1306
1422
  }
1307
1423
 
1308
1424
  static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
@@ -1347,7 +1463,17 @@ static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
1347
1463
  retry_cfg->retry_delay_ms =
1348
1464
  parse_long_option(options, KEY_RETRY_DELAY, "retry_delay", 0, MAX_RETRY_DELAY_MS,
1349
1465
  retry_cfg->retry_delay_ms, NULL);
1350
- parse_retry_codes(options, retry_cfg);
1466
+ }
1467
+
1468
+ static void configure_retry_mode(retry_config_t *retry_cfg, int target, int stream) {
1469
+ if (!stream && target <= 0)
1470
+ return;
1471
+
1472
+ if (retry_cfg->retries_explicit && retry_cfg->max_retries > 0 && stream)
1473
+ rb_warn("FastCurl: retries are not supported in stream_execute, ignoring retries option");
1474
+ if (retry_cfg->retries_explicit && retry_cfg->max_retries > 0 && target > 0)
1475
+ rb_warn("FastCurl: retries are not supported in first_execute, ignoring retries option");
1476
+ retry_cfg->max_retries = 0;
1351
1477
  }
1352
1478
 
1353
1479
  static void multi_session_init(multi_session_t *session, CURLM *multi, int count, int max_conn,
@@ -1410,6 +1536,27 @@ static FAST_CURL_NORETURN void cleanup_and_raise(cleanup_ctx_t *cleanup, VALUE e
1410
1536
  rb_raise(exception, "%s", message);
1411
1537
  }
1412
1538
 
1539
+ static void prepare_execute_session(multi_session_t *session, int **invalid, cleanup_ctx_t *cleanup,
1540
+ retry_config_t *retry_cfg, int count, int max_conn,
1541
+ long timeout_sec) {
1542
+ multi_session_init(session, curl_multi_init(), count, max_conn, timeout_sec);
1543
+ cleanup->session = session;
1544
+ cleanup->invalid = NULL;
1545
+ cleanup->retry_cfg = retry_cfg;
1546
+ if (!session->multi)
1547
+ cleanup_and_raise(cleanup, rb_eNoMemError, "failed to initialize curl multi handle");
1548
+
1549
+ multi_session_configure(session->multi, max_conn);
1550
+ session->requests = calloc((size_t)count, sizeof(*session->requests));
1551
+ if (!session->requests)
1552
+ cleanup_and_raise(cleanup, rb_eNoMemError, "failed to allocate request contexts");
1553
+
1554
+ *invalid = calloc((size_t)count, sizeof(**invalid));
1555
+ cleanup->invalid = *invalid;
1556
+ if (!*invalid)
1557
+ cleanup_and_raise(cleanup, rb_eNoMemError, "failed to allocate tracking array");
1558
+ }
1559
+
1413
1560
  typedef struct {
1414
1561
  VALUE requests;
1415
1562
  VALUE options;
@@ -1421,130 +1568,144 @@ typedef struct {
1421
1568
  request_options_t *opts;
1422
1569
  } execute_args_t;
1423
1570
 
1424
- static VALUE internal_execute_body(VALUE arg) {
1425
- execute_args_t *ea = (execute_args_t *)arg;
1426
- VALUE requests = ea->requests;
1571
+ static void completion_context_init(completion_ctx_t *cctx, const execute_args_t *ea, int count) {
1572
+ cctx->results =
1573
+ ea->stream ? Qnil : (ea->target > 0 ? rb_ary_new2(ea->target) : rb_ary_new2(count));
1574
+ cctx->accept =
1575
+ ea->target > 0 && !NIL_P(ea->options) ? hash_aref_key(ea->options, KEY_ACCEPT) : Qnil;
1576
+ cctx->completed = 0;
1577
+ cctx->target = ea->target;
1578
+ cctx->stream = ea->stream;
1579
+
1580
+ if (!NIL_P(cctx->accept) && !rb_respond_to(cctx->accept, rb_intern("call")))
1581
+ rb_raise(rb_eArgError, "accept must respond to #call");
1582
+
1583
+ if (!ea->stream && ea->target <= 0) {
1584
+ for (int i = 0; i < count; i++)
1585
+ rb_ary_store(cctx->results, i, Qnil);
1586
+ }
1587
+ }
1588
+
1589
+ static int retry_one_attempt(execute_args_t *ea, completion_ctx_t *cctx, int *deadline_hit,
1590
+ int attempt) {
1427
1591
  multi_session_t *session = ea->session;
1428
- int *invalid = ea->invalid;
1429
- retry_config_t *retry_cfg = ea->retry_cfg;
1430
- request_options_t *opts = ea->opts;
1431
- int deadline_hit = 0;
1432
1592
  int count = session->count;
1433
- int target = ea->target;
1434
- int stream = ea->stream;
1593
+ request_options_t *opts = ea->opts;
1594
+ retry_config_t *retry_cfg = ea->retry_cfg;
1435
1595
 
1436
- for (int i = 0; i < count; i++)
1437
- request_ctx_init(&session->requests[i], i);
1596
+ if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
1597
+ *deadline_hit = 1;
1598
+ return 0;
1599
+ }
1438
1600
 
1439
- completion_ctx_t cctx;
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;
1442
- cctx.completed = 0;
1443
- cctx.target = target;
1444
- cctx.stream = stream;
1601
+ int *retry_indices = ALLOCA_N(int, count);
1602
+ int retry_count = 0;
1603
+ for (int i = 0; i < count; i++) {
1604
+ if (!ea->invalid[i] && session->requests[i].done &&
1605
+ should_retry(&session->requests[i], retry_cfg))
1606
+ retry_indices[retry_count++] = i;
1607
+ }
1445
1608
 
1446
- if (!NIL_P(cctx.accept) && !rb_respond_to(cctx.accept, rb_intern("call")))
1447
- rb_raise(rb_eArgError, "accept must respond to #call");
1609
+ if (retry_count == 0)
1610
+ return 0;
1448
1611
 
1449
- if (!stream && target <= 0) {
1450
- for (int i = 0; i < count; i++)
1451
- rb_ary_store(cctx.results, i, Qnil);
1612
+ long backoff = retry_backoff_ms(retry_cfg, attempt);
1613
+ if (opts->deadline_ms > 0) {
1614
+ long long left = opts->deadline_ms - fast_now_ms();
1615
+ if (left <= 0) {
1616
+ *deadline_hit = 1;
1617
+ return 0;
1618
+ }
1619
+ if (backoff > (long)left)
1620
+ backoff = (long)left;
1452
1621
  }
1622
+ retry_delay_sleep(backoff);
1453
1623
 
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);
1624
+ int runnable_count = 0;
1625
+ for (int r = 0; r < retry_count; r++) {
1626
+ int idx = retry_indices[r];
1627
+ request_ctx_t *request = &session->requests[idx];
1628
+ if (!request_ctx_reset_for_retry(request)) {
1629
+ ea->invalid[idx] = 1;
1630
+ request->done = 1;
1631
+ request->setup_error = "Failed to allocate a curl handle for the retry";
1632
+ continue;
1633
+ }
1458
1634
 
1459
- if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
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
- }
1635
+ retry_indices[runnable_count++] = idx;
1636
+ }
1465
1637
 
1466
- int *retry_indices = malloc(sizeof(int) * (size_t)count);
1467
- if (!retry_indices)
1468
- rb_raise(rb_eNoMemError, "failed to allocate retry index array");
1638
+ if (runnable_count > 0) {
1639
+ cctx->completed = 0;
1640
+ run_multi_loop(session, cctx, ea->requests, ea->invalid, opts, retry_indices,
1641
+ runnable_count);
1642
+ }
1643
+ return 1;
1644
+ }
1469
1645
 
1470
- int retry_count = 0;
1471
- for (int i = 0; i < count; i++) {
1472
- if (invalid[i] || !session->requests[i].done)
1473
- continue;
1474
- if (should_retry(&session->requests[i], retry_cfg))
1475
- retry_indices[retry_count++] = i;
1476
- }
1646
+ static int retry_completed_requests(execute_args_t *ea, completion_ctx_t *cctx) {
1647
+ int deadline_hit = 0;
1648
+ for (int attempt = 0; attempt < ea->retry_cfg->max_retries; attempt++) {
1649
+ if (!retry_one_attempt(ea, cctx, &deadline_hit, attempt))
1650
+ break;
1651
+ }
1652
+ return deadline_hit;
1653
+ }
1477
1654
 
1478
- if (retry_count == 0) {
1479
- free(retry_indices);
1480
- break;
1481
- }
1655
+ static void store_execution_results(execute_args_t *ea, completion_ctx_t *cctx, int deadline_hit) {
1656
+ multi_session_t *session = ea->session;
1657
+ request_options_t *opts = ea->opts;
1658
+ for (int i = 0; i < session->count; i++) {
1659
+ request_ctx_t *request = &session->requests[i];
1660
+ VALUE response;
1661
+
1662
+ if (ea->invalid[i]) {
1663
+ response = build_error_response(request->setup_error ? request->setup_error
1664
+ : "Invalid request configuration",
1665
+ ERR_INVALID_REQUEST, request->attempts);
1666
+ } else if (!request->done) {
1667
+ response = deadline_hit || (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1668
+ ? build_error_response("Total timeout exceeded", ERR_DEADLINE,
1669
+ request->attempts)
1670
+ : build_error_response("Request was not completed", ERR_NOT_COMPLETED,
1671
+ request->attempts);
1672
+ } else if (request->curl_result == CURLE_OK) {
1673
+ response = build_response(request);
1674
+ } else {
1675
+ response = build_error_response_with_code(curl_easy_strerror(request->curl_result),
1676
+ (int)request->curl_result, request->attempts);
1677
+ }
1482
1678
 
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);
1679
+ rb_ary_store(cctx->results, i, build_result_pair(i, response));
1680
+ }
1681
+ }
1495
1682
 
1496
- int runnable_count = 0;
1497
- for (int r = 0; r < retry_count; r++) {
1498
- int idx = retry_indices[r];
1499
- request_ctx_t *rc = &session->requests[idx];
1683
+ static VALUE internal_execute_body(VALUE arg) {
1684
+ execute_args_t *ea = (execute_args_t *)arg;
1685
+ multi_session_t *session = ea->session;
1686
+ retry_config_t *retry_cfg = ea->retry_cfg;
1687
+ request_options_t *opts = ea->opts;
1688
+ int count = session->count;
1500
1689
 
1501
- if (!request_ctx_reset_for_retry(rc)) {
1502
- invalid[idx] = 1;
1503
- rc->done = 1;
1504
- rc->setup_error = "Failed to allocate a curl handle for the retry";
1505
- continue;
1506
- }
1690
+ for (int i = 0; i < count; i++)
1691
+ request_ctx_init(&session->requests[i], i);
1507
1692
 
1508
- retry_indices[runnable_count++] = idx;
1509
- }
1693
+ completion_ctx_t cctx;
1694
+ completion_context_init(&cctx, ea, count);
1510
1695
 
1511
- if (runnable_count > 0) {
1512
- cctx.completed = 0;
1513
- run_multi_loop(session, &cctx, requests, invalid, opts, retry_indices,
1514
- runnable_count);
1515
- }
1696
+ if (opts->total_timeout_ms > 0)
1697
+ opts->deadline_ms = fast_now_ms() + opts->total_timeout_ms;
1516
1698
 
1517
- free(retry_indices);
1518
- }
1519
- }
1699
+ run_multi_loop(session, &cctx, ea->requests, ea->invalid, opts, NULL, count);
1520
1700
 
1521
- if (!stream && target <= 0) {
1522
- for (int i = 0; i < count; i++) {
1523
- request_ctx_t *rc = &session->requests[i];
1524
- VALUE response;
1525
-
1526
- if (invalid[i]) {
1527
- response = build_error_response(rc->setup_error ? rc->setup_error
1528
- : "Invalid request configuration",
1529
- ERR_INVALID_REQUEST, rc->attempts);
1530
- } else if (!rc->done) {
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);
1536
- } else if (rc->curl_result == CURLE_OK) {
1537
- response = build_response(rc);
1538
- } else {
1539
- response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
1540
- (int)rc->curl_result, rc->attempts);
1541
- }
1701
+ int deadline_hit = 0;
1702
+ if (!ea->stream && ea->target <= 0 && retry_cfg->max_retries > 0)
1703
+ deadline_hit = retry_completed_requests(ea, &cctx);
1542
1704
 
1543
- rb_ary_store(cctx.results, i, build_result_pair(i, response));
1544
- }
1545
- }
1705
+ if (!ea->stream && ea->target <= 0)
1706
+ store_execution_results(ea, &cctx, deadline_hit);
1546
1707
 
1547
- return stream ? Qnil : cctx.results;
1708
+ return ea->stream ? Qnil : cctx.results;
1548
1709
  }
1549
1710
 
1550
1711
  typedef enum {
@@ -1777,7 +1938,7 @@ static void *lazy_poll_without_gvl(void *arg) {
1777
1938
  long long started = fast_now_ms();
1778
1939
  int before = session->still_running;
1779
1940
 
1780
- while (!session->cancelled) {
1941
+ while (!session_cancelled(session)) {
1781
1942
  int numfds = 0;
1782
1943
  curl_multi_poll(session->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
1783
1944
  curl_multi_perform(session->multi, &session->still_running);
@@ -1962,6 +2123,49 @@ static VALUE cleanup_lazy_execute(VALUE arg) {
1962
2123
  return Qnil;
1963
2124
  }
1964
2125
 
2126
+ static void initialize_lazy_completion(lazy_execute_ctx_t *ctx, VALUE requests, VALUE options,
2127
+ int target, int stream, int max_conn) {
2128
+ ctx->capacity = max_conn + ctx->buffer_size;
2129
+ ctx->source = requests;
2130
+ ctx->anchors = rb_ary_new2(ctx->capacity);
2131
+ ctx->next_index = 0;
2132
+
2133
+ ctx->cctx.results = stream ? Qnil : rb_ary_new();
2134
+ ctx->cctx.accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
2135
+ ctx->cctx.completed = 0;
2136
+ ctx->cctx.target = target;
2137
+ ctx->cctx.stream = stream;
2138
+
2139
+ if (!NIL_P(ctx->cctx.accept) && !rb_respond_to(ctx->cctx.accept, rb_intern("call")))
2140
+ rb_raise(rb_eArgError, "accept must respond to #call");
2141
+ }
2142
+
2143
+ static void allocate_lazy_state(lazy_execute_ctx_t *ctx, int max_conn) {
2144
+ materialize_retry_codes(&ctx->retry_cfg);
2145
+ multi_session_init(&ctx->session, curl_multi_init(), ctx->capacity, max_conn,
2146
+ ctx->opts.timeout_sec);
2147
+ if (!ctx->session.multi) {
2148
+ cleanup_lazy_execute((VALUE)ctx);
2149
+ rb_raise(rb_eNoMemError, "failed to initialize curl multi handle");
2150
+ }
2151
+ multi_session_configure(ctx->session.multi, max_conn);
2152
+
2153
+ ctx->session.requests = calloc((size_t)ctx->capacity, sizeof(*ctx->session.requests));
2154
+ ctx->states = calloc((size_t)ctx->capacity, sizeof(*ctx->states));
2155
+ ctx->free_slots = malloc(sizeof(*ctx->free_slots) * (size_t)ctx->capacity);
2156
+ ctx->pending_slots = malloc(sizeof(*ctx->pending_slots) * (size_t)ctx->capacity);
2157
+ ctx->retry_at_ms = calloc((size_t)ctx->capacity, sizeof(*ctx->retry_at_ms));
2158
+
2159
+ if (!ctx->session.requests || !ctx->states || !ctx->free_slots || !ctx->pending_slots ||
2160
+ !ctx->retry_at_ms) {
2161
+ cleanup_lazy_execute((VALUE)ctx);
2162
+ rb_raise(rb_eNoMemError, "failed to allocate lazy request state");
2163
+ }
2164
+
2165
+ for (int slot = 0; slot < ctx->capacity; slot++)
2166
+ ctx->free_slots[ctx->free_count++] = slot;
2167
+ }
2168
+
1965
2169
  static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, int stream) {
1966
2170
  if (!rb_respond_to(requests, rb_intern("each")))
1967
2171
  rb_raise(rb_eArgError, "requests must be an Array or respond to #each");
@@ -1971,54 +2175,14 @@ static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, in
1971
2175
 
1972
2176
  int max_conn;
1973
2177
  parse_options(options, &ctx.opts, &max_conn, &ctx.retry_cfg);
2178
+ parse_retry_codes(options, &ctx.retry_cfg);
1974
2179
  ctx.buffer_size = NIL_P(options) ? max_conn
1975
2180
  : parse_int_option(options, KEY_BUFFER, "buffer", 1,
1976
2181
  MAX_BUFFER, max_conn, NULL);
1977
2182
 
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;
2183
+ configure_retry_mode(&ctx.retry_cfg, target, stream);
2184
+ initialize_lazy_completion(&ctx, requests, options, target, stream, max_conn);
2185
+ allocate_lazy_state(&ctx, max_conn);
2022
2186
 
2023
2187
  return rb_ensure(lazy_execute_body, (VALUE)&ctx, cleanup_lazy_execute, (VALUE)&ctx);
2024
2188
  }
@@ -2053,7 +2217,7 @@ static VALUE scheduler_execute_thread(void *arg) {
2053
2217
  static void cancel_scheduler_execute(scheduler_execute_ctx_t *ctx) {
2054
2218
  multi_session_t *session = ctx->ea->session;
2055
2219
 
2056
- session->cancelled = 1;
2220
+ cancel_session(session);
2057
2221
  #ifdef HAVE_CURL_MULTI_WAKEUP
2058
2222
  if (session->multi)
2059
2223
  curl_multi_wakeup(session->multi);
@@ -2120,63 +2284,17 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
2120
2284
  int max_conn;
2121
2285
  retry_config_t retry_cfg;
2122
2286
  parse_options(options, &opts, &max_conn, &retry_cfg);
2287
+ parse_retry_codes(options, &retry_cfg);
2123
2288
 
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
- }
2150
-
2151
- if (stream || target > 0) {
2152
- if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
2153
- rb_warn(
2154
- "FastCurl: retries are not supported in stream_execute, ignoring retries option");
2155
- if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && target > 0)
2156
- rb_warn(
2157
- "FastCurl: retries are not supported in first_execute, ignoring retries option");
2158
- retry_cfg.max_retries = 0;
2159
- }
2289
+ validate_execute_requests(requests, count, target, options, &opts);
2290
+ configure_retry_mode(&retry_cfg, target, stream);
2160
2291
 
2292
+ materialize_retry_codes(&retry_cfg);
2161
2293
  multi_session_t session;
2162
2294
  int *invalid = NULL;
2163
- multi_session_init(&session, curl_multi_init(), count, max_conn, opts.timeout_sec);
2164
-
2165
- cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
2166
-
2167
- if (!session.multi)
2168
- cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to initialize curl multi handle");
2169
-
2170
- multi_session_configure(session.multi, max_conn);
2171
-
2172
- session.requests = calloc((size_t)count, sizeof(request_ctx_t));
2173
- if (!session.requests)
2174
- cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to allocate request contexts");
2175
-
2176
- invalid = calloc((size_t)count, sizeof(int));
2177
- cleanup.invalid = invalid;
2178
- if (!invalid)
2179
- cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to allocate tracking array");
2295
+ cleanup_ctx_t cleanup = {0};
2296
+ prepare_execute_session(&session, &invalid, &cleanup, &retry_cfg, count, max_conn,
2297
+ opts.timeout_sec);
2180
2298
  execute_args_t ea = {
2181
2299
  .requests = requests,
2182
2300
  .options = options,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCurl
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_curl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - roman-haidarov