fast_curl 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32aca3a9aec734c69bc40aa9b84b519aeddc15defc3ad3a9e8f278f63d9f0ffb
4
- data.tar.gz: 8037377b3d3e5ab02f4171c3f4ae56e6f42552a6cb5c6d7e2f5fd54921e2cc71
3
+ metadata.gz: 576cbc25d05c1a0857f8f762c3ed3744ad84c65457669c63bd5da62d904e72a0
4
+ data.tar.gz: b13fcb38e809a48ac3c85deb14a5ea56dbedb37803cad3cb3c2218a7bf88d483
5
5
  SHA512:
6
- metadata.gz: 670eefe8b9fa597e947ae0858bda833d35d15914fbd62e3093483cdf8c2c68560c121586c4b01eec75f81d3a4f6d49d04829d5711180178247f051cb510c8cd6
7
- data.tar.gz: 709792e880465cc09de983246936ce66a843f5b7899e69ac6f5a390cef35b4cdc8baaf1dbda36abe39627cc2fe457e994076329ab746887d000d1a089e649af7
6
+ metadata.gz: c3fed870f48939b5d016c9951d1c2503941496706af5fa0d157c964269a925190187e1b3378dcd8a43dea5c7a7d4c56bbf8edf0923f325fd74323b0166d97361
7
+ data.tar.gz: 6f13f0839354222ed5ff8ede5acf39137904226ddad7f860b8b08e3f2b3dcff6fda6b65d21ed8324673f4fda1cc24e8cf51b13a32a6681740bc012507e36072c
data/README.md CHANGED
@@ -8,6 +8,7 @@ Ultra-fast parallel HTTP client for Ruby. C extension built on libcurl `curl_mul
8
8
  - **GVL release** — `rb_thread_call_without_gvl` during I/O, other Ruby threads keep running
9
9
  - **Fiber scheduler compatible** — works inside `Async do ... end` without blocking other fibers
10
10
  - **Three modes**: execute (all), first_execute (first N), stream_execute (yield as ready)
11
+ - **Lazy Enumerable sources** — bounded request preparation for large or infinite streams
11
12
  - **Zero dependencies** — only libcurl (available everywhere)
12
13
 
13
14
  ## Installation
@@ -81,6 +82,34 @@ Query parameters can be passed separately:
81
82
  FastCurl.get([{ url: "https://api.example.com/search", params: { q: "ruby", page: 2 } }])
82
83
  ```
83
84
 
85
+ ### Lazy / bounded Enumerable sources
86
+
87
+ `Array` keeps the existing fast path. Any other object responding to `#each` is
88
+ consumed lazily, so large request sets do not need to be materialized first:
89
+
90
+ ```ruby
91
+ requests = Enumerator.new do |y|
92
+ 1_000_000.times do |i|
93
+ y << { url: "https://api.example.com/items/#{i}" }
94
+ end
95
+ end
96
+
97
+ FastCurl.stream_get(requests, connections: 20, buffer: 20) do |index, response|
98
+ puts "#{index}: #{response[:status]}"
99
+ end
100
+ ```
101
+
102
+ For lazy sources, at most `connections + buffer` requests are retained by
103
+ `fast_curl`. The source itself may have produced one additional item before
104
+ backpressure is applied, so a generator can observe a maximum look-ahead of
105
+ `connections + buffer + 1`. `buffer` defaults to `connections`.
106
+
107
+ `FastCurl.get` still returns all results in input order, so its result array is
108
+ necessarily O(N). Use `stream_get` when the whole pipeline must stay bounded.
109
+ Source exceptions and stream callback exceptions unwind the native multi loop
110
+ and release active curl handles; an `ensure` in the source is also unwound on
111
+ early completed `first_*` calls.
112
+
84
113
  ### First N responses (cancel the rest)
85
114
 
86
115
  ```ruby
@@ -91,6 +120,19 @@ result = FastCurl.first_get([
91
120
  ], count: 1)
92
121
  ```
93
122
 
123
+ `accept:` can keep the race running until a response satisfies a predicate:
124
+
125
+ ```ruby
126
+ result = FastCurl.first_get(
127
+ mirrors,
128
+ connections: 3,
129
+ accept: ->(response) { response[:status].between?(200, 299) }
130
+ )
131
+ ```
132
+
133
+ The predicate receives the response Hash. Rejected responses do not count
134
+ toward `count`.
135
+
94
136
  ### Stream responses as they arrive
95
137
 
96
138
  ```ruby
@@ -195,6 +237,7 @@ folded and is **always** an Array, even for a single cookie.
195
237
  | Option | Default | Description |
196
238
  |---|---|---|
197
239
  | `connections` | 20 | Max parallel connections |
240
+ | `buffer` | `connections` | Lazy-source prefetch window; ignored for already-materialized Arrays |
198
241
  | `timeout` | 30 | Timeout for a single attempt, in seconds (1-300) |
199
242
  | `connect_timeout` | 10000 | Connection phase timeout, in milliseconds |
200
243
  | `total_timeout` | none | Wall-clock budget for the whole call, in milliseconds |
@@ -204,6 +247,7 @@ folded and is **always** an Array, even for a single cookie.
204
247
  | `retry_non_idempotent` | false | Also retry POST and PATCH |
205
248
  | `follow_redirects` | true | Follow `Location` headers |
206
249
  | `max_redirects` | 5 | Redirect limit (0-100) |
250
+ | `accept` | none | `first_*` predicate receiving the response Hash |
207
251
 
208
252
  DNS results and TLS sessions are cached process-wide, so repeated calls to the
209
253
  same host skip resolution and can resume TLS. TCP connections are pooled only
@@ -32,6 +32,7 @@
32
32
  #define MAX_RETRIES 10
33
33
  #define MAX_REQUESTS 10000
34
34
  #define MAX_CONNECTIONS 100
35
+ #define MAX_BUFFER 10000
35
36
  #define MAX_RETRY_DELAY_MS 30000
36
37
  #define DEFAULT_RETRIES 1
37
38
  #define DEFAULT_RETRY_DELAY 100
@@ -72,6 +73,8 @@ typedef enum {
72
73
  KEY_EFFECTIVE_URL,
73
74
  KEY_ERROR,
74
75
  KEY_ATTEMPTS,
76
+ KEY_BUFFER,
77
+ KEY_ACCEPT,
75
78
  KEY_LAST
76
79
  } key_id_t;
77
80
 
@@ -101,6 +104,8 @@ static const char *const KEY_NAMES[KEY_LAST] = {
101
104
  "effective_url",
102
105
  "error",
103
106
  "attempts",
107
+ "buffer",
108
+ "accept",
104
109
  };
105
110
 
106
111
  typedef enum {
@@ -320,7 +325,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
320
325
 
321
326
  typedef struct {
322
327
  CURL *easy;
323
- int index;
328
+ long index;
324
329
  buffer_t body;
325
330
  header_list_t headers;
326
331
  struct curl_slist *req_headers;
@@ -341,7 +346,7 @@ static VALUE fast_validation_error = Qnil;
341
346
  (ctx)->setup_error_fatal = (fatal); \
342
347
  } while (0)
343
348
 
344
- static inline void request_ctx_init(request_ctx_t *ctx, int index) {
349
+ static inline void request_ctx_init(request_ctx_t *ctx, long index) {
345
350
  ctx->easy = NULL;
346
351
  ctx->index = index;
347
352
  buffer_init(&ctx->body);
@@ -961,30 +966,52 @@ static void unblock_perform(void *arg) {
961
966
 
962
967
  typedef struct {
963
968
  VALUE results;
969
+ VALUE accept;
964
970
  int completed;
965
971
  int target;
966
972
  int stream;
967
973
  } completion_ctx_t;
968
974
 
969
- static VALUE build_result_pair(int index, VALUE response) {
970
- return rb_ary_new_from_args(2, INT2NUM(index), response);
975
+ static VALUE build_result_pair(long index, VALUE response) {
976
+ return rb_ary_new_from_args(2, LONG2NUM(index), response);
971
977
  }
972
978
 
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));
979
+ static int response_accepted(completion_ctx_t *cctx, VALUE response) {
980
+ if (cctx->target <= 0 || NIL_P(cctx->accept))
981
+ return 1;
978
982
 
979
- if (cctx->stream)
980
- rb_yield(pair);
981
- else
982
- rb_ary_push(cctx->results, pair);
983
+ return RTEST(rb_funcall(cctx->accept, rb_intern("call"), 1, response));
984
+ }
983
985
 
986
+ static int record_response(completion_ctx_t *cctx, long index, VALUE response) {
987
+ VALUE pair = build_result_pair(index, response);
988
+
989
+ if (cctx->stream) {
990
+ rb_yield(pair);
984
991
  cctx->completed++;
985
- if (cctx->target > 0 && cctx->completed >= cctx->target)
986
- return 1;
992
+ return 0;
987
993
  }
994
+
995
+ if (cctx->target > 0) {
996
+ if (!response_accepted(cctx, response))
997
+ return 0;
998
+
999
+ rb_ary_push(cctx->results, pair);
1000
+ cctx->completed++;
1001
+ return cctx->completed >= cctx->target;
1002
+ }
1003
+
1004
+ rb_ary_store(cctx->results, index, pair);
1005
+ cctx->completed++;
1006
+ return 0;
1007
+ }
1008
+
1009
+ static int record_immediate_error(completion_ctx_t *cctx, long index, const char *message,
1010
+ int attempts) {
1011
+ if (cctx->stream || cctx->target > 0)
1012
+ return record_response(cctx, index,
1013
+ build_error_response(message, ERR_INVALID_REQUEST, attempts));
1014
+
988
1015
  return 0;
989
1016
  }
990
1017
 
@@ -1019,17 +1046,11 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
1019
1046
  ? build_response(ctx)
1020
1047
  : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1021
1048
  (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);
1049
+ if (record_response(cctx, ctx->index, response))
1050
+ return 1;
1051
+ } else {
1052
+ cctx->completed++;
1028
1053
  }
1029
-
1030
- cctx->completed++;
1031
- if (cctx->target > 0 && cctx->completed >= cctx->target)
1032
- return 1;
1033
1054
  }
1034
1055
 
1035
1056
  return 0;
@@ -1417,10 +1438,14 @@ static VALUE internal_execute_body(VALUE arg) {
1417
1438
 
1418
1439
  completion_ctx_t cctx;
1419
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;
1420
1442
  cctx.completed = 0;
1421
1443
  cctx.target = target;
1422
1444
  cctx.stream = stream;
1423
1445
 
1446
+ if (!NIL_P(cctx.accept) && !rb_respond_to(cctx.accept, rb_intern("call")))
1447
+ rb_raise(rb_eArgError, "accept must respond to #call");
1448
+
1424
1449
  if (!stream && target <= 0) {
1425
1450
  for (int i = 0; i < count; i++)
1426
1451
  rb_ary_store(cctx.results, i, Qnil);
@@ -1522,6 +1547,482 @@ static VALUE internal_execute_body(VALUE arg) {
1522
1547
  return stream ? Qnil : cctx.results;
1523
1548
  }
1524
1549
 
1550
+ typedef enum {
1551
+ LAZY_SLOT_FREE,
1552
+ LAZY_SLOT_PENDING,
1553
+ LAZY_SLOT_ACTIVE,
1554
+ LAZY_SLOT_RETRY_WAIT
1555
+ } lazy_slot_state_t;
1556
+
1557
+ typedef struct {
1558
+ multi_session_t session;
1559
+ request_options_t opts;
1560
+ retry_config_t retry_cfg;
1561
+ completion_ctx_t cctx;
1562
+ VALUE source;
1563
+ VALUE anchors;
1564
+ int *states;
1565
+ int *free_slots;
1566
+ int free_count;
1567
+ int *pending_slots;
1568
+ int pending_head;
1569
+ int pending_tail;
1570
+ int pending_count;
1571
+ long long *retry_at_ms;
1572
+ int capacity;
1573
+ int buffer_size;
1574
+ int occupied_count;
1575
+ long next_index;
1576
+ int deadline_hit;
1577
+ int stop;
1578
+ } lazy_execute_ctx_t;
1579
+
1580
+ static int lazy_pending_push(lazy_execute_ctx_t *ctx, int slot) {
1581
+ if (ctx->pending_count >= ctx->capacity)
1582
+ return 0;
1583
+
1584
+ ctx->pending_slots[ctx->pending_tail] = slot;
1585
+ ctx->pending_tail = (ctx->pending_tail + 1) % ctx->capacity;
1586
+ ctx->pending_count++;
1587
+ return 1;
1588
+ }
1589
+
1590
+ static int lazy_pending_pop(lazy_execute_ctx_t *ctx) {
1591
+ if (ctx->pending_count == 0)
1592
+ return -1;
1593
+
1594
+ int slot = ctx->pending_slots[ctx->pending_head];
1595
+ ctx->pending_head = (ctx->pending_head + 1) % ctx->capacity;
1596
+ ctx->pending_count--;
1597
+ return slot;
1598
+ }
1599
+
1600
+ static void lazy_release_slot(lazy_execute_ctx_t *ctx, int slot) {
1601
+ request_ctx_free(&ctx->session.requests[slot]);
1602
+ rb_ary_store(ctx->anchors, slot, Qnil);
1603
+ ctx->states[slot] = LAZY_SLOT_FREE;
1604
+ ctx->retry_at_ms[slot] = 0;
1605
+ ctx->free_slots[ctx->free_count++] = slot;
1606
+ if (ctx->occupied_count > 0)
1607
+ ctx->occupied_count--;
1608
+ }
1609
+
1610
+ static FAST_CURL_NORETURN void lazy_raise_setup_error(request_ctx_t *request) {
1611
+ VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
1612
+ rb_raise(klass, "%s (request %ld)",
1613
+ request->setup_error ? request->setup_error : "Invalid request configuration",
1614
+ request->index);
1615
+ }
1616
+
1617
+ static int lazy_finalize_error(lazy_execute_ctx_t *ctx, int slot, const char *message) {
1618
+ request_ctx_t *request = &ctx->session.requests[slot];
1619
+ VALUE response = build_error_response(message, ERR_INVALID_REQUEST, request->attempts);
1620
+ int stop = record_response(&ctx->cctx, request->index, response);
1621
+ lazy_release_slot(ctx, slot);
1622
+ return stop;
1623
+ }
1624
+
1625
+ static int lazy_activate_pending(lazy_execute_ctx_t *ctx) {
1626
+ while (ctx->session.active_count < ctx->session.max_connections && ctx->pending_count > 0) {
1627
+ int slot = lazy_pending_pop(ctx);
1628
+ request_ctx_t *request = &ctx->session.requests[slot];
1629
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1630
+
1631
+ if (!request_ctx_prepare_easy(request)) {
1632
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle"))
1633
+ return 1;
1634
+ continue;
1635
+ }
1636
+
1637
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1638
+ if (request->setup_error_fatal)
1639
+ lazy_raise_setup_error(request);
1640
+ if (lazy_finalize_error(ctx, slot,
1641
+ request->setup_error ? request->setup_error
1642
+ : "Invalid request configuration"))
1643
+ return 1;
1644
+ continue;
1645
+ }
1646
+
1647
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1648
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the request"))
1649
+ return 1;
1650
+ continue;
1651
+ }
1652
+
1653
+ request->attempts++;
1654
+ request->active = 1;
1655
+ request->done = 0;
1656
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1657
+ ctx->session.active_count++;
1658
+ }
1659
+
1660
+ return 0;
1661
+ }
1662
+
1663
+ static int lazy_activate_retries(lazy_execute_ctx_t *ctx) {
1664
+ long long now = fast_now_ms();
1665
+
1666
+ for (int slot = 0;
1667
+ slot < ctx->capacity && ctx->session.active_count < ctx->session.max_connections; slot++) {
1668
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT || ctx->retry_at_ms[slot] > now)
1669
+ continue;
1670
+
1671
+ request_ctx_t *request = &ctx->session.requests[slot];
1672
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1673
+
1674
+ if (!request_ctx_reset_for_retry(request)) {
1675
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle for the retry"))
1676
+ return 1;
1677
+ continue;
1678
+ }
1679
+
1680
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1681
+ if (request->setup_error_fatal)
1682
+ lazy_raise_setup_error(request);
1683
+ if (lazy_finalize_error(ctx, slot,
1684
+ request->setup_error ? request->setup_error
1685
+ : "Invalid request configuration"))
1686
+ return 1;
1687
+ continue;
1688
+ }
1689
+
1690
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1691
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the retry"))
1692
+ return 1;
1693
+ continue;
1694
+ }
1695
+
1696
+ request->attempts++;
1697
+ request->active = 1;
1698
+ request->done = 0;
1699
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1700
+ ctx->retry_at_ms[slot] = 0;
1701
+ ctx->session.active_count++;
1702
+ }
1703
+
1704
+ return 0;
1705
+ }
1706
+
1707
+ static long long lazy_next_retry_at(const lazy_execute_ctx_t *ctx) {
1708
+ long long next = 0;
1709
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1710
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT)
1711
+ continue;
1712
+ if (next == 0 || ctx->retry_at_ms[slot] < next)
1713
+ next = ctx->retry_at_ms[slot];
1714
+ }
1715
+ return next;
1716
+ }
1717
+
1718
+ static int lazy_process_completed(lazy_execute_ctx_t *ctx) {
1719
+ CURLMsg *msg;
1720
+ int msgs_left;
1721
+
1722
+ while ((msg = curl_multi_info_read(ctx->session.multi, &msgs_left))) {
1723
+ if (msg->msg != CURLMSG_DONE)
1724
+ continue;
1725
+
1726
+ request_ctx_t *request = NULL;
1727
+ curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&request);
1728
+ if (!request || request->done)
1729
+ continue;
1730
+
1731
+ int slot = (int)(request - ctx->session.requests);
1732
+ if (slot < 0 || slot >= ctx->capacity)
1733
+ continue;
1734
+
1735
+ if (request->active) {
1736
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1737
+ request->active = 0;
1738
+ if (ctx->session.active_count > 0)
1739
+ ctx->session.active_count--;
1740
+ }
1741
+
1742
+ request->done = 1;
1743
+ request->curl_result = msg->data.result;
1744
+ if (msg->data.result == CURLE_OK)
1745
+ curl_easy_getinfo(request->easy, CURLINFO_RESPONSE_CODE, &request->http_status);
1746
+
1747
+ if (request->attempts <= ctx->retry_cfg.max_retries &&
1748
+ should_retry(request, &ctx->retry_cfg)) {
1749
+ ctx->states[slot] = LAZY_SLOT_RETRY_WAIT;
1750
+ ctx->retry_at_ms[slot] =
1751
+ fast_now_ms() + retry_backoff_ms(&ctx->retry_cfg, request->attempts - 1);
1752
+ continue;
1753
+ }
1754
+
1755
+ VALUE response =
1756
+ (msg->data.result == CURLE_OK)
1757
+ ? build_response(request)
1758
+ : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1759
+ (int)msg->data.result, request->attempts);
1760
+ int stop = record_response(&ctx->cctx, request->index, response);
1761
+ lazy_release_slot(ctx, slot);
1762
+ if (stop)
1763
+ return 1;
1764
+ }
1765
+
1766
+ return 0;
1767
+ }
1768
+
1769
+ typedef struct {
1770
+ multi_session_t *session;
1771
+ long long wake_at_ms;
1772
+ } lazy_poll_args_t;
1773
+
1774
+ static void *lazy_poll_without_gvl(void *arg) {
1775
+ lazy_poll_args_t *poll = (lazy_poll_args_t *)arg;
1776
+ multi_session_t *session = poll->session;
1777
+ long long started = fast_now_ms();
1778
+ int before = session->still_running;
1779
+
1780
+ while (!session->cancelled) {
1781
+ int numfds = 0;
1782
+ curl_multi_poll(session->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
1783
+ curl_multi_perform(session->multi, &session->still_running);
1784
+
1785
+ if (session->still_running == 0 || session->still_running < before)
1786
+ break;
1787
+ if (poll->wake_at_ms > 0 && fast_now_ms() >= poll->wake_at_ms)
1788
+ break;
1789
+ if (fast_now_ms() - started >= POLL_SLICE_MS)
1790
+ break;
1791
+ }
1792
+
1793
+ return NULL;
1794
+ }
1795
+
1796
+ static void lazy_wait(lazy_execute_ctx_t *ctx) {
1797
+ long long next_retry = lazy_next_retry_at(ctx);
1798
+
1799
+ if (ctx->session.active_count > 0) {
1800
+ lazy_poll_args_t poll = {.session = &ctx->session, .wake_at_ms = next_retry};
1801
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1802
+ VALUE scheduler = current_fiber_scheduler();
1803
+ if (scheduler != Qnil)
1804
+ run_via_fiber_worker(scheduler, lazy_poll_without_gvl, &poll);
1805
+ else
1806
+ #endif
1807
+ rb_thread_call_without_gvl(lazy_poll_without_gvl, &poll, unblock_perform,
1808
+ &ctx->session);
1809
+ return;
1810
+ }
1811
+
1812
+ if (next_retry > 0) {
1813
+ long long delay = next_retry - fast_now_ms();
1814
+ if (delay > 0)
1815
+ retry_delay_sleep((long)delay);
1816
+ }
1817
+ }
1818
+
1819
+ static int lazy_deadline_reached(lazy_execute_ctx_t *ctx) {
1820
+ if (ctx->opts.deadline_ms <= 0 || fast_now_ms() < ctx->opts.deadline_ms)
1821
+ return 0;
1822
+
1823
+ ctx->deadline_hit = 1;
1824
+ ctx->stop = 1;
1825
+ return 1;
1826
+ }
1827
+
1828
+ static void lazy_progress(lazy_execute_ctx_t *ctx) {
1829
+ if (ctx->stop || lazy_deadline_reached(ctx))
1830
+ return;
1831
+
1832
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
1833
+ ctx->stop = 1;
1834
+ return;
1835
+ }
1836
+
1837
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
1838
+ if (lazy_process_completed(ctx)) {
1839
+ ctx->stop = 1;
1840
+ return;
1841
+ }
1842
+
1843
+ if (ctx->occupied_count == 0)
1844
+ return;
1845
+
1846
+ lazy_wait(ctx);
1847
+ if (lazy_process_completed(ctx))
1848
+ ctx->stop = 1;
1849
+ }
1850
+
1851
+ static VALUE lazy_each_request(VALUE yielded, VALUE arg, int argc, const VALUE *argv,
1852
+ VALUE blockarg) {
1853
+ (void)blockarg;
1854
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1855
+ VALUE request = argc == 1 ? argv[0] : yielded;
1856
+
1857
+ while (!ctx->stop && ctx->occupied_count >= ctx->capacity)
1858
+ lazy_progress(ctx);
1859
+
1860
+ if (ctx->stop || lazy_deadline_reached(ctx))
1861
+ rb_iter_break();
1862
+
1863
+ if (ctx->next_index > INT_MAX)
1864
+ rb_raise(rb_eArgError, "lazy request index exceeds supported range");
1865
+
1866
+ int slot = ctx->free_slots[--ctx->free_count];
1867
+ request_ctx_init(&ctx->session.requests[slot], ctx->next_index++);
1868
+ rb_ary_store(ctx->anchors, slot, request);
1869
+ ctx->states[slot] = LAZY_SLOT_PENDING;
1870
+ ctx->occupied_count++;
1871
+ lazy_pending_push(ctx, slot);
1872
+
1873
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
1874
+ ctx->stop = 1;
1875
+ rb_iter_break();
1876
+ }
1877
+
1878
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
1879
+ if (lazy_process_completed(ctx)) {
1880
+ ctx->stop = 1;
1881
+ rb_iter_break();
1882
+ }
1883
+
1884
+ return Qnil;
1885
+ }
1886
+
1887
+ static void lazy_finalize_deadline(lazy_execute_ctx_t *ctx) {
1888
+ if (ctx->cctx.stream || ctx->cctx.target > 0)
1889
+ return;
1890
+
1891
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1892
+ if (ctx->states[slot] == LAZY_SLOT_FREE)
1893
+ continue;
1894
+
1895
+ request_ctx_t *request = &ctx->session.requests[slot];
1896
+ if (request->active) {
1897
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1898
+ request->active = 0;
1899
+ if (ctx->session.active_count > 0)
1900
+ ctx->session.active_count--;
1901
+ }
1902
+
1903
+ VALUE response =
1904
+ build_error_response("Total timeout exceeded", ERR_DEADLINE, request->attempts);
1905
+ record_response(&ctx->cctx, request->index, response);
1906
+ lazy_release_slot(ctx, slot);
1907
+ }
1908
+ }
1909
+
1910
+ static VALUE lazy_execute_body(VALUE arg) {
1911
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1912
+
1913
+ if (ctx->opts.total_timeout_ms > 0)
1914
+ ctx->opts.deadline_ms = fast_now_ms() + ctx->opts.total_timeout_ms;
1915
+
1916
+ rb_block_call(ctx->source, rb_intern("each"), 0, NULL, lazy_each_request, (VALUE)ctx);
1917
+
1918
+ while (!ctx->stop && ctx->occupied_count > 0)
1919
+ lazy_progress(ctx);
1920
+
1921
+ if (ctx->deadline_hit)
1922
+ lazy_finalize_deadline(ctx);
1923
+
1924
+ RB_GC_GUARD(ctx->source);
1925
+ RB_GC_GUARD(ctx->anchors);
1926
+ return ctx->cctx.stream ? Qnil : ctx->cctx.results;
1927
+ }
1928
+
1929
+ static VALUE cleanup_lazy_execute(VALUE arg) {
1930
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1931
+
1932
+ if (ctx->session.requests) {
1933
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1934
+ request_ctx_t *request = &ctx->session.requests[slot];
1935
+ if (request->easy && request->active && ctx->session.multi)
1936
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1937
+ request_ctx_free(request);
1938
+ }
1939
+ free(ctx->session.requests);
1940
+ ctx->session.requests = NULL;
1941
+ }
1942
+
1943
+ free(ctx->states);
1944
+ ctx->states = NULL;
1945
+ free(ctx->free_slots);
1946
+ ctx->free_slots = NULL;
1947
+ free(ctx->pending_slots);
1948
+ ctx->pending_slots = NULL;
1949
+ free(ctx->retry_at_ms);
1950
+ ctx->retry_at_ms = NULL;
1951
+
1952
+ if (ctx->session.multi) {
1953
+ curl_multi_cleanup(ctx->session.multi);
1954
+ ctx->session.multi = NULL;
1955
+ }
1956
+
1957
+ if (ctx->retry_cfg.retry_http_codes) {
1958
+ free(ctx->retry_cfg.retry_http_codes);
1959
+ ctx->retry_cfg.retry_http_codes = NULL;
1960
+ }
1961
+
1962
+ return Qnil;
1963
+ }
1964
+
1965
+ static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, int stream) {
1966
+ if (!rb_respond_to(requests, rb_intern("each")))
1967
+ rb_raise(rb_eArgError, "requests must be an Array or respond to #each");
1968
+
1969
+ lazy_execute_ctx_t ctx;
1970
+ memset(&ctx, 0, sizeof(ctx));
1971
+
1972
+ int max_conn;
1973
+ parse_options(options, &ctx.opts, &max_conn, &ctx.retry_cfg);
1974
+ ctx.buffer_size = NIL_P(options) ? max_conn
1975
+ : parse_int_option(options, KEY_BUFFER, "buffer", 1,
1976
+ MAX_BUFFER, max_conn, NULL);
1977
+
1978
+ if (stream || target > 0) {
1979
+ if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && stream)
1980
+ rb_warn(
1981
+ "FastCurl: retries are not supported in stream_execute, ignoring retries option");
1982
+ if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && target > 0)
1983
+ rb_warn(
1984
+ "FastCurl: retries are not supported in first_execute, ignoring retries option");
1985
+ ctx.retry_cfg.max_retries = 0;
1986
+ }
1987
+
1988
+ ctx.capacity = max_conn + ctx.buffer_size;
1989
+ ctx.source = requests;
1990
+ ctx.anchors = rb_ary_new2(ctx.capacity);
1991
+ ctx.next_index = 0;
1992
+
1993
+ ctx.cctx.results = stream ? Qnil : rb_ary_new();
1994
+ ctx.cctx.accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
1995
+ ctx.cctx.completed = 0;
1996
+ ctx.cctx.target = target;
1997
+ ctx.cctx.stream = stream;
1998
+
1999
+ if (!NIL_P(ctx.cctx.accept) && !rb_respond_to(ctx.cctx.accept, rb_intern("call")))
2000
+ rb_raise(rb_eArgError, "accept must respond to #call");
2001
+
2002
+ multi_session_init(&ctx.session, curl_multi_init(), ctx.capacity, max_conn,
2003
+ ctx.opts.timeout_sec);
2004
+ if (!ctx.session.multi)
2005
+ rb_raise(rb_eNoMemError, "failed to initialize curl multi handle");
2006
+ multi_session_configure(ctx.session.multi, max_conn);
2007
+
2008
+ ctx.session.requests = calloc((size_t)ctx.capacity, sizeof(request_ctx_t));
2009
+ ctx.states = calloc((size_t)ctx.capacity, sizeof(int));
2010
+ ctx.free_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
2011
+ ctx.pending_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
2012
+ ctx.retry_at_ms = calloc((size_t)ctx.capacity, sizeof(long long));
2013
+
2014
+ if (!ctx.session.requests || !ctx.states || !ctx.free_slots || !ctx.pending_slots ||
2015
+ !ctx.retry_at_ms) {
2016
+ cleanup_lazy_execute((VALUE)&ctx);
2017
+ rb_raise(rb_eNoMemError, "failed to allocate lazy request state");
2018
+ }
2019
+
2020
+ for (int slot = 0; slot < ctx.capacity; slot++)
2021
+ ctx.free_slots[ctx.free_count++] = slot;
2022
+
2023
+ return rb_ensure(lazy_execute_body, (VALUE)&ctx, cleanup_lazy_execute, (VALUE)&ctx);
2024
+ }
2025
+
1525
2026
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1526
2027
  typedef struct {
1527
2028
  execute_args_t *ea;
@@ -1597,6 +2098,9 @@ static VALUE execute_with_fiber_scheduler(VALUE arg) {
1597
2098
  #endif
1598
2099
 
1599
2100
  static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
2101
+ if (!RB_TYPE_P(requests, T_ARRAY))
2102
+ return internal_lazy_execute(requests, options, target, stream);
2103
+
1600
2104
  Check_Type(requests, T_ARRAY);
1601
2105
 
1602
2106
  long count_long = RARRAY_LEN(requests);
@@ -1617,6 +2121,12 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1617
2121
  retry_config_t retry_cfg;
1618
2122
  parse_options(options, &opts, &max_conn, &retry_cfg);
1619
2123
 
2124
+ if (target > 0 && !NIL_P(options)) {
2125
+ VALUE accept = hash_aref_key(options, KEY_ACCEPT);
2126
+ if (!NIL_P(accept) && !rb_respond_to(accept, rb_intern("call")))
2127
+ rb_raise(rb_eArgError, "accept must respond to #call");
2128
+ }
2129
+
1620
2130
  for (int i = 0; i < count; i++) {
1621
2131
  request_ctx_t probe;
1622
2132
  request_ctx_init(&probe, i);
@@ -1680,7 +2190,8 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1680
2190
 
1681
2191
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1682
2192
  VALUE scheduler = current_fiber_scheduler();
1683
- if (scheduler != Qnil && !stream) {
2193
+ VALUE accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
2194
+ if (scheduler != Qnil && !stream && NIL_P(accept)) {
1684
2195
  scheduler_execute_ctx_t scheduler_ctx = {
1685
2196
  .ea = &ea,
1686
2197
  .scheduler = scheduler,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCurl
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/fast_curl.rb CHANGED
@@ -88,7 +88,8 @@ module FastCurl
88
88
  execute(build_requests(requests, method), **DEFAULT_OPTIONS.merge(options))
89
89
  end
90
90
 
91
- define_method(:"first_#{method}") do |requests, count: 1, **options|
91
+ define_method(:"first_#{method}") do |requests, count: 1, accept: nil, **options|
92
+ options[:accept] = accept unless accept.nil?
92
93
  first_execute(build_requests(requests, method), count: count, **DEFAULT_OPTIONS.merge(options))
93
94
  end
94
95
 
@@ -100,24 +101,38 @@ module FastCurl
100
101
  private
101
102
 
102
103
  def build_requests(requests, method)
103
- requests.map do |req|
104
- r = { url: build_url(req), method: method.to_s.upcase }
105
- headers = req[:headers] ? req[:headers].dup : nil
106
-
107
- if BODY_METHODS.include?(method)
108
- body, content_type = build_body(req)
109
- if body
110
- r[:body] = body
111
- if content_type && !content_type?(headers)
112
- headers ||= {}
113
- headers["Content-Type"] = content_type
114
- end
115
- end
104
+ source = requests.is_a?(Hash) ? [requests] : requests
105
+
106
+ if source.is_a?(Array)
107
+ source.map { |request| build_request(request, method) }
108
+ elsif source.respond_to?(:each)
109
+ Enumerator.new do |yielder|
110
+ source.each { |request| yielder << build_request(request, method) }
116
111
  end
112
+ else
113
+ raise ArgumentError, "requests must be a Hash, Array or respond to #each"
114
+ end
115
+ end
116
+
117
+ def build_request(req, method)
118
+ raise ArgumentError, "request must be a Hash" unless req.is_a?(Hash)
117
119
 
118
- r[:headers] = headers if headers && !headers.empty?
119
- r
120
+ r = { url: build_url(req), method: method.to_s.upcase }
121
+ headers = req[:headers] ? req[:headers].dup : nil
122
+
123
+ if BODY_METHODS.include?(method)
124
+ body, content_type = build_body(req)
125
+ if body
126
+ r[:body] = body
127
+ if content_type && !content_type?(headers)
128
+ headers ||= {}
129
+ headers["Content-Type"] = content_type
130
+ end
131
+ end
120
132
  end
133
+
134
+ r[:headers] = headers if headers && !headers.empty?
135
+ r
121
136
  end
122
137
 
123
138
  def build_url(req)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_curl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - roman-haidarov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: json
@@ -125,7 +124,6 @@ homepage: https://github.com/roman-haidarov/fast_curl
125
124
  licenses:
126
125
  - MIT
127
126
  metadata: {}
128
- post_install_message:
129
127
  rdoc_options: []
130
128
  require_paths:
131
129
  - lib
@@ -140,8 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
138
  - !ruby/object:Gem::Version
141
139
  version: '0'
142
140
  requirements: []
143
- rubygems_version: 3.4.22
144
- signing_key:
141
+ rubygems_version: 3.6.7
145
142
  specification_version: 4
146
143
  summary: Ultra-fast parallel HTTP client as Ruby C extension on libcurl multi
147
144
  test_files: []