fast_curl 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa97f9eaef2dd36adbff2816b3ab08ec7e924b7a7a23c01c7c38723c6f071ff3
4
- data.tar.gz: 5c5e02905534b2c54602b9f970fab2d587603f7c31ab6a0ab251bd6a1857932b
3
+ metadata.gz: 279ce69cb40c48246c4e6276260a928f4e03ae86e691f876492ba25a39eb00c7
4
+ data.tar.gz: 577583ed2fde2e2653f1e27f22caca08846fd32bb49524a59f44e5b3ba3acee9
5
5
  SHA512:
6
- metadata.gz: 54c9d82c53ab24b08129c38cda9a82e06d834cf16f4b2b210f62edd2361c77db84f49eb59e3cbcbfc04d94dc12a758d89bc6e65bed23183132e8365186892716
7
- data.tar.gz: 354ee56ed5973a66f365f76eb0f2a01264ae3e14e238ff616cd34d6251f10830babda151037a713b7e17573bb209387e05713b19b291c573b31cf11099ecb3b7
6
+ metadata.gz: 3e0edb04a304a46adaab5368289d2332672fcd7654506c6449cde10ea5b5620cc4ba71549f60fccb0308d2ef67c8dc4e4e54304efa8c84dfc1631f360f89cdc8
7
+ data.tar.gz: 54693fc0a4189cf60e48f35b92d4f82ba1971b170cf54b610ed27378b5c296c50fa88798b4d0ac135ca3734b1cce7abf0b5214ed60e18aa90f0f158d8f708199
data/README.md CHANGED
@@ -76,6 +76,23 @@ FastCurl.stream_get(urls, connections: 50) do |index, response|
76
76
  end
77
77
  ```
78
78
 
79
+ ### Retry functionality (v0.2.0+)
80
+
81
+ ```ruby
82
+ # Automatic retry on network errors (timeout, connection issues)
83
+ results = FastCurl.get([
84
+ { url: "https://unreliable-api.com/data" }
85
+ ], retries: 3, retry_delay: 1000) # 3 retries with 1s delay
86
+
87
+ # Retry on specific HTTP status codes
88
+ results = FastCurl.get([
89
+ { url: "https://api.example.com/data" }
90
+ ], retries: 2, retry_codes: [500, 502, 503], retry_delay: 500)
91
+
92
+ # Disable retries (default is 1 retry)
93
+ results = FastCurl.get(urls, retries: 0)
94
+ ```
95
+
79
96
  ### Inside Async
80
97
 
81
98
  ```ruby
@@ -119,6 +136,9 @@ end
119
136
  |---|---|---|
120
137
  | `connections` | 20 | Max parallel connections |
121
138
  | `timeout` | 30 | Per-request timeout in seconds |
139
+ | `retries` | 1 | Number of retry attempts (0-10) |
140
+ | `retry_delay` | 0 | Delay between retries in milliseconds |
141
+ | `retry_codes` | [] | HTTP status codes to retry on |
122
142
 
123
143
  ## Performance
124
144
 
@@ -11,12 +11,21 @@
11
11
  #define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
12
12
  #define MAX_REDIRECTS 5
13
13
  #define MAX_TIMEOUT 300
14
+ #define MAX_RETRIES 10
15
+ #define DEFAULT_RETRIES 1
16
+ #define DEFAULT_RETRY_DELAY 0
14
17
  #define INITIAL_BUF_CAP 8192
15
18
  #define INITIAL_HEADER_CAP 16
16
19
  #define POLL_TIMEOUT_MS 50
17
20
  #define FIBER_POLL_TIMEOUT_MS 10
18
21
  #define HEADER_LINE_BUF_SIZE 512
19
22
 
23
+ static const CURLcode DEFAULT_RETRYABLE_CURLE[] = {
24
+ CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEDOUT, CURLE_SEND_ERROR, CURLE_RECV_ERROR,
25
+ CURLE_GOT_NOTHING, CURLE_PARTIAL_FILE, CURLE_SSL_CONNECT_ERROR};
26
+ #define DEFAULT_RETRYABLE_CURLE_COUNT \
27
+ (int)(sizeof(DEFAULT_RETRYABLE_CURLE) / sizeof(DEFAULT_RETRYABLE_CURLE[0]))
28
+
20
29
  static ID id_status;
21
30
  static ID id_headers;
22
31
  static ID id_body;
@@ -27,6 +36,9 @@ static ID id_timeout;
27
36
  static ID id_connections;
28
37
  static ID id_count;
29
38
  static ID id_keys;
39
+ static ID id_retries;
40
+ static ID id_retry_delay;
41
+ static ID id_retry_codes;
30
42
  static VALUE sym_status;
31
43
  static VALUE sym_headers;
32
44
  static VALUE sym_body;
@@ -36,6 +48,9 @@ static VALUE sym_method;
36
48
  static VALUE sym_timeout;
37
49
  static VALUE sym_connections;
38
50
  static VALUE sym_count;
51
+ static VALUE sym_retries;
52
+ static VALUE sym_retry_delay;
53
+ static VALUE sym_retry_codes;
39
54
 
40
55
  typedef struct {
41
56
  char *data;
@@ -60,6 +75,10 @@ static inline void buffer_free(buffer_t *buf) {
60
75
  buf->cap = 0;
61
76
  }
62
77
 
78
+ static inline void buffer_reset(buffer_t *buf) {
79
+ buf->len = 0;
80
+ }
81
+
63
82
  static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
64
83
  buffer_t *buf = (buffer_t *)userdata;
65
84
  size_t total = size * nmemb;
@@ -114,6 +133,12 @@ static void header_list_free(header_list_t *h) {
114
133
  h->cap = 0;
115
134
  }
116
135
 
136
+ static void header_list_reset(header_list_t *h) {
137
+ for (int i = 0; i < h->count; i++)
138
+ free(h->entries[i].str);
139
+ h->count = 0;
140
+ }
141
+
117
142
  static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
118
143
  header_list_t *h = (header_list_t *)userdata;
119
144
  size_t total = size * nmemb;
@@ -154,6 +179,8 @@ typedef struct {
154
179
  header_list_t headers;
155
180
  struct curl_slist *req_headers;
156
181
  int done;
182
+ CURLcode curl_result;
183
+ long http_status;
157
184
  } request_ctx_t;
158
185
 
159
186
  static inline void request_ctx_init(request_ctx_t *ctx, int index) {
@@ -163,6 +190,8 @@ static inline void request_ctx_init(request_ctx_t *ctx, int index) {
163
190
  header_list_init(&ctx->headers);
164
191
  ctx->req_headers = NULL;
165
192
  ctx->done = 0;
193
+ ctx->curl_result = CURLE_OK;
194
+ ctx->http_status = 0;
166
195
  }
167
196
 
168
197
  static void request_ctx_free(request_ctx_t *ctx) {
@@ -178,6 +207,26 @@ static void request_ctx_free(request_ctx_t *ctx) {
178
207
  }
179
208
  }
180
209
 
210
+ static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
211
+ if (ctx->easy) {
212
+ curl_easy_cleanup(ctx->easy);
213
+ ctx->easy = NULL;
214
+ }
215
+ buffer_reset(&ctx->body);
216
+ header_list_reset(&ctx->headers);
217
+ if (ctx->req_headers) {
218
+ curl_slist_free_all(ctx->req_headers);
219
+ ctx->req_headers = NULL;
220
+ }
221
+ ctx->easy = curl_easy_init();
222
+ if (!ctx->easy)
223
+ return 0;
224
+ ctx->done = 0;
225
+ ctx->curl_result = CURLE_OK;
226
+ ctx->http_status = 0;
227
+ return 1;
228
+ }
229
+
181
230
  typedef struct {
182
231
  CURLM *multi;
183
232
  request_ctx_t *requests;
@@ -187,6 +236,13 @@ typedef struct {
187
236
  int max_connections;
188
237
  } multi_session_t;
189
238
 
239
+ typedef struct {
240
+ int max_retries;
241
+ long retry_delay_ms;
242
+ int *retry_http_codes;
243
+ int retry_http_count;
244
+ } retry_config_t;
245
+
190
246
  static VALUE build_response(request_ctx_t *ctx) {
191
247
  long status = 0;
192
248
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
@@ -318,8 +374,8 @@ static CURLcode setup_method_and_body(CURL *easy, VALUE method, VALUE body) {
318
374
  }
319
375
 
320
376
  if (!NIL_P(body)) {
321
- CURL_SETOPT_CHECK(easy, CURLOPT_POSTFIELDS, StringValuePtr(body));
322
377
  CURL_SETOPT_CHECK(easy, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(body));
378
+ CURL_SETOPT_CHECK(easy, CURLOPT_COPYPOSTFIELDS, StringValuePtr(body));
323
379
  }
324
380
 
325
381
  return CURLE_OK;
@@ -480,25 +536,26 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
480
536
  if (!ctx || ctx->done)
481
537
  continue;
482
538
  ctx->done = 1;
483
-
484
- VALUE response;
539
+ ctx->curl_result = msg->data.result;
485
540
  if (msg->data.result == CURLE_OK) {
486
- response = build_response(ctx);
487
- } else {
488
- response = build_error_response_with_code(curl_easy_strerror(msg->data.result),
489
- (int)msg->data.result);
541
+ curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &ctx->http_status);
490
542
  }
491
543
 
492
- VALUE pair = rb_ary_new_from_args(2, INT2NUM(ctx->index), response);
493
-
494
544
  if (cctx->stream) {
545
+ VALUE response;
546
+ if (msg->data.result == CURLE_OK) {
547
+ response = build_response(ctx);
548
+ } else {
549
+ response = build_error_response_with_code(curl_easy_strerror(msg->data.result),
550
+ (int)msg->data.result);
551
+ }
552
+ VALUE pair = rb_ary_new_from_args(2, INT2NUM(ctx->index), response);
495
553
  rb_yield(pair);
496
- } else if (!NIL_P(cctx->results)) {
497
- rb_ary_store(cctx->results, ctx->index, pair);
554
+ cctx->completed++;
555
+ } else {
556
+ cctx->completed++;
498
557
  }
499
558
 
500
- cctx->completed++;
501
-
502
559
  if (cctx->target > 0 && cctx->completed >= cctx->target)
503
560
  return 1;
504
561
  }
@@ -506,9 +563,102 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
506
563
  return 0;
507
564
  }
508
565
 
509
- static void parse_options(VALUE options, long *timeout, int *max_conn) {
566
+ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx) {
567
+ if (has_fiber_scheduler()) {
568
+ for (;;) {
569
+ CURLMcode mc = curl_multi_perform(session->multi, &session->still_running);
570
+ if (mc != CURLM_OK)
571
+ break;
572
+ if (process_completed(session, cctx))
573
+ break;
574
+ if (session->still_running == 0)
575
+ break;
576
+
577
+ int numfds = 0;
578
+ curl_multi_poll(session->multi, NULL, 0, FIBER_POLL_TIMEOUT_MS, &numfds);
579
+ rb_thread_schedule();
580
+ }
581
+ process_completed(session, cctx);
582
+ } else {
583
+ if (cctx->stream || cctx->target > 0) {
584
+ curl_multi_perform(session->multi, &session->still_running);
585
+ while (session->still_running > 0) {
586
+ rb_thread_call_without_gvl(poll_without_gvl, session, unblock_perform, session);
587
+ if (process_completed(session, cctx))
588
+ break;
589
+ }
590
+ process_completed(session, cctx);
591
+ } else {
592
+ session->still_running = 1;
593
+ curl_multi_perform(session->multi, &session->still_running);
594
+ rb_thread_call_without_gvl(perform_without_gvl, session, unblock_perform, session);
595
+ process_completed(session, cctx);
596
+ }
597
+ }
598
+ }
599
+
600
+ static int is_default_retryable_curle(CURLcode code) {
601
+ for (int i = 0; i < DEFAULT_RETRYABLE_CURLE_COUNT; i++) {
602
+ if (DEFAULT_RETRYABLE_CURLE[i] == code)
603
+ return 1;
604
+ }
605
+ return 0;
606
+ }
607
+
608
+ static int should_retry(request_ctx_t *ctx, retry_config_t *retry_cfg) {
609
+ if (ctx->curl_result != CURLE_OK) {
610
+ return is_default_retryable_curle(ctx->curl_result);
611
+ }
612
+
613
+ if (retry_cfg->retry_http_count > 0) {
614
+ for (int i = 0; i < retry_cfg->retry_http_count; i++) {
615
+ if (retry_cfg->retry_http_codes[i] == (int)ctx->http_status)
616
+ return 1;
617
+ }
618
+ }
619
+
620
+ return 0;
621
+ }
622
+
623
+ typedef struct {
624
+ long delay_ms;
625
+ } sleep_arg_t;
626
+
627
+ static void *sleep_without_gvl(void *arg) {
628
+ sleep_arg_t *sa = (sleep_arg_t *)arg;
629
+ struct timespec ts;
630
+ ts.tv_sec = sa->delay_ms / 1000;
631
+ ts.tv_nsec = (sa->delay_ms % 1000) * 1000000L;
632
+ nanosleep(&ts, NULL);
633
+ return NULL;
634
+ }
635
+
636
+ static void retry_delay_sleep(long delay_ms) {
637
+ if (delay_ms <= 0)
638
+ return;
639
+
640
+ if (has_fiber_scheduler()) {
641
+ long remaining = delay_ms;
642
+ while (remaining > 0) {
643
+ long chunk = remaining > FIBER_POLL_TIMEOUT_MS ? FIBER_POLL_TIMEOUT_MS : remaining;
644
+ sleep_arg_t sa = {.delay_ms = chunk};
645
+ sleep_without_gvl(&sa);
646
+ rb_thread_schedule();
647
+ remaining -= chunk;
648
+ }
649
+ } else {
650
+ sleep_arg_t sa = {.delay_ms = delay_ms};
651
+ rb_thread_call_without_gvl(sleep_without_gvl, &sa, unblock_perform, NULL);
652
+ }
653
+ }
654
+
655
+ static void parse_options(VALUE options, long *timeout, int *max_conn, retry_config_t *retry_cfg) {
510
656
  *timeout = 30;
511
657
  *max_conn = 20;
658
+ retry_cfg->max_retries = DEFAULT_RETRIES;
659
+ retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
660
+ retry_cfg->retry_http_codes = NULL;
661
+ retry_cfg->retry_http_count = 0;
512
662
 
513
663
  if (NIL_P(options) || !rb_obj_is_kind_of(options, rb_cHash))
514
664
  return;
@@ -532,6 +682,40 @@ static void parse_options(VALUE options, long *timeout, int *max_conn) {
532
682
  conn_val = 20;
533
683
  *max_conn = conn_val;
534
684
  }
685
+
686
+ VALUE r = rb_hash_aref(options, sym_retries);
687
+ if (!NIL_P(r)) {
688
+ int retries_val = NUM2INT(r);
689
+ if (retries_val < 0)
690
+ retries_val = 0;
691
+ if (retries_val > MAX_RETRIES)
692
+ retries_val = MAX_RETRIES;
693
+ retry_cfg->max_retries = retries_val;
694
+ }
695
+
696
+ VALUE rd = rb_hash_aref(options, sym_retry_delay);
697
+ if (!NIL_P(rd)) {
698
+ long delay_val = NUM2LONG(rd);
699
+ if (delay_val < 0)
700
+ delay_val = 0;
701
+ if (delay_val > 30000)
702
+ delay_val = 30000;
703
+ retry_cfg->retry_delay_ms = delay_val;
704
+ }
705
+
706
+ VALUE rc = rb_hash_aref(options, sym_retry_codes);
707
+ if (!NIL_P(rc) && rb_obj_is_kind_of(rc, rb_cArray)) {
708
+ int len = (int)RARRAY_LEN(rc);
709
+ if (len > 0) {
710
+ retry_cfg->retry_http_codes = malloc(sizeof(int) * len);
711
+ if (retry_cfg->retry_http_codes) {
712
+ retry_cfg->retry_http_count = len;
713
+ for (int i = 0; i < len; i++) {
714
+ retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(rc, i));
715
+ }
716
+ }
717
+ }
718
+ }
535
719
  }
536
720
 
537
721
  static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
@@ -542,7 +726,19 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
542
726
 
543
727
  long timeout_sec;
544
728
  int max_conn;
545
- parse_options(options, &timeout_sec, &max_conn);
729
+ retry_config_t retry_cfg;
730
+ parse_options(options, &timeout_sec, &max_conn, &retry_cfg);
731
+
732
+ /* Disable retries for stream and first_execute modes */
733
+ if (stream || target > 0) {
734
+ if (retry_cfg.max_retries > 0 && stream)
735
+ rb_warn(
736
+ "FastCurl: retries are not supported in stream_execute, ignoring retries option");
737
+ if (retry_cfg.max_retries > 0 && target > 0)
738
+ rb_warn(
739
+ "FastCurl: retries are not supported in first_execute, ignoring retries option");
740
+ retry_cfg.max_retries = 0;
741
+ }
546
742
 
547
743
  multi_session_t session;
548
744
  session.multi = curl_multi_init();
@@ -559,9 +755,20 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
559
755
  session.requests = calloc(count, sizeof(request_ctx_t));
560
756
  if (!session.requests) {
561
757
  curl_multi_cleanup(session.multi);
758
+ if (retry_cfg.retry_http_codes)
759
+ free(retry_cfg.retry_http_codes);
562
760
  rb_raise(rb_eNoMemError, "failed to allocate request contexts");
563
761
  }
564
762
 
763
+ int *invalid = calloc(count, sizeof(int));
764
+ if (!invalid) {
765
+ free(session.requests);
766
+ curl_multi_cleanup(session.multi);
767
+ if (retry_cfg.retry_http_codes)
768
+ free(retry_cfg.retry_http_codes);
769
+ rb_raise(rb_eNoMemError, "failed to allocate tracking array");
770
+ }
771
+
565
772
  int valid_requests = 0;
566
773
  for (int i = 0; i < count; i++) {
567
774
  VALUE req = rb_ary_entry(requests, i);
@@ -569,12 +776,14 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
569
776
 
570
777
  if (!setup_easy_handle(&session.requests[i], req, timeout_sec)) {
571
778
  session.requests[i].done = 1;
779
+ invalid[i] = 1;
572
780
  continue;
573
781
  }
574
782
 
575
783
  CURLMcode mc = curl_multi_add_handle(session.multi, session.requests[i].easy);
576
784
  if (mc != CURLM_OK) {
577
785
  session.requests[i].done = 1;
786
+ invalid[i] = 1;
578
787
  continue;
579
788
  }
580
789
 
@@ -595,45 +804,103 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
595
804
  rb_ary_store(cctx.results, i, Qnil);
596
805
  }
597
806
 
598
- if (has_fiber_scheduler()) {
599
- for (;;) {
600
- CURLMcode mc = curl_multi_perform(session.multi, &session.still_running);
601
- if (mc != CURLM_OK)
807
+ run_multi_loop(&session, &cctx);
808
+
809
+ /* === Retry loop (execute mode only) === */
810
+ if (!stream && retry_cfg.max_retries > 0) {
811
+ int prev_all_failed = 0;
812
+
813
+ for (int attempt = 0; attempt < retry_cfg.max_retries; attempt++) {
814
+ int retry_count = 0;
815
+ int *retry_indices = malloc(sizeof(int) * count);
816
+ if (!retry_indices)
602
817
  break;
603
- if (process_completed(&session, &cctx))
818
+
819
+ for (int i = 0; i < count; i++) {
820
+ if (invalid[i])
821
+ continue;
822
+ if (!session.requests[i].done)
823
+ continue;
824
+ if (should_retry(&session.requests[i], &retry_cfg)) {
825
+ retry_indices[retry_count++] = i;
826
+ }
827
+ }
828
+
829
+ if (retry_count == 0) {
830
+ free(retry_indices);
604
831
  break;
605
- if (session.still_running == 0)
832
+ }
833
+
834
+ int done_count = 0;
835
+ for (int i = 0; i < count; i++) {
836
+ if (!invalid[i] && session.requests[i].done)
837
+ done_count++;
838
+ }
839
+
840
+ int all_failed_this_round = (retry_count == done_count);
841
+
842
+ if (all_failed_this_round && prev_all_failed) {
843
+ free(retry_indices);
606
844
  break;
845
+ }
607
846
 
608
- int numfds = 0;
609
- curl_multi_poll(session.multi, NULL, 0, FIBER_POLL_TIMEOUT_MS, &numfds);
610
- rb_thread_schedule();
611
- }
612
- process_completed(&session, &cctx);
613
- } else {
614
- if (stream || target > 0) {
615
- curl_multi_perform(session.multi, &session.still_running);
616
- while (session.still_running > 0) {
617
- rb_thread_call_without_gvl(poll_without_gvl, &session, unblock_perform, &session);
618
- if (process_completed(&session, &cctx))
619
- break;
847
+ prev_all_failed = all_failed_this_round;
848
+
849
+ retry_delay_sleep(retry_cfg.retry_delay_ms);
850
+
851
+ for (int r = 0; r < retry_count; r++) {
852
+ int idx = retry_indices[r];
853
+ request_ctx_t *ctx = &session.requests[idx];
854
+
855
+ curl_multi_remove_handle(session.multi, ctx->easy);
856
+
857
+ if (!request_ctx_reset_for_retry(ctx)) {
858
+ ctx->done = 1;
859
+ invalid[idx] = 1;
860
+ continue;
861
+ }
862
+
863
+ VALUE req = rb_ary_entry(requests, idx);
864
+ if (!setup_easy_handle(ctx, req, timeout_sec)) {
865
+ ctx->done = 1;
866
+ invalid[idx] = 1;
867
+ continue;
868
+ }
869
+
870
+ CURLMcode mc = curl_multi_add_handle(session.multi, ctx->easy);
871
+ if (mc != CURLM_OK) {
872
+ ctx->done = 1;
873
+ invalid[idx] = 1;
874
+ }
620
875
  }
621
- process_completed(&session, &cctx);
622
- } else {
623
- session.still_running = 1;
624
- curl_multi_perform(session.multi, &session.still_running);
625
- rb_thread_call_without_gvl(perform_without_gvl, &session, unblock_perform, &session);
626
- process_completed(&session, &cctx);
876
+
877
+ free(retry_indices);
878
+
879
+ cctx.completed = 0;
880
+ run_multi_loop(&session, &cctx);
627
881
  }
628
882
  }
629
883
 
630
884
  if (!stream) {
631
885
  for (int i = 0; i < count; i++) {
632
- if (session.requests[i].done && rb_ary_entry(cctx.results, i) == Qnil) {
886
+ request_ctx_t *ctx = &session.requests[i];
887
+
888
+ if (invalid[i]) {
633
889
  VALUE error_response = build_error_response("Invalid request configuration");
634
890
  VALUE pair = rb_ary_new_from_args(2, INT2NUM(i), error_response);
635
891
  rb_ary_store(cctx.results, i, pair);
892
+ continue;
893
+ }
894
+
895
+ VALUE response;
896
+ if (ctx->curl_result == CURLE_OK) {
897
+ response = build_response(ctx);
898
+ } else {
899
+ response = build_error_response_with_code(curl_easy_strerror(ctx->curl_result),
900
+ (int)ctx->curl_result);
636
901
  }
902
+ VALUE pair = rb_ary_new_from_args(2, INT2NUM(i), response);
903
+ rb_ary_store(cctx.results, i, pair);
637
904
  }
638
905
  }
639
906
 
@@ -642,7 +909,10 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
642
909
  request_ctx_free(&session.requests[i]);
643
910
  }
644
911
  free(session.requests);
912
+ free(invalid);
645
913
  curl_multi_cleanup(session.multi);
914
+ if (retry_cfg.retry_http_codes)
915
+ free(retry_cfg.retry_http_codes);
646
916
 
647
917
  return stream ? Qnil : cctx.results;
648
918
  }
@@ -690,6 +960,9 @@ void Init_fast_curl(void) {
690
960
  id_connections = rb_intern("connections");
691
961
  id_count = rb_intern("count");
692
962
  id_keys = rb_intern("keys");
963
+ id_retries = rb_intern("retries");
964
+ id_retry_delay = rb_intern("retry_delay");
965
+ id_retry_codes = rb_intern("retry_codes");
693
966
 
694
967
  sym_status = ID2SYM(id_status);
695
968
  rb_gc_register_address(&sym_status);
@@ -709,6 +982,12 @@ void Init_fast_curl(void) {
709
982
  rb_gc_register_address(&sym_connections);
710
983
  sym_count = ID2SYM(id_count);
711
984
  rb_gc_register_address(&sym_count);
985
+ sym_retries = ID2SYM(id_retries);
986
+ rb_gc_register_address(&sym_retries);
987
+ sym_retry_delay = ID2SYM(id_retry_delay);
988
+ rb_gc_register_address(&sym_retry_delay);
989
+ sym_retry_codes = ID2SYM(id_retry_codes);
990
+ rb_gc_register_address(&sym_retry_codes);
712
991
 
713
992
  VALUE mFastCurl = rb_define_module("FastCurl");
714
993
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCurl
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_curl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - roman-haidarov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 2026-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -96,7 +96,8 @@ dependencies:
96
96
  version: '1.8'
97
97
  description: Parallel HTTP requests via libcurl curl_multi API. Releases GVL during
98
98
  I/O, compatible with Async gem and Fiber scheduler. Supports execute (all), first_execute
99
- (first N), stream_execute (yield as ready).
99
+ (first N), stream_execute (yield as ready). Built-in retry functionality for network
100
+ errors and custom HTTP status codes.
100
101
  email:
101
102
  - roman.haidarov@gmail.com
102
103
  executables: []