curb 1.3.6 → 1.3.7

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.
data/ext/curb_multi.c CHANGED
@@ -52,6 +52,11 @@
52
52
  #define CURB_MAYBE_UNUSED_DECL
53
53
  #endif
54
54
 
55
+ /* Older rubies do not define UNDEF_P. */
56
+ #ifndef UNDEF_P
57
+ #define UNDEF_P(obj) ((VALUE)(obj) == Qundef)
58
+ #endif
59
+
55
60
  #ifdef _WIN32
56
61
  // for O_RDWR and O_BINARY
57
62
  #include <fcntl.h>
@@ -70,6 +75,88 @@ static void *curl_multi_wait_wrapper(void *p) {
70
75
  }
71
76
  #endif
72
77
 
78
+ static VALUE cRubyFiber;
79
+ static ID id_fiber_blocking_p;
80
+ static ID id_fiber_scheduler;
81
+ static ID id_kernel_sleep;
82
+ static ID id_io_wait;
83
+
84
+ /*
85
+ * Ruby 3.0 introduced the Fiber scheduler at the Ruby level, but did not ship
86
+ * the public ruby/fiber/scheduler.h C API. Use the public C entry points when
87
+ * available and otherwise dispatch through Fiber and the scheduler object.
88
+ */
89
+ static VALUE curb_fiber_scheduler_current(void) {
90
+ #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
91
+ return rb_fiber_scheduler_current();
92
+ #else
93
+ VALUE scheduler;
94
+
95
+ if (!rb_respond_to(cRubyFiber, id_fiber_scheduler)) return Qnil;
96
+
97
+ scheduler = rb_funcall(cRubyFiber, id_fiber_scheduler, 0);
98
+ if (NIL_P(scheduler)) return Qnil;
99
+
100
+ /* Fiber.scheduler returns the thread scheduler even from a blocking fiber,
101
+ * while rb_fiber_scheduler_current returns nil there. Match the latter so
102
+ * ordinary blocking fibers keep using the legacy multi loop. */
103
+ if (rb_respond_to(cRubyFiber, id_fiber_blocking_p) &&
104
+ RTEST(rb_funcall(cRubyFiber, id_fiber_blocking_p, 0))) {
105
+ return Qnil;
106
+ }
107
+
108
+ return scheduler;
109
+ #endif
110
+ }
111
+
112
+ static VALUE curb_fiber_scheduler_kernel_sleep(VALUE scheduler, VALUE timeout) {
113
+ #if defined(HAVE_RB_FIBER_SCHEDULER_KERNEL_SLEEP)
114
+ return rb_fiber_scheduler_kernel_sleep(scheduler, timeout);
115
+ #else
116
+ return rb_funcall(scheduler, id_kernel_sleep, 1, timeout);
117
+ #endif
118
+ }
119
+
120
+ static VALUE curb_fiber_scheduler_io_wait(VALUE scheduler, VALUE io, VALUE events, VALUE timeout) {
121
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT)
122
+ return rb_fiber_scheduler_io_wait(scheduler, io, events, timeout);
123
+ #else
124
+ return rb_funcall(scheduler, id_io_wait, 3, io, events, timeout);
125
+ #endif
126
+ }
127
+
128
+ /*
129
+ * Sleep for the given interval without stalling sibling fibers: when the
130
+ * current fiber runs under a fiber scheduler, wait via the scheduler's
131
+ * kernel_sleep hook. rb_thread_fd_select(0, ...) and rb_thread_wait_for are
132
+ * thread-level waits that never consult the scheduler, so they are only used
133
+ * when no scheduler is active.
134
+ */
135
+ static void curb_multi_scheduler_sleep(struct timeval *tv) {
136
+ VALUE scheduler = curb_fiber_scheduler_current();
137
+ if (scheduler != Qnil) {
138
+ curb_fiber_scheduler_kernel_sleep(scheduler, rb_float_new((double)tv->tv_sec + ((double)tv->tv_usec / 1e6)));
139
+ return;
140
+ }
141
+ #ifdef HAVE_RB_THREAD_FD_SELECT
142
+ {
143
+ struct timeval tv_sleep = *tv;
144
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
145
+ }
146
+ #else
147
+ rb_thread_wait_for(*tv);
148
+ #endif
149
+ }
150
+
151
+ /* Yield a nonblocking fiber without changing legacy behavior when no
152
+ * scheduler is active. */
153
+ static void curb_multi_scheduler_yield(void) {
154
+ VALUE scheduler = curb_fiber_scheduler_current();
155
+ if (scheduler != Qnil) {
156
+ curb_fiber_scheduler_kernel_sleep(scheduler, INT2FIX(0));
157
+ }
158
+ }
159
+
73
160
  extern VALUE mCurl;
74
161
  static VALUE idCall;
75
162
  static ID id_deferred_exception_ivar;
@@ -1172,30 +1259,6 @@ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
1172
1259
  #define cselect_flags_str(...) ""
1173
1260
  #endif
1174
1261
 
1175
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1176
- /* Protected call to rb_fiber_scheduler_io_wait to avoid unwinding into C on TypeError. */
1177
- struct fiber_io_wait_args { VALUE scheduler; VALUE io; VALUE events; VALUE timeout; };
1178
- static VALUE fiber_io_wait_protected(VALUE argp) {
1179
- struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
1180
- return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
1181
- }
1182
- #endif
1183
-
1184
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1185
- struct fiber_io_select_args {
1186
- VALUE scheduler;
1187
- VALUE readables;
1188
- VALUE writables;
1189
- VALUE exceptables;
1190
- VALUE timeout;
1191
- };
1192
-
1193
- static VALUE fiber_io_select_protected(VALUE argp) {
1194
- struct fiber_io_select_args *a = (struct fiber_io_select_args *)argp;
1195
- return rb_fiber_scheduler_io_select(a->scheduler, a->readables, a->writables, a->exceptables, a->timeout);
1196
- }
1197
- #endif
1198
-
1199
1262
  #if defined(RB_INTEGER_TYPE_P)
1200
1263
  #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1201
1264
  #else
@@ -1387,6 +1450,7 @@ struct build_io_select_arrays_args {
1387
1450
  VALUE writables;
1388
1451
  VALUE exceptables;
1389
1452
  int failed;
1453
+ int state;
1390
1454
  };
1391
1455
 
1392
1456
  static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
@@ -1405,12 +1469,7 @@ static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp
1405
1469
  io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1406
1470
  if (io_state || NIL_P(io)) {
1407
1471
  if (io_state) {
1408
- #if CURB_SOCKET_DEBUG
1409
- VALUE err = rb_errinfo();
1410
- VALUE msg = rb_obj_as_string(err);
1411
- curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1412
- #endif
1413
- rb_set_errinfo(Qnil);
1472
+ a->state = io_state;
1414
1473
  }
1415
1474
  a->failed = 1;
1416
1475
  return ST_STOP;
@@ -1455,14 +1514,17 @@ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t arg
1455
1514
  #endif
1456
1515
 
1457
1516
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
1458
- /* prime the state: let libcurl act on timeouts to setup sockets */
1459
- CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1460
- if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1461
- curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1462
- rb_curl_multi_read_info(self, rbcm->handle);
1463
- rb_curl_multi_yield_if_given(self, block);
1517
+ CURLMcode mrc;
1464
1518
 
1465
- while (rbcm->running) {
1519
+ do {
1520
+ /* Prime the state: let libcurl act on timeouts to set up sockets. */
1521
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1522
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1523
+ curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1524
+ rb_curl_multi_read_info(self, rbcm->handle);
1525
+ rb_curl_multi_yield_if_given(self, block);
1526
+
1527
+ while (rbcm->running) {
1466
1528
  struct timeval tv = {0, 0};
1467
1529
  long wait_ms = cCurlMutiDefaulttimeout;
1468
1530
 
@@ -1473,6 +1535,10 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1473
1535
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1474
1536
  rb_curl_multi_read_info(self, rbcm->handle);
1475
1537
  rb_curl_multi_yield_if_given(self, block);
1538
+ if (rbcm->running && multi_socket_timer_due(ctx)) {
1539
+ /* A repeatedly rearmed 0ms timer must still let sibling fibers run. */
1540
+ curb_multi_scheduler_yield();
1541
+ }
1476
1542
  continue;
1477
1543
  }
1478
1544
 
@@ -1510,26 +1576,24 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1510
1576
  if (count_tracked > 1) {
1511
1577
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1512
1578
  {
1513
- VALUE scheduler = rb_fiber_scheduler_current();
1579
+ VALUE scheduler = curb_fiber_scheduler_current();
1514
1580
  if (scheduler != Qnil) {
1515
1581
  VALUE readables = rb_ary_new();
1516
1582
  VALUE writables = rb_ary_new();
1517
1583
  VALUE exceptables = rb_ary_new();
1518
- struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
1584
+ struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0, 0 };
1519
1585
  st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
1586
+ if (build_args.state) rb_jump_tag(build_args.state);
1520
1587
  if (!build_args.failed) {
1521
1588
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1522
1589
  VALUE timeout = rb_float_new(timeout_s);
1523
- struct fiber_io_select_args select_args = { scheduler, readables, writables, exceptables, timeout };
1524
- int state = 0;
1525
- VALUE ready = rb_protect(fiber_io_select_protected, (VALUE)&select_args, &state);
1526
- if (state) {
1527
- #if CURB_SOCKET_DEBUG
1528
- VALUE err = rb_errinfo();
1529
- VALUE msg = rb_obj_as_string(err);
1530
- curb_debugf("[curb.socket] scheduler io_select failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1531
- #endif
1532
- rb_set_errinfo(Qnil);
1590
+ VALUE ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
1591
+ if (UNDEF_P(ready)) {
1592
+ /* The scheduler does not implement the optional io_select hook:
1593
+ * rb_fiber_scheduler_io_select returns Qundef immediately.
1594
+ * Leave handled_wait unset so the io_wait fallback below can
1595
+ * perform a cooperative wait. */
1596
+ curb_debugf("[curb.socket] scheduler io_select not implemented; falling back to io_wait");
1533
1597
  } else {
1534
1598
  handled_wait = 1;
1535
1599
  any_ready = RB_TYPE_P(ready, T_ARRAY);
@@ -1568,8 +1632,36 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1568
1632
  }
1569
1633
  }
1570
1634
  #endif
1635
+ /* Ruby versions without the public io_select C API, and newer
1636
+ * schedulers that omit the optional hook, wait cooperatively on one
1637
+ * representative descriptor, then use a zero-time select below to
1638
+ * collect readiness across the whole set. */
1639
+ if (!handled_wait && wait_fd >= 0) {
1640
+ VALUE scheduler = curb_fiber_scheduler_current();
1641
+ if (scheduler != Qnil) {
1642
+ if (!multi_socket_fd_valid_p(wait_fd)) {
1643
+ multi_socket_forget_fd(ctx, wait_fd);
1644
+ did_timeout = 1;
1645
+ handled_wait = 1;
1646
+ } else {
1647
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1648
+ int io_state = 0;
1649
+ VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1650
+ if (io_state) rb_jump_tag(io_state);
1651
+ if (!NIL_P(io)) {
1652
+ int events = multi_socket_wait_events_for_curl_poll(wait_what);
1653
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1654
+ curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), rb_float_new(timeout_s));
1655
+ tv.tv_sec = 0;
1656
+ tv.tv_usec = 0;
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1571
1661
  if (!handled_wait) {
1572
- /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1662
+ /* Multi-fd select. After the scheduler io_wait fallback above, tv is
1663
+ * zero and this only polls the complete descriptor set. Without a
1664
+ * scheduler it performs the normal GVL-releasing wait. */
1573
1665
  rb_fdset_t rfds, wfds, efds;
1574
1666
  rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1575
1667
  int maxfd = -1;
@@ -1608,9 +1700,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1608
1700
  handled_wait = 1;
1609
1701
  }
1610
1702
  } else if (count_tracked == 1) {
1611
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1612
1703
  {
1613
- VALUE scheduler = rb_fiber_scheduler_current();
1704
+ VALUE scheduler = curb_fiber_scheduler_current();
1614
1705
  if (scheduler != Qnil) {
1615
1706
  int scheduler_wait_handled = 0;
1616
1707
  int events = 0;
@@ -1620,11 +1711,9 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1620
1711
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1621
1712
  VALUE timeout = rb_float_new(timeout_s);
1622
1713
  if (wait_fd < 0) {
1623
- #ifdef HAVE_RB_THREAD_FD_SELECT
1624
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1625
- #else
1626
- rb_thread_wait_for(tv);
1627
- #endif
1714
+ /* No pollable fd yet: sleep via the scheduler's kernel_sleep so
1715
+ * sibling fibers keep running while libcurl gets ready. */
1716
+ curb_multi_scheduler_sleep(&tv);
1628
1717
  did_timeout = multi_socket_timer_due(ctx);
1629
1718
  scheduler_wait_handled = 1;
1630
1719
  } else if (!multi_socket_fd_valid_p(wait_fd)) {
@@ -1635,44 +1724,25 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1635
1724
  struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1636
1725
  int io_state = 0;
1637
1726
  VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1638
- if (io_state || NIL_P(io)) {
1639
- if (io_state) {
1640
- #if CURB_SOCKET_DEBUG
1641
- VALUE err = rb_errinfo();
1642
- VALUE msg = rb_obj_as_string(err);
1643
- curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1644
- #endif
1645
- rb_set_errinfo(Qnil);
1646
- }
1727
+ if (io_state) rb_jump_tag(io_state);
1728
+ if (NIL_P(io)) {
1647
1729
  any_ready = 0;
1648
1730
  } else {
1649
- struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1650
- int state = 0;
1651
- VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1652
- if (state) {
1653
- #if CURB_SOCKET_DEBUG
1654
- VALUE err = rb_errinfo();
1655
- VALUE msg = rb_obj_as_string(err);
1656
- curb_debugf("[curb.socket] scheduler io_wait failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1657
- #endif
1658
- rb_set_errinfo(Qnil);
1659
- any_ready = 0;
1660
- } else {
1661
- scheduler_wait_handled = 1;
1662
- any_ready = (ready != Qfalse && !NIL_P(ready));
1663
- did_timeout = !any_ready && multi_socket_timer_due(ctx);
1664
- if (any_ready) {
1665
- if (ready == Qtrue) {
1666
- ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1667
- } else if (CURB_INTEGER_P(ready)) {
1668
- ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1669
- if (ready_flags == 0) {
1670
- any_ready = 0;
1671
- did_timeout = multi_socket_timer_due(ctx);
1672
- }
1673
- } else {
1674
- ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1731
+ VALUE ready = curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), timeout);
1732
+ scheduler_wait_handled = 1;
1733
+ any_ready = (ready != Qfalse && !NIL_P(ready));
1734
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1735
+ if (any_ready) {
1736
+ if (ready == Qtrue) {
1737
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1738
+ } else if (CURB_INTEGER_P(ready)) {
1739
+ ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1740
+ if (ready_flags == 0) {
1741
+ any_ready = 0;
1742
+ did_timeout = multi_socket_timer_due(ctx);
1675
1743
  }
1744
+ } else {
1745
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1676
1746
  }
1677
1747
  }
1678
1748
  }
@@ -1680,7 +1750,6 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1680
1750
  if (scheduler_wait_handled) handled_wait = 1;
1681
1751
  }
1682
1752
  }
1683
- #endif
1684
1753
  #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1685
1754
  if (!handled_wait && wait_fd >= 0) {
1686
1755
  int ev = multi_socket_wait_events_for_curl_poll(wait_what);
@@ -1724,14 +1793,13 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1724
1793
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1725
1794
  }
1726
1795
  } else { /* count_tracked == 0 */
1727
- #ifdef HAVE_RB_THREAD_FD_SELECT
1728
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1729
- #else
1730
- rb_thread_wait_for(tv);
1731
- #endif
1796
+ /* No sockets exposed yet (e.g. libcurl's threaded resolver doing DNS):
1797
+ * sleep via the scheduler's kernel_sleep when one is active so sibling
1798
+ * fibers keep running, otherwise a plain thread wait. */
1799
+ curb_multi_scheduler_sleep(&tv);
1732
1800
  /* libcurl can report active work without a socket callback or deadline;
1733
- * drive the timeout socket after the scheduler-aware sleep so the state
1734
- * machine does not stall indefinitely. */
1801
+ * drive the timeout socket after the sleep so the state machine does
1802
+ * not stall indefinitely. */
1735
1803
  did_timeout = 1;
1736
1804
  }
1737
1805
 
@@ -1759,7 +1827,13 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1759
1827
  rb_curl_multi_read_info(self, rbcm->handle);
1760
1828
  curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
1761
1829
  rb_curl_multi_yield_if_given(self, block);
1762
- }
1830
+ }
1831
+
1832
+ /* Match the legacy perform contract: the block gets a final idle yield,
1833
+ * and work queued from that yield is driven before perform returns. */
1834
+ rb_curl_multi_read_info(self, rbcm->handle);
1835
+ rb_curl_multi_yield_if_given(self, block);
1836
+ } while (rbcm->running);
1763
1837
  }
1764
1838
 
1765
1839
  struct socket_drive_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; VALUE block; };
@@ -1822,8 +1896,6 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1822
1896
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1823
1897
 
1824
1898
  /* finalize */
1825
- rb_curl_multi_read_info(self, rbcm->handle);
1826
- rb_curl_multi_yield_if_given(self, block);
1827
1899
  if (cCurlMutiAutoClose == 1) {
1828
1900
  rbcm->allow_close_during_perform = 1;
1829
1901
  rb_funcall(self, rb_intern("_autoclose"), 0);
@@ -1959,11 +2031,9 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1959
2031
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1960
2032
  rb_curl_multi_read_info( self, rbcm->handle );
1961
2033
  rb_curl_multi_yield_if_given(self, block);
1962
- #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1963
- if (rb_fiber_scheduler_current() != Qnil) {
1964
- rb_thread_schedule();
1965
- }
1966
- #endif
2034
+ /* Yield the fiber so repeated immediate curl timeouts cannot starve
2035
+ * sibling work on this thread. */
2036
+ curb_multi_scheduler_yield();
1967
2037
  continue;
1968
2038
  }
1969
2039
 
@@ -1979,11 +2049,11 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1979
2049
  wait_args.timeout_ms = timeout_milliseconds;
1980
2050
  wait_args.numfds = 0;
1981
2051
  /*
1982
- * When a Fiber scheduler is available (Ruby >= 3.x), rb_thread_fd_select
1983
- * integrates with it. If we have rb_thread_fd_select available at build
1984
- * time, we avoid curl_multi_wait entirely (see preprocessor guard above)
1985
- * and use the fdset branch below. Otherwise, we use curl_multi_wait and
1986
- * release the GVL so Ruby threads can continue to run.
2052
+ * Wait via curl_multi_wait with the GVL released so other Ruby
2053
+ * threads can continue to run. Like rb_thread_fd_select, this wait
2054
+ * does not consult the fiber scheduler; scheduler-aware waiting is
2055
+ * provided by the socket-action loop, which perform routes to when a
2056
+ * scheduler is active (see ruby_curl_multi_perform).
1987
2057
  */
1988
2058
  CURLMcode wait_rc;
1989
2059
  #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
@@ -1997,13 +2067,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1997
2067
  raise_curl_multi_error_exception(wait_rc);
1998
2068
  }
1999
2069
  if (wait_args.numfds == 0) {
2000
- #ifdef HAVE_RB_THREAD_FD_SELECT
2001
- struct timeval tv_sleep = tv_100ms;
2002
- /* Sleep in a scheduler-aware way. */
2003
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
2004
- #else
2005
- rb_thread_wait_for(tv_100ms);
2006
- #endif
2070
+ curb_multi_scheduler_sleep(&tv_100ms);
2007
2071
  }
2008
2072
  /* Process pending transfers after waiting */
2009
2073
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
@@ -2027,12 +2091,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2027
2091
 
2028
2092
  if (maxfd == -1) {
2029
2093
  /* libcurl recommends sleeping for 100ms */
2030
- #ifdef HAVE_RB_THREAD_FD_SELECT
2031
- struct timeval tv_sleep = tv_100ms;
2032
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
2033
- #else
2034
- rb_thread_wait_for(tv_100ms);
2035
- #endif
2094
+ curb_multi_scheduler_sleep(&tv_100ms);
2036
2095
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
2037
2096
  rb_curl_multi_read_info( self, rbcm->handle );
2038
2097
  rb_curl_multi_yield_if_given(self, block);
@@ -2055,7 +2114,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2055
2114
  #endif
2056
2115
 
2057
2116
  #ifdef HAVE_RB_THREAD_FD_SELECT
2058
- /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
2117
+ /* Wait with the GVL released. Note rb_thread_fd_select does not consult
2118
+ * the fiber scheduler — under a scheduler, perform routes to the
2119
+ * socket-action loop instead (see ruby_curl_multi_perform). Build
2120
+ * rb_fdset_t sets. */
2059
2121
  {
2060
2122
  rb_fdset_t rfds, wfds, efds;
2061
2123
  rb_fd_init(&rfds);
@@ -2167,6 +2229,18 @@ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE sel
2167
2229
  }
2168
2230
 
2169
2231
  VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2232
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2233
+ /*
2234
+ * The legacy fdset loop waits with rb_thread_fd_select, which releases the
2235
+ * GVL but never consults the fiber scheduler, stalling every other fiber on
2236
+ * this thread for the duration of the transfers. When the calling fiber
2237
+ * runs under a fiber scheduler, use the socket-action drive loop, which
2238
+ * waits through the scheduler's io_wait/io_select/kernel_sleep hooks.
2239
+ */
2240
+ if (curb_fiber_scheduler_current() != Qnil) {
2241
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
2242
+ }
2243
+ #endif
2170
2244
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2171
2245
  }
2172
2246
 
@@ -2233,6 +2307,12 @@ static void curl_multi_mark(void *ptr) {
2233
2307
  /* =================== INIT LIB =====================*/
2234
2308
  void init_curb_multi() {
2235
2309
  idCall = rb_intern("call");
2310
+ cRubyFiber = rb_const_get(rb_cObject, rb_intern("Fiber"));
2311
+ rb_global_variable(&cRubyFiber);
2312
+ id_fiber_blocking_p = rb_intern("blocking?");
2313
+ id_fiber_scheduler = rb_intern("scheduler");
2314
+ id_kernel_sleep = rb_intern("kernel_sleep");
2315
+ id_io_wait = rb_intern("io_wait");
2236
2316
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
2237
2317
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2238
2318
  id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
@@ -2254,9 +2334,10 @@ void init_curb_multi() {
2254
2334
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
2255
2335
  rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
2256
2336
  /*
2257
- * The legacy fdset loop is the stable default. The newer socket-action path
2258
- * is kept in-tree, but it has shown scheduler regressions for one-handle
2259
- * multi usage (for example Curl::Easy#perform under Async).
2337
+ * perform uses the legacy fdset loop by default, but routes to the
2338
+ * socket-action drive loop when the calling fiber runs under a fiber
2339
+ * scheduler so sibling fibers keep running during transfers. The
2340
+ * socket-action path is also exposed directly as _socket_perform.
2260
2341
  */
2261
2342
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
2262
2343
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
data/ext/extconf.rb CHANGED
@@ -462,6 +462,9 @@ have_constant "curlopt_ftp_use_eprt"
462
462
  have_constant "curlopt_ftp_use_epsv"
463
463
  have_constant "curlopt_ftp_use_pret"
464
464
  have_constant "curlopt_ftp_create_missing_dirs"
465
+ have_constant "curlftp_create_dir_none"
466
+ have_constant "curlftp_create_dir"
467
+ have_constant "curlftp_create_dir_retry"
465
468
  have_constant "curlopt_ftp_response_timeout"
466
469
  have_constant "curlopt_ftp_alternative_to_user"
467
470
  have_constant "curlopt_ftp_skip_pasv_ip"
@@ -700,6 +703,7 @@ have_header('ruby/fiber/scheduler.h')
700
703
  have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
701
704
  have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
702
705
  have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
706
+ have_func('rb_fiber_scheduler_kernel_sleep', 'ruby/fiber/scheduler.h')
703
707
  have_func('rb_io_stdio_file')
704
708
  have_func('curl_multi_wait')
705
709
  have_func('curl_multi_socket_action')