fast_curl 0.4.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.
@@ -20,18 +20,22 @@
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
33
36
  #define MAX_REQUESTS 10000
34
37
  #define MAX_CONNECTIONS 100
38
+ #define MAX_BUFFER 10000
35
39
  #define MAX_RETRY_DELAY_MS 30000
36
40
  #define DEFAULT_RETRIES 1
37
41
  #define DEFAULT_RETRY_DELAY 100
@@ -72,6 +76,8 @@ typedef enum {
72
76
  KEY_EFFECTIVE_URL,
73
77
  KEY_ERROR,
74
78
  KEY_ATTEMPTS,
79
+ KEY_BUFFER,
80
+ KEY_ACCEPT,
75
81
  KEY_LAST
76
82
  } key_id_t;
77
83
 
@@ -101,6 +107,8 @@ static const char *const KEY_NAMES[KEY_LAST] = {
101
107
  "effective_url",
102
108
  "error",
103
109
  "attempts",
110
+ "buffer",
111
+ "accept",
104
112
  };
105
113
 
106
114
  typedef enum {
@@ -253,29 +261,63 @@ typedef struct {
253
261
 
254
262
  typedef struct {
255
263
  header_entry_t *entries;
256
- int count;
257
- int cap;
264
+ size_t count;
265
+ size_t cap;
266
+ size_t total_bytes;
258
267
  } header_list_t;
259
268
 
260
269
  static inline void header_list_init(header_list_t *h) {
261
270
  h->entries = NULL;
262
271
  h->count = 0;
263
272
  h->cap = 0;
273
+ h->total_bytes = 0;
264
274
  }
265
275
 
266
276
  static void header_list_free(header_list_t *h) {
267
- for (int i = 0; i < h->count; i++)
277
+ for (size_t i = 0; i < h->count; i++)
268
278
  free(h->entries[i].str);
269
279
  free(h->entries);
270
280
  h->entries = NULL;
271
281
  h->count = 0;
272
282
  h->cap = 0;
283
+ h->total_bytes = 0;
273
284
  }
274
285
 
275
286
  static void header_list_reset(header_list_t *h) {
276
- for (int i = 0; i < h->count; i++)
287
+ for (size_t i = 0; i < h->count; i++)
277
288
  free(h->entries[i].str);
278
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;
279
321
  }
280
322
 
281
323
  static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
@@ -289,22 +331,18 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
289
331
  while (stripped > 0 && (ptr[stripped - 1] == '\r' || ptr[stripped - 1] == '\n'))
290
332
  stripped--;
291
333
 
292
- if (stripped >= 5 && memcmp(ptr, "HTTP/", 5) == 0) {
334
+ int status_line = stripped >= 5 && memcmp(ptr, "HTTP/", 5) == 0;
335
+ if (status_line)
293
336
  header_list_reset(h);
294
- return total;
295
- }
296
337
 
297
- if (stripped == 0)
338
+ if (!header_list_add_bytes(h, total))
339
+ return 0;
340
+
341
+ if (status_line || stripped == 0)
298
342
  return total;
299
343
 
300
- if (h->count >= h->cap) {
301
- int new_cap = (h->cap == 0) ? INITIAL_HEADER_CAP : h->cap * 2;
302
- header_entry_t *ne = realloc(h->entries, sizeof(header_entry_t) * new_cap);
303
- if (!ne)
304
- return 0;
305
- h->entries = ne;
306
- h->cap = new_cap;
307
- }
344
+ if (!header_list_reserve(h))
345
+ return 0;
308
346
 
309
347
  char *entry = malloc(stripped + 1);
310
348
  if (!entry)
@@ -320,7 +358,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
320
358
 
321
359
  typedef struct {
322
360
  CURL *easy;
323
- int index;
361
+ long index;
324
362
  buffer_t body;
325
363
  header_list_t headers;
326
364
  struct curl_slist *req_headers;
@@ -341,7 +379,7 @@ static VALUE fast_validation_error = Qnil;
341
379
  (ctx)->setup_error_fatal = (fatal); \
342
380
  } while (0)
343
381
 
344
- static inline void request_ctx_init(request_ctx_t *ctx, int index) {
382
+ static inline void request_ctx_init(request_ctx_t *ctx, long index) {
345
383
  ctx->easy = NULL;
346
384
  ctx->index = index;
347
385
  buffer_init(&ctx->body);
@@ -410,7 +448,7 @@ typedef struct {
410
448
  int still_running;
411
449
  long timeout_ms;
412
450
  int max_connections;
413
- volatile int cancelled;
451
+ int cancelled;
414
452
 
415
453
  int active_count;
416
454
  int pending_pos;
@@ -418,12 +456,21 @@ typedef struct {
418
456
  int *pending_indices;
419
457
  } multi_session_t;
420
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
+
421
467
  typedef struct {
422
468
  int max_retries;
423
469
  int retries_explicit;
424
470
  long retry_delay_ms;
425
471
  int *retry_http_codes;
426
472
  int retry_http_count;
473
+ VALUE retry_code_values;
427
474
  int retry_non_idempotent;
428
475
  } retry_config_t;
429
476
 
@@ -566,7 +613,7 @@ static VALUE build_response(request_ctx_t *ctx) {
566
613
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
567
614
 
568
615
  VALUE headers_hash = new_headers_hash();
569
- for (int i = 0; i < ctx->headers.count; i++) {
616
+ for (size_t i = 0; i < ctx->headers.count; i++) {
570
617
  const char *hdr = ctx->headers.entries[i].str;
571
618
  size_t hdr_len = ctx->headers.entries[i].len;
572
619
  const char *colon = memchr(hdr, ':', hdr_len);
@@ -593,7 +640,7 @@ static VALUE build_response(request_ctx_t *ctx) {
593
640
  }
594
641
 
595
642
  VALUE body_str =
596
- 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("");
597
644
 
598
645
  VALUE result = rb_hash_new();
599
646
  rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
@@ -638,7 +685,7 @@ static int is_valid_url(const char *url) {
638
685
  return 0;
639
686
  if (strncmp(url, "https://", 8) == 0)
640
687
  return 1;
641
- if (len >= 7 && strncmp(url, "http://", 7) == 0)
688
+ if (strncmp(url, "http://", 7) == 0)
642
689
  return 1;
643
690
  return 0;
644
691
  }
@@ -881,7 +928,7 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_op
881
928
  SET_SETUP_ERROR(ctx, "Missing :url", 0);
882
929
  return 0;
883
930
  }
884
- 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))) {
885
932
  SET_SETUP_ERROR(ctx, "Invalid URL", 1);
886
933
  return 0;
887
934
  }
@@ -929,6 +976,67 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_op
929
976
  return 1;
930
977
  }
931
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
+
932
1040
  /* Poll until something actually completes, or the slice budget expires.
933
1041
  Previously this returned after every single 50ms poll, which under a fiber
934
1042
  scheduler meant a fresh OS thread twenty times a second. Cancellation still
@@ -938,7 +1046,7 @@ static void *poll_without_gvl(void *arg) {
938
1046
  long long started = fast_now_ms();
939
1047
  int before = s->still_running;
940
1048
 
941
- while (!s->cancelled) {
1049
+ while (!session_cancelled(s)) {
942
1050
  int numfds = 0;
943
1051
  curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
944
1052
  curl_multi_perform(s->multi, &s->still_running);
@@ -953,7 +1061,7 @@ static void *poll_without_gvl(void *arg) {
953
1061
 
954
1062
  static void unblock_perform(void *arg) {
955
1063
  multi_session_t *s = (multi_session_t *)arg;
956
- s->cancelled = 1;
1064
+ cancel_session(s);
957
1065
  #ifdef HAVE_CURL_MULTI_WAKEUP
958
1066
  curl_multi_wakeup(s->multi);
959
1067
  #endif
@@ -961,30 +1069,52 @@ static void unblock_perform(void *arg) {
961
1069
 
962
1070
  typedef struct {
963
1071
  VALUE results;
1072
+ VALUE accept;
964
1073
  int completed;
965
1074
  int target;
966
1075
  int stream;
967
1076
  } completion_ctx_t;
968
1077
 
969
- static VALUE build_result_pair(int index, VALUE response) {
970
- return rb_ary_new_from_args(2, INT2NUM(index), response);
1078
+ static VALUE build_result_pair(long index, VALUE response) {
1079
+ return rb_ary_new_from_args(2, LONG2NUM(index), response);
971
1080
  }
972
1081
 
973
- static int record_immediate_error(completion_ctx_t *cctx, int index, const char *message,
974
- int attempts) {
975
- if (cctx->stream || cctx->target > 0) {
976
- VALUE pair =
977
- build_result_pair(index, build_error_response(message, ERR_INVALID_REQUEST, attempts));
1082
+ static int response_accepted(completion_ctx_t *cctx, VALUE response) {
1083
+ if (cctx->target <= 0 || NIL_P(cctx->accept))
1084
+ return 1;
978
1085
 
979
- if (cctx->stream)
980
- rb_yield(pair);
981
- else
982
- rb_ary_push(cctx->results, pair);
1086
+ return RTEST(rb_funcall(cctx->accept, rb_intern("call"), 1, response));
1087
+ }
983
1088
 
1089
+ static int record_response(completion_ctx_t *cctx, long index, VALUE response) {
1090
+ VALUE pair = build_result_pair(index, response);
1091
+
1092
+ if (cctx->stream) {
1093
+ rb_yield(pair);
984
1094
  cctx->completed++;
985
- if (cctx->target > 0 && cctx->completed >= cctx->target)
986
- return 1;
1095
+ return 0;
1096
+ }
1097
+
1098
+ if (cctx->target > 0) {
1099
+ if (!response_accepted(cctx, response))
1100
+ return 0;
1101
+
1102
+ rb_ary_push(cctx->results, pair);
1103
+ cctx->completed++;
1104
+ return cctx->completed >= cctx->target;
987
1105
  }
1106
+
1107
+ rb_ary_store(cctx->results, index, pair);
1108
+ cctx->completed++;
1109
+ return 0;
1110
+ }
1111
+
1112
+ static int record_immediate_error(completion_ctx_t *cctx, long index, const char *message,
1113
+ int attempts) {
1114
+ if (cctx->stream || cctx->target > 0)
1115
+ return record_response(cctx, index,
1116
+ build_error_response(message, ERR_INVALID_REQUEST, attempts));
1117
+
988
1118
  return 0;
989
1119
  }
990
1120
 
@@ -1019,17 +1149,11 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
1019
1149
  ? build_response(ctx)
1020
1150
  : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1021
1151
  (int)msg->data.result, ctx->attempts);
1022
- VALUE pair = build_result_pair(ctx->index, response);
1023
-
1024
- if (cctx->stream)
1025
- rb_yield(pair);
1026
- else
1027
- rb_ary_push(cctx->results, pair);
1152
+ if (record_response(cctx, ctx->index, response))
1153
+ return 1;
1154
+ } else {
1155
+ cctx->completed++;
1028
1156
  }
1029
-
1030
- cctx->completed++;
1031
- if (cctx->target > 0 && cctx->completed >= cctx->target)
1032
- return 1;
1033
1157
  }
1034
1158
 
1035
1159
  return 0;
@@ -1123,7 +1247,8 @@ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VAL
1123
1247
  if (process_completed(session, cctx))
1124
1248
  return;
1125
1249
 
1126
- while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
1250
+ while (!session_cancelled(session) &&
1251
+ (session->active_count > 0 || pending_remaining(session))) {
1127
1252
  if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1128
1253
  break;
1129
1254
 
@@ -1208,6 +1333,7 @@ static void retry_config_init(retry_config_t *retry_cfg) {
1208
1333
  retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
1209
1334
  retry_cfg->retry_http_codes = NULL;
1210
1335
  retry_cfg->retry_http_count = 0;
1336
+ retry_cfg->retry_code_values = Qnil;
1211
1337
  retry_cfg->retry_non_idempotent = 0;
1212
1338
  }
1213
1339
 
@@ -1266,22 +1392,33 @@ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
1266
1392
  rb_raise(rb_eArgError, "retry_codes is too large");
1267
1393
 
1268
1394
  len = (int)len_long;
1395
+ VALUE normalized = rb_ary_new_capa(len);
1269
1396
  for (int i = 0; i < len; i++) {
1270
1397
  int code = NUM2INT(rb_ary_entry(codes, i));
1271
1398
  if (code < 100 || code > 599)
1272
1399
  rb_raise(rb_eArgError, "retry_codes must contain valid HTTP status codes");
1400
+ rb_ary_push(normalized, INT2NUM(code));
1273
1401
  }
1274
1402
 
1275
- 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)
1276
1410
  return;
1277
1411
 
1278
- 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);
1279
1417
  if (!retry_cfg->retry_http_codes)
1280
1418
  rb_raise(rb_eNoMemError, "failed to allocate retry codes");
1281
1419
 
1282
- retry_cfg->retry_http_count = len;
1283
- for (int i = 0; i < len; i++)
1284
- 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));
1285
1422
  }
1286
1423
 
1287
1424
  static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
@@ -1326,7 +1463,17 @@ static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
1326
1463
  retry_cfg->retry_delay_ms =
1327
1464
  parse_long_option(options, KEY_RETRY_DELAY, "retry_delay", 0, MAX_RETRY_DELAY_MS,
1328
1465
  retry_cfg->retry_delay_ms, NULL);
1329
- 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;
1330
1477
  }
1331
1478
 
1332
1479
  static void multi_session_init(multi_session_t *session, CURLM *multi, int count, int max_conn,
@@ -1389,6 +1536,27 @@ static FAST_CURL_NORETURN void cleanup_and_raise(cleanup_ctx_t *cleanup, VALUE e
1389
1536
  rb_raise(exception, "%s", message);
1390
1537
  }
1391
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
+
1392
1560
  typedef struct {
1393
1561
  VALUE requests;
1394
1562
  VALUE options;
@@ -1400,126 +1568,623 @@ typedef struct {
1400
1568
  request_options_t *opts;
1401
1569
  } execute_args_t;
1402
1570
 
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) {
1591
+ multi_session_t *session = ea->session;
1592
+ int count = session->count;
1593
+ request_options_t *opts = ea->opts;
1594
+ retry_config_t *retry_cfg = ea->retry_cfg;
1595
+
1596
+ if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
1597
+ *deadline_hit = 1;
1598
+ return 0;
1599
+ }
1600
+
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
+ }
1608
+
1609
+ if (retry_count == 0)
1610
+ return 0;
1611
+
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;
1621
+ }
1622
+ retry_delay_sleep(backoff);
1623
+
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
+ }
1634
+
1635
+ retry_indices[runnable_count++] = idx;
1636
+ }
1637
+
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
+ }
1645
+
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
+ }
1654
+
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
+ }
1678
+
1679
+ rb_ary_store(cctx->results, i, build_result_pair(i, response));
1680
+ }
1681
+ }
1682
+
1403
1683
  static VALUE internal_execute_body(VALUE arg) {
1404
1684
  execute_args_t *ea = (execute_args_t *)arg;
1405
- VALUE requests = ea->requests;
1406
1685
  multi_session_t *session = ea->session;
1407
- int *invalid = ea->invalid;
1408
1686
  retry_config_t *retry_cfg = ea->retry_cfg;
1409
1687
  request_options_t *opts = ea->opts;
1410
- int deadline_hit = 0;
1411
1688
  int count = session->count;
1412
- int target = ea->target;
1413
- int stream = ea->stream;
1414
1689
 
1415
1690
  for (int i = 0; i < count; i++)
1416
1691
  request_ctx_init(&session->requests[i], i);
1417
1692
 
1418
1693
  completion_ctx_t cctx;
1419
- cctx.results = stream ? Qnil : ((target > 0) ? rb_ary_new2(target) : rb_ary_new2(count));
1420
- cctx.completed = 0;
1421
- cctx.target = target;
1422
- cctx.stream = stream;
1423
-
1424
- if (!stream && target <= 0) {
1425
- for (int i = 0; i < count; i++)
1426
- rb_ary_store(cctx.results, i, Qnil);
1427
- }
1694
+ completion_context_init(&cctx, ea, count);
1428
1695
 
1429
1696
  if (opts->total_timeout_ms > 0)
1430
1697
  opts->deadline_ms = fast_now_ms() + opts->total_timeout_ms;
1431
1698
 
1432
- run_multi_loop(session, &cctx, requests, invalid, opts, NULL, count);
1699
+ run_multi_loop(session, &cctx, ea->requests, ea->invalid, opts, NULL, count);
1433
1700
 
1434
- if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
1435
- for (int attempt = 0; attempt < retry_cfg->max_retries; attempt++) {
1436
- if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
1437
- deadline_hit = 1;
1438
- break;
1439
- }
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);
1440
1704
 
1441
- int *retry_indices = malloc(sizeof(int) * (size_t)count);
1442
- if (!retry_indices)
1443
- rb_raise(rb_eNoMemError, "failed to allocate retry index array");
1705
+ if (!ea->stream && ea->target <= 0)
1706
+ store_execution_results(ea, &cctx, deadline_hit);
1444
1707
 
1445
- int retry_count = 0;
1446
- for (int i = 0; i < count; i++) {
1447
- if (invalid[i] || !session->requests[i].done)
1448
- continue;
1449
- if (should_retry(&session->requests[i], retry_cfg))
1450
- retry_indices[retry_count++] = i;
1451
- }
1708
+ return ea->stream ? Qnil : cctx.results;
1709
+ }
1452
1710
 
1453
- if (retry_count == 0) {
1454
- free(retry_indices);
1455
- break;
1456
- }
1711
+ typedef enum {
1712
+ LAZY_SLOT_FREE,
1713
+ LAZY_SLOT_PENDING,
1714
+ LAZY_SLOT_ACTIVE,
1715
+ LAZY_SLOT_RETRY_WAIT
1716
+ } lazy_slot_state_t;
1457
1717
 
1458
- long backoff = retry_backoff_ms(retry_cfg, attempt);
1459
- if (opts->deadline_ms > 0) {
1460
- long long left = opts->deadline_ms - fast_now_ms();
1461
- if (left <= 0) {
1462
- deadline_hit = 1;
1463
- free(retry_indices);
1464
- break;
1465
- }
1466
- if (backoff > (long)left)
1467
- backoff = (long)left;
1468
- }
1469
- retry_delay_sleep(backoff);
1718
+ typedef struct {
1719
+ multi_session_t session;
1720
+ request_options_t opts;
1721
+ retry_config_t retry_cfg;
1722
+ completion_ctx_t cctx;
1723
+ VALUE source;
1724
+ VALUE anchors;
1725
+ int *states;
1726
+ int *free_slots;
1727
+ int free_count;
1728
+ int *pending_slots;
1729
+ int pending_head;
1730
+ int pending_tail;
1731
+ int pending_count;
1732
+ long long *retry_at_ms;
1733
+ int capacity;
1734
+ int buffer_size;
1735
+ int occupied_count;
1736
+ long next_index;
1737
+ int deadline_hit;
1738
+ int stop;
1739
+ } lazy_execute_ctx_t;
1740
+
1741
+ static int lazy_pending_push(lazy_execute_ctx_t *ctx, int slot) {
1742
+ if (ctx->pending_count >= ctx->capacity)
1743
+ return 0;
1470
1744
 
1471
- int runnable_count = 0;
1472
- for (int r = 0; r < retry_count; r++) {
1473
- int idx = retry_indices[r];
1474
- request_ctx_t *rc = &session->requests[idx];
1745
+ ctx->pending_slots[ctx->pending_tail] = slot;
1746
+ ctx->pending_tail = (ctx->pending_tail + 1) % ctx->capacity;
1747
+ ctx->pending_count++;
1748
+ return 1;
1749
+ }
1475
1750
 
1476
- if (!request_ctx_reset_for_retry(rc)) {
1477
- invalid[idx] = 1;
1478
- rc->done = 1;
1479
- rc->setup_error = "Failed to allocate a curl handle for the retry";
1480
- continue;
1481
- }
1751
+ static int lazy_pending_pop(lazy_execute_ctx_t *ctx) {
1752
+ if (ctx->pending_count == 0)
1753
+ return -1;
1482
1754
 
1483
- retry_indices[runnable_count++] = idx;
1484
- }
1755
+ int slot = ctx->pending_slots[ctx->pending_head];
1756
+ ctx->pending_head = (ctx->pending_head + 1) % ctx->capacity;
1757
+ ctx->pending_count--;
1758
+ return slot;
1759
+ }
1485
1760
 
1486
- if (runnable_count > 0) {
1487
- cctx.completed = 0;
1488
- run_multi_loop(session, &cctx, requests, invalid, opts, retry_indices,
1489
- runnable_count);
1490
- }
1761
+ static void lazy_release_slot(lazy_execute_ctx_t *ctx, int slot) {
1762
+ request_ctx_free(&ctx->session.requests[slot]);
1763
+ rb_ary_store(ctx->anchors, slot, Qnil);
1764
+ ctx->states[slot] = LAZY_SLOT_FREE;
1765
+ ctx->retry_at_ms[slot] = 0;
1766
+ ctx->free_slots[ctx->free_count++] = slot;
1767
+ if (ctx->occupied_count > 0)
1768
+ ctx->occupied_count--;
1769
+ }
1491
1770
 
1492
- free(retry_indices);
1771
+ static FAST_CURL_NORETURN void lazy_raise_setup_error(request_ctx_t *request) {
1772
+ VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
1773
+ rb_raise(klass, "%s (request %ld)",
1774
+ request->setup_error ? request->setup_error : "Invalid request configuration",
1775
+ request->index);
1776
+ }
1777
+
1778
+ static int lazy_finalize_error(lazy_execute_ctx_t *ctx, int slot, const char *message) {
1779
+ request_ctx_t *request = &ctx->session.requests[slot];
1780
+ VALUE response = build_error_response(message, ERR_INVALID_REQUEST, request->attempts);
1781
+ int stop = record_response(&ctx->cctx, request->index, response);
1782
+ lazy_release_slot(ctx, slot);
1783
+ return stop;
1784
+ }
1785
+
1786
+ static int lazy_activate_pending(lazy_execute_ctx_t *ctx) {
1787
+ while (ctx->session.active_count < ctx->session.max_connections && ctx->pending_count > 0) {
1788
+ int slot = lazy_pending_pop(ctx);
1789
+ request_ctx_t *request = &ctx->session.requests[slot];
1790
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1791
+
1792
+ if (!request_ctx_prepare_easy(request)) {
1793
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle"))
1794
+ return 1;
1795
+ continue;
1493
1796
  }
1797
+
1798
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1799
+ if (request->setup_error_fatal)
1800
+ lazy_raise_setup_error(request);
1801
+ if (lazy_finalize_error(ctx, slot,
1802
+ request->setup_error ? request->setup_error
1803
+ : "Invalid request configuration"))
1804
+ return 1;
1805
+ continue;
1806
+ }
1807
+
1808
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1809
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the request"))
1810
+ return 1;
1811
+ continue;
1812
+ }
1813
+
1814
+ request->attempts++;
1815
+ request->active = 1;
1816
+ request->done = 0;
1817
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1818
+ ctx->session.active_count++;
1494
1819
  }
1495
1820
 
1496
- if (!stream && target <= 0) {
1497
- for (int i = 0; i < count; i++) {
1498
- request_ctx_t *rc = &session->requests[i];
1499
- VALUE response;
1500
-
1501
- if (invalid[i]) {
1502
- response = build_error_response(rc->setup_error ? rc->setup_error
1503
- : "Invalid request configuration",
1504
- ERR_INVALID_REQUEST, rc->attempts);
1505
- } else if (!rc->done) {
1506
- response =
1507
- deadline_hit || (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1508
- ? build_error_response("Total timeout exceeded", ERR_DEADLINE, rc->attempts)
1509
- : build_error_response("Request was not completed", ERR_NOT_COMPLETED,
1510
- rc->attempts);
1511
- } else if (rc->curl_result == CURLE_OK) {
1512
- response = build_response(rc);
1513
- } else {
1514
- response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
1515
- (int)rc->curl_result, rc->attempts);
1516
- }
1821
+ return 0;
1822
+ }
1823
+
1824
+ static int lazy_activate_retries(lazy_execute_ctx_t *ctx) {
1825
+ long long now = fast_now_ms();
1826
+
1827
+ for (int slot = 0;
1828
+ slot < ctx->capacity && ctx->session.active_count < ctx->session.max_connections; slot++) {
1829
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT || ctx->retry_at_ms[slot] > now)
1830
+ continue;
1831
+
1832
+ request_ctx_t *request = &ctx->session.requests[slot];
1833
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1834
+
1835
+ if (!request_ctx_reset_for_retry(request)) {
1836
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle for the retry"))
1837
+ return 1;
1838
+ continue;
1839
+ }
1840
+
1841
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1842
+ if (request->setup_error_fatal)
1843
+ lazy_raise_setup_error(request);
1844
+ if (lazy_finalize_error(ctx, slot,
1845
+ request->setup_error ? request->setup_error
1846
+ : "Invalid request configuration"))
1847
+ return 1;
1848
+ continue;
1849
+ }
1850
+
1851
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1852
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the retry"))
1853
+ return 1;
1854
+ continue;
1855
+ }
1856
+
1857
+ request->attempts++;
1858
+ request->active = 1;
1859
+ request->done = 0;
1860
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1861
+ ctx->retry_at_ms[slot] = 0;
1862
+ ctx->session.active_count++;
1863
+ }
1864
+
1865
+ return 0;
1866
+ }
1867
+
1868
+ static long long lazy_next_retry_at(const lazy_execute_ctx_t *ctx) {
1869
+ long long next = 0;
1870
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1871
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT)
1872
+ continue;
1873
+ if (next == 0 || ctx->retry_at_ms[slot] < next)
1874
+ next = ctx->retry_at_ms[slot];
1875
+ }
1876
+ return next;
1877
+ }
1878
+
1879
+ static int lazy_process_completed(lazy_execute_ctx_t *ctx) {
1880
+ CURLMsg *msg;
1881
+ int msgs_left;
1882
+
1883
+ while ((msg = curl_multi_info_read(ctx->session.multi, &msgs_left))) {
1884
+ if (msg->msg != CURLMSG_DONE)
1885
+ continue;
1886
+
1887
+ request_ctx_t *request = NULL;
1888
+ curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&request);
1889
+ if (!request || request->done)
1890
+ continue;
1891
+
1892
+ int slot = (int)(request - ctx->session.requests);
1893
+ if (slot < 0 || slot >= ctx->capacity)
1894
+ continue;
1895
+
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
+ request->done = 1;
1904
+ request->curl_result = msg->data.result;
1905
+ if (msg->data.result == CURLE_OK)
1906
+ curl_easy_getinfo(request->easy, CURLINFO_RESPONSE_CODE, &request->http_status);
1907
+
1908
+ if (request->attempts <= ctx->retry_cfg.max_retries &&
1909
+ should_retry(request, &ctx->retry_cfg)) {
1910
+ ctx->states[slot] = LAZY_SLOT_RETRY_WAIT;
1911
+ ctx->retry_at_ms[slot] =
1912
+ fast_now_ms() + retry_backoff_ms(&ctx->retry_cfg, request->attempts - 1);
1913
+ continue;
1914
+ }
1915
+
1916
+ VALUE response =
1917
+ (msg->data.result == CURLE_OK)
1918
+ ? build_response(request)
1919
+ : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1920
+ (int)msg->data.result, request->attempts);
1921
+ int stop = record_response(&ctx->cctx, request->index, response);
1922
+ lazy_release_slot(ctx, slot);
1923
+ if (stop)
1924
+ return 1;
1925
+ }
1926
+
1927
+ return 0;
1928
+ }
1929
+
1930
+ typedef struct {
1931
+ multi_session_t *session;
1932
+ long long wake_at_ms;
1933
+ } lazy_poll_args_t;
1934
+
1935
+ static void *lazy_poll_without_gvl(void *arg) {
1936
+ lazy_poll_args_t *poll = (lazy_poll_args_t *)arg;
1937
+ multi_session_t *session = poll->session;
1938
+ long long started = fast_now_ms();
1939
+ int before = session->still_running;
1940
+
1941
+ while (!session_cancelled(session)) {
1942
+ int numfds = 0;
1943
+ curl_multi_poll(session->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
1944
+ curl_multi_perform(session->multi, &session->still_running);
1945
+
1946
+ if (session->still_running == 0 || session->still_running < before)
1947
+ break;
1948
+ if (poll->wake_at_ms > 0 && fast_now_ms() >= poll->wake_at_ms)
1949
+ break;
1950
+ if (fast_now_ms() - started >= POLL_SLICE_MS)
1951
+ break;
1952
+ }
1953
+
1954
+ return NULL;
1955
+ }
1956
+
1957
+ static void lazy_wait(lazy_execute_ctx_t *ctx) {
1958
+ long long next_retry = lazy_next_retry_at(ctx);
1959
+
1960
+ if (ctx->session.active_count > 0) {
1961
+ lazy_poll_args_t poll = {.session = &ctx->session, .wake_at_ms = next_retry};
1962
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1963
+ VALUE scheduler = current_fiber_scheduler();
1964
+ if (scheduler != Qnil)
1965
+ run_via_fiber_worker(scheduler, lazy_poll_without_gvl, &poll);
1966
+ else
1967
+ #endif
1968
+ rb_thread_call_without_gvl(lazy_poll_without_gvl, &poll, unblock_perform,
1969
+ &ctx->session);
1970
+ return;
1971
+ }
1972
+
1973
+ if (next_retry > 0) {
1974
+ long long delay = next_retry - fast_now_ms();
1975
+ if (delay > 0)
1976
+ retry_delay_sleep((long)delay);
1977
+ }
1978
+ }
1979
+
1980
+ static int lazy_deadline_reached(lazy_execute_ctx_t *ctx) {
1981
+ if (ctx->opts.deadline_ms <= 0 || fast_now_ms() < ctx->opts.deadline_ms)
1982
+ return 0;
1983
+
1984
+ ctx->deadline_hit = 1;
1985
+ ctx->stop = 1;
1986
+ return 1;
1987
+ }
1988
+
1989
+ static void lazy_progress(lazy_execute_ctx_t *ctx) {
1990
+ if (ctx->stop || lazy_deadline_reached(ctx))
1991
+ return;
1992
+
1993
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
1994
+ ctx->stop = 1;
1995
+ return;
1996
+ }
1997
+
1998
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
1999
+ if (lazy_process_completed(ctx)) {
2000
+ ctx->stop = 1;
2001
+ return;
2002
+ }
2003
+
2004
+ if (ctx->occupied_count == 0)
2005
+ return;
2006
+
2007
+ lazy_wait(ctx);
2008
+ if (lazy_process_completed(ctx))
2009
+ ctx->stop = 1;
2010
+ }
2011
+
2012
+ static VALUE lazy_each_request(VALUE yielded, VALUE arg, int argc, const VALUE *argv,
2013
+ VALUE blockarg) {
2014
+ (void)blockarg;
2015
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
2016
+ VALUE request = argc == 1 ? argv[0] : yielded;
2017
+
2018
+ while (!ctx->stop && ctx->occupied_count >= ctx->capacity)
2019
+ lazy_progress(ctx);
2020
+
2021
+ if (ctx->stop || lazy_deadline_reached(ctx))
2022
+ rb_iter_break();
1517
2023
 
1518
- rb_ary_store(cctx.results, i, build_result_pair(i, response));
2024
+ if (ctx->next_index > INT_MAX)
2025
+ rb_raise(rb_eArgError, "lazy request index exceeds supported range");
2026
+
2027
+ int slot = ctx->free_slots[--ctx->free_count];
2028
+ request_ctx_init(&ctx->session.requests[slot], ctx->next_index++);
2029
+ rb_ary_store(ctx->anchors, slot, request);
2030
+ ctx->states[slot] = LAZY_SLOT_PENDING;
2031
+ ctx->occupied_count++;
2032
+ lazy_pending_push(ctx, slot);
2033
+
2034
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
2035
+ ctx->stop = 1;
2036
+ rb_iter_break();
2037
+ }
2038
+
2039
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
2040
+ if (lazy_process_completed(ctx)) {
2041
+ ctx->stop = 1;
2042
+ rb_iter_break();
2043
+ }
2044
+
2045
+ return Qnil;
2046
+ }
2047
+
2048
+ static void lazy_finalize_deadline(lazy_execute_ctx_t *ctx) {
2049
+ if (ctx->cctx.stream || ctx->cctx.target > 0)
2050
+ return;
2051
+
2052
+ for (int slot = 0; slot < ctx->capacity; slot++) {
2053
+ if (ctx->states[slot] == LAZY_SLOT_FREE)
2054
+ continue;
2055
+
2056
+ request_ctx_t *request = &ctx->session.requests[slot];
2057
+ if (request->active) {
2058
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
2059
+ request->active = 0;
2060
+ if (ctx->session.active_count > 0)
2061
+ ctx->session.active_count--;
1519
2062
  }
2063
+
2064
+ VALUE response =
2065
+ build_error_response("Total timeout exceeded", ERR_DEADLINE, request->attempts);
2066
+ record_response(&ctx->cctx, request->index, response);
2067
+ lazy_release_slot(ctx, slot);
1520
2068
  }
2069
+ }
2070
+
2071
+ static VALUE lazy_execute_body(VALUE arg) {
2072
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1521
2073
 
1522
- return stream ? Qnil : cctx.results;
2074
+ if (ctx->opts.total_timeout_ms > 0)
2075
+ ctx->opts.deadline_ms = fast_now_ms() + ctx->opts.total_timeout_ms;
2076
+
2077
+ rb_block_call(ctx->source, rb_intern("each"), 0, NULL, lazy_each_request, (VALUE)ctx);
2078
+
2079
+ while (!ctx->stop && ctx->occupied_count > 0)
2080
+ lazy_progress(ctx);
2081
+
2082
+ if (ctx->deadline_hit)
2083
+ lazy_finalize_deadline(ctx);
2084
+
2085
+ RB_GC_GUARD(ctx->source);
2086
+ RB_GC_GUARD(ctx->anchors);
2087
+ return ctx->cctx.stream ? Qnil : ctx->cctx.results;
2088
+ }
2089
+
2090
+ static VALUE cleanup_lazy_execute(VALUE arg) {
2091
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
2092
+
2093
+ if (ctx->session.requests) {
2094
+ for (int slot = 0; slot < ctx->capacity; slot++) {
2095
+ request_ctx_t *request = &ctx->session.requests[slot];
2096
+ if (request->easy && request->active && ctx->session.multi)
2097
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
2098
+ request_ctx_free(request);
2099
+ }
2100
+ free(ctx->session.requests);
2101
+ ctx->session.requests = NULL;
2102
+ }
2103
+
2104
+ free(ctx->states);
2105
+ ctx->states = NULL;
2106
+ free(ctx->free_slots);
2107
+ ctx->free_slots = NULL;
2108
+ free(ctx->pending_slots);
2109
+ ctx->pending_slots = NULL;
2110
+ free(ctx->retry_at_ms);
2111
+ ctx->retry_at_ms = NULL;
2112
+
2113
+ if (ctx->session.multi) {
2114
+ curl_multi_cleanup(ctx->session.multi);
2115
+ ctx->session.multi = NULL;
2116
+ }
2117
+
2118
+ if (ctx->retry_cfg.retry_http_codes) {
2119
+ free(ctx->retry_cfg.retry_http_codes);
2120
+ ctx->retry_cfg.retry_http_codes = NULL;
2121
+ }
2122
+
2123
+ return Qnil;
2124
+ }
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
+
2169
+ static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, int stream) {
2170
+ if (!rb_respond_to(requests, rb_intern("each")))
2171
+ rb_raise(rb_eArgError, "requests must be an Array or respond to #each");
2172
+
2173
+ lazy_execute_ctx_t ctx;
2174
+ memset(&ctx, 0, sizeof(ctx));
2175
+
2176
+ int max_conn;
2177
+ parse_options(options, &ctx.opts, &max_conn, &ctx.retry_cfg);
2178
+ parse_retry_codes(options, &ctx.retry_cfg);
2179
+ ctx.buffer_size = NIL_P(options) ? max_conn
2180
+ : parse_int_option(options, KEY_BUFFER, "buffer", 1,
2181
+ MAX_BUFFER, max_conn, NULL);
2182
+
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);
2186
+
2187
+ return rb_ensure(lazy_execute_body, (VALUE)&ctx, cleanup_lazy_execute, (VALUE)&ctx);
1523
2188
  }
1524
2189
 
1525
2190
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
@@ -1552,7 +2217,7 @@ static VALUE scheduler_execute_thread(void *arg) {
1552
2217
  static void cancel_scheduler_execute(scheduler_execute_ctx_t *ctx) {
1553
2218
  multi_session_t *session = ctx->ea->session;
1554
2219
 
1555
- session->cancelled = 1;
2220
+ cancel_session(session);
1556
2221
  #ifdef HAVE_CURL_MULTI_WAKEUP
1557
2222
  if (session->multi)
1558
2223
  curl_multi_wakeup(session->multi);
@@ -1597,6 +2262,9 @@ static VALUE execute_with_fiber_scheduler(VALUE arg) {
1597
2262
  #endif
1598
2263
 
1599
2264
  static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
2265
+ if (!RB_TYPE_P(requests, T_ARRAY))
2266
+ return internal_lazy_execute(requests, options, target, stream);
2267
+
1600
2268
  Check_Type(requests, T_ARRAY);
1601
2269
 
1602
2270
  long count_long = RARRAY_LEN(requests);
@@ -1616,57 +2284,17 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1616
2284
  int max_conn;
1617
2285
  retry_config_t retry_cfg;
1618
2286
  parse_options(options, &opts, &max_conn, &retry_cfg);
2287
+ parse_retry_codes(options, &retry_cfg);
1619
2288
 
1620
- for (int i = 0; i < count; i++) {
1621
- request_ctx_t probe;
1622
- request_ctx_init(&probe, i);
1623
- if (!request_ctx_prepare_easy(&probe)) {
1624
- request_ctx_free(&probe);
1625
- continue;
1626
- }
1627
- setup_easy_handle(&probe, rb_ary_entry(requests, i), &opts);
1628
- int fatal = probe.setup_error_fatal;
1629
- const char *message = probe.setup_error;
1630
- char buffer[128];
1631
- if (fatal && message) {
1632
- snprintf(buffer, sizeof(buffer), "%s (request %d)", message, i);
1633
- }
1634
- request_ctx_free(&probe);
1635
- if (fatal) {
1636
- VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
1637
- rb_raise(klass, "%s", buffer);
1638
- }
1639
- }
1640
-
1641
- if (stream || target > 0) {
1642
- if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
1643
- rb_warn(
1644
- "FastCurl: retries are not supported in stream_execute, ignoring retries option");
1645
- if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && target > 0)
1646
- rb_warn(
1647
- "FastCurl: retries are not supported in first_execute, ignoring retries option");
1648
- retry_cfg.max_retries = 0;
1649
- }
2289
+ validate_execute_requests(requests, count, target, options, &opts);
2290
+ configure_retry_mode(&retry_cfg, target, stream);
1650
2291
 
2292
+ materialize_retry_codes(&retry_cfg);
1651
2293
  multi_session_t session;
1652
2294
  int *invalid = NULL;
1653
- multi_session_init(&session, curl_multi_init(), count, max_conn, opts.timeout_sec);
1654
-
1655
- cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
1656
-
1657
- if (!session.multi)
1658
- cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to initialize curl multi handle");
1659
-
1660
- multi_session_configure(session.multi, max_conn);
1661
-
1662
- session.requests = calloc((size_t)count, sizeof(request_ctx_t));
1663
- if (!session.requests)
1664
- cleanup_and_raise(&cleanup, rb_eNoMemError, "failed to allocate request contexts");
1665
-
1666
- invalid = calloc((size_t)count, sizeof(int));
1667
- cleanup.invalid = invalid;
1668
- if (!invalid)
1669
- 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);
1670
2298
  execute_args_t ea = {
1671
2299
  .requests = requests,
1672
2300
  .options = options,
@@ -1680,7 +2308,8 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1680
2308
 
1681
2309
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1682
2310
  VALUE scheduler = current_fiber_scheduler();
1683
- if (scheduler != Qnil && !stream) {
2311
+ VALUE accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
2312
+ if (scheduler != Qnil && !stream && NIL_P(accept)) {
1684
2313
  scheduler_execute_ctx_t scheduler_ctx = {
1685
2314
  .ea = &ea,
1686
2315
  .scheduler = scheduler,