curb 1.3.6 → 1.4.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.
data/ext/curb_multi.c CHANGED
@@ -32,6 +32,7 @@
32
32
  #include <sys/time.h>
33
33
  #include <time.h>
34
34
  #endif
35
+ #include <limits.h>
35
36
  #include <stdint.h>
36
37
  #include <stdarg.h>
37
38
 
@@ -52,12 +53,50 @@
52
53
  #define CURB_MAYBE_UNUSED_DECL
53
54
  #endif
54
55
 
56
+ /* Older rubies do not define UNDEF_P. */
57
+ #ifndef UNDEF_P
58
+ #define UNDEF_P(obj) ((VALUE)(obj) == Qundef)
59
+ #endif
60
+
55
61
  #ifdef _WIN32
56
62
  // for O_RDWR and O_BINARY
57
63
  #include <fcntl.h>
58
64
  #endif
59
65
 
60
- #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
66
+ /*
67
+ * Prefer curl_multi_poll for the blocking perform loop: it uses poll(), so it
68
+ * has no FD_SETSIZE limit (select-based waiting cannot see sockets numbered
69
+ * above 1024 and degrades to fixed sleeps), and curl_multi_wakeup lets the
70
+ * unblocking function end the wait immediately for Thread#raise, Thread#kill
71
+ * and signal delivery.
72
+ */
73
+ #if defined(HAVE_CURL_MULTI_POLL) && defined(HAVE_CURL_MULTI_WAKEUP) && defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
74
+ #define CURB_USE_MULTI_POLL 1
75
+ #endif
76
+
77
+ #ifdef CURB_USE_MULTI_POLL
78
+ struct multi_poll_args {
79
+ CURLM *handle;
80
+ int timeout_ms;
81
+ int numfds;
82
+ CURLMcode code;
83
+ };
84
+
85
+ static void *curb_multi_poll_without_gvl(void *p) {
86
+ struct multi_poll_args *args = p;
87
+ args->code = curl_multi_poll(args->handle, NULL, 0, args->timeout_ms, &args->numfds);
88
+ return NULL;
89
+ }
90
+
91
+ /* Unblocking function: curl_multi_wakeup is safe to call from another thread
92
+ * while curl_multi_poll is blocked on the same handle. A wakeup that arrives
93
+ * after the poll returned only makes the next poll return early once. */
94
+ static void curb_multi_poll_ubf(void *handle) {
95
+ curl_multi_wakeup((CURLM *)handle);
96
+ }
97
+ #endif
98
+
99
+ #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT) && !defined(CURB_USE_MULTI_POLL)
61
100
  struct wait_args {
62
101
  CURLM *handle;
63
102
  long timeout_ms;
@@ -70,12 +109,101 @@ static void *curl_multi_wait_wrapper(void *p) {
70
109
  }
71
110
  #endif
72
111
 
112
+ static VALUE cRubyFiber;
113
+ static VALUE cRubyRactor;
114
+ static ID id_fiber_blocking_p;
115
+ static ID id_fiber_scheduler;
116
+ static ID id_kernel_sleep;
117
+ static ID id_io_wait;
118
+ static ID id_ractor_current;
119
+ static ID id_ractor_aref;
120
+ static ID id_ractor_aset;
121
+ static ID id_ractor_default_timeout_key;
122
+ static ID id_ractor_autoclose_key;
123
+
124
+ /*
125
+ * Ruby 3.0 introduced the Fiber scheduler at the Ruby level, but did not ship
126
+ * the public ruby/fiber/scheduler.h C API. Use the public C entry points when
127
+ * available and otherwise dispatch through Fiber and the scheduler object.
128
+ */
129
+ static VALUE curb_fiber_scheduler_current(void) {
130
+ #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
131
+ return rb_fiber_scheduler_current();
132
+ #else
133
+ VALUE scheduler;
134
+
135
+ if (!rb_respond_to(cRubyFiber, id_fiber_scheduler)) return Qnil;
136
+
137
+ scheduler = rb_funcall(cRubyFiber, id_fiber_scheduler, 0);
138
+ if (NIL_P(scheduler)) return Qnil;
139
+
140
+ /* Fiber.scheduler returns the thread scheduler even from a blocking fiber,
141
+ * while rb_fiber_scheduler_current returns nil there. Match the latter so
142
+ * ordinary blocking fibers keep using the legacy multi loop. */
143
+ if (rb_respond_to(cRubyFiber, id_fiber_blocking_p) &&
144
+ RTEST(rb_funcall(cRubyFiber, id_fiber_blocking_p, 0))) {
145
+ return Qnil;
146
+ }
147
+
148
+ return scheduler;
149
+ #endif
150
+ }
151
+
152
+ static VALUE curb_fiber_scheduler_kernel_sleep(VALUE scheduler, VALUE timeout) {
153
+ #if defined(HAVE_RB_FIBER_SCHEDULER_KERNEL_SLEEP)
154
+ return rb_fiber_scheduler_kernel_sleep(scheduler, timeout);
155
+ #else
156
+ return rb_funcall(scheduler, id_kernel_sleep, 1, timeout);
157
+ #endif
158
+ }
159
+
160
+ static VALUE curb_fiber_scheduler_io_wait(VALUE scheduler, VALUE io, VALUE events, VALUE timeout) {
161
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT)
162
+ return rb_fiber_scheduler_io_wait(scheduler, io, events, timeout);
163
+ #else
164
+ return rb_funcall(scheduler, id_io_wait, 3, io, events, timeout);
165
+ #endif
166
+ }
167
+
168
+ /*
169
+ * Sleep for the given interval without stalling sibling fibers: when the
170
+ * current fiber runs under a fiber scheduler, wait via the scheduler's
171
+ * kernel_sleep hook. rb_thread_fd_select(0, ...) and rb_thread_wait_for are
172
+ * thread-level waits that never consult the scheduler, so they are only used
173
+ * when no scheduler is active.
174
+ */
175
+ static void curb_multi_scheduler_sleep(struct timeval *tv) {
176
+ VALUE scheduler = curb_fiber_scheduler_current();
177
+ if (scheduler != Qnil) {
178
+ curb_fiber_scheduler_kernel_sleep(scheduler, rb_float_new((double)tv->tv_sec + ((double)tv->tv_usec / 1e6)));
179
+ return;
180
+ }
181
+ #ifdef HAVE_RB_THREAD_FD_SELECT
182
+ {
183
+ struct timeval tv_sleep = *tv;
184
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
185
+ }
186
+ #else
187
+ rb_thread_wait_for(*tv);
188
+ #endif
189
+ }
190
+
191
+ /* Yield a nonblocking fiber without changing legacy behavior when no
192
+ * scheduler is active. */
193
+ static void curb_multi_scheduler_yield(void) {
194
+ VALUE scheduler = curb_fiber_scheduler_current();
195
+ if (scheduler != Qnil) {
196
+ curb_fiber_scheduler_kernel_sleep(scheduler, INT2FIX(0));
197
+ }
198
+ }
199
+
73
200
  extern VALUE mCurl;
74
201
  static VALUE idCall;
75
202
  static ID id_deferred_exception_ivar;
76
203
  static ID id_deferred_exception_source_id_ivar;
77
204
  static ID id_native_safety_signatures_ivar;
78
205
  static ID id_socket_io_cache_ivar;
206
+ static ID id_socket_poller_io_ivar;
79
207
 
80
208
  #ifdef RDOC_NEVER_DEFINED
81
209
  mCurl = rb_define_module("Curl");
@@ -86,6 +214,32 @@ VALUE cCurlMulti;
86
214
  static long cCurlMutiDefaulttimeout = 100; /* milliseconds */
87
215
  static char cCurlMutiAutoClose = 0;
88
216
 
217
+ /* Ruby 3.0+ provides storage owned by the current Ractor. Keep configurable
218
+ * defaults there so separate Ractors never race on process-global C values.
219
+ * Rubies without Ractor retain the historical process-wide settings. */
220
+ static VALUE curb_multi_ractor_setting(ID key) {
221
+ VALUE current;
222
+
223
+ if (NIL_P(cRubyRactor)) return Qundef;
224
+ current = rb_funcall(cRubyRactor, id_ractor_current, 0);
225
+ return rb_funcall(current, id_ractor_aref, 1, ID2SYM(key));
226
+ }
227
+
228
+ static void curb_multi_set_ractor_setting(ID key, VALUE value) {
229
+ VALUE current = rb_funcall(cRubyRactor, id_ractor_current, 0);
230
+ rb_funcall(current, id_ractor_aset, 2, ID2SYM(key), value);
231
+ }
232
+
233
+ static long curb_multi_default_timeout(void) {
234
+ VALUE value = curb_multi_ractor_setting(id_ractor_default_timeout_key);
235
+ return (UNDEF_P(value) || NIL_P(value)) ? cCurlMutiDefaulttimeout : NUM2LONG(value);
236
+ }
237
+
238
+ static int curb_multi_autoclose_enabled(void) {
239
+ VALUE value = curb_multi_ractor_setting(id_ractor_autoclose_key);
240
+ return (UNDEF_P(value) || NIL_P(value)) ? cCurlMutiAutoClose == 1 : RTEST(value);
241
+ }
242
+
89
243
  static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result);
90
244
  static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
91
245
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
@@ -476,12 +630,18 @@ static VALUE ruby_curl_multi_initialize(VALUE self) {
476
630
  * call-seq:
477
631
  * Curl::Multi.default_timeout = 4 => 4
478
632
  *
479
- * Set the global default time out for all Curl::Multi Handles. This value is used
480
- * when libcurl cannot determine a timeout value when calling curl_multi_timeout.
633
+ * Set the default timeout for Curl::Multi handles in the current Ractor. On
634
+ * Rubies without Ractor this remains process-wide. This value is used when
635
+ * libcurl cannot determine a timeout value when calling curl_multi_timeout.
481
636
  *
482
637
  */
483
638
  VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
484
- cCurlMutiDefaulttimeout = NUM2LONG(timeout);
639
+ long value = NUM2LONG(timeout);
640
+ if (NIL_P(cRubyRactor)) {
641
+ cCurlMutiDefaulttimeout = value;
642
+ } else {
643
+ curb_multi_set_ractor_setting(id_ractor_default_timeout_key, LONG2NUM(value));
644
+ }
485
645
  return timeout;
486
646
  }
487
647
 
@@ -489,23 +649,29 @@ VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
489
649
  * call-seq:
490
650
  * Curl::Multi.default_timeout = 4 => 4
491
651
  *
492
- * Get the global default time out for all Curl::Multi Handles.
652
+ * Get the current Ractor's default timeout for Curl::Multi handles.
493
653
  *
494
654
  */
495
655
  VALUE ruby_curl_multi_get_default_timeout(VALUE klass) {
496
- return LONG2NUM(cCurlMutiDefaulttimeout);
656
+ return LONG2NUM(curb_multi_default_timeout());
497
657
  }
498
658
 
499
659
  /*
500
660
  * call-seq:
501
661
  * Curl::Multi.autoclose = true => true
502
662
  *
503
- * Automatically close open connections after each request. Otherwise, the connection will remain open
504
- * for reuse until the next GC
663
+ * Automatically close open connections after each request in the current
664
+ * Ractor. Otherwise, the connection will remain open for reuse until the next
665
+ * GC.
505
666
  *
506
667
  */
507
668
  VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
508
- cCurlMutiAutoClose = ((onoff == Qtrue) ? 1 : 0);
669
+ VALUE value = onoff == Qtrue ? Qtrue : Qfalse;
670
+ if (NIL_P(cRubyRactor)) {
671
+ cCurlMutiAutoClose = RTEST(value) ? 1 : 0;
672
+ } else {
673
+ curb_multi_set_ractor_setting(id_ractor_autoclose_key, value);
674
+ }
509
675
  return onoff;
510
676
  }
511
677
 
@@ -513,11 +679,11 @@ VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
513
679
  * call-seq:
514
680
  * Curl::Multi.autoclose => true|false
515
681
  *
516
- * Get the global default autoclose setting for all Curl::Multi Handles.
682
+ * Get the current Ractor's default autoclose setting for Curl::Multi handles.
517
683
  *
518
684
  */
519
685
  VALUE ruby_curl_multi_get_autoclose(VALUE klass) {
520
- return cCurlMutiAutoClose == 1 ? Qtrue : Qfalse;
686
+ return curb_multi_autoclose_enabled() ? Qtrue : Qfalse;
521
687
  }
522
688
 
523
689
  /*
@@ -652,7 +818,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
652
818
  }
653
819
  }
654
820
 
655
- rbce->multi_attachment_generation++;
821
+ rbce->multi_attachment_generation = ++rbcm->attachment_sequence;
656
822
  st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
657
823
 
658
824
  /* track a reference to associated multi handle */
@@ -772,42 +938,17 @@ struct multi_complete_callback_args {
772
938
  ruby_curl_multi *rbcm;
773
939
  ruby_curl_easy *rbce;
774
940
  int result;
775
- st_table *attached_snapshot;
941
+ /* rbcm->attachment_sequence before the callbacks ran; easies stamped later
942
+ * were attached (or re-attached) by the callbacks. */
943
+ unsigned long attachment_sequence_before;
776
944
  };
777
945
 
778
- static int snapshot_attached_easy_i(st_data_t key, st_data_t val, st_data_t arg) {
779
- st_table *snapshot = (st_table *)arg;
780
- ruby_curl_easy *rbce = (ruby_curl_easy *)key;
781
-
782
- if (!rbce) {
783
- return ST_CONTINUE;
784
- }
785
-
786
- st_insert(snapshot, key, (st_data_t)rbce->multi_attachment_generation);
787
- return ST_CONTINUE;
788
- }
789
-
790
- static st_table *capture_attached_easy_snapshot(ruby_curl_multi *rbcm) {
791
- st_table *snapshot = st_init_numtable();
792
-
793
- if (!snapshot) {
794
- rb_raise(rb_eNoMemError, "Failed to allocate multi callback snapshot table");
795
- }
796
-
797
- if (rbcm && rbcm->attached) {
798
- st_foreach(rbcm->attached, snapshot_attached_easy_i, (st_data_t)snapshot);
799
- }
800
-
801
- return snapshot;
802
- }
803
-
804
946
  struct collect_new_attached_easies_ctx {
805
- st_table *snapshot;
947
+ unsigned long attachment_sequence_before;
806
948
  VALUE easies;
807
949
  };
808
950
 
809
951
  static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t arg) {
810
- st_data_t existing = 0;
811
952
  ruby_curl_easy *rbce = (ruby_curl_easy *)key;
812
953
  struct collect_new_attached_easies_ctx *ctx = (struct collect_new_attached_easies_ctx *)arg;
813
954
 
@@ -815,24 +956,25 @@ static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t
815
956
  return ST_CONTINUE;
816
957
  }
817
958
 
818
- if (!ctx->snapshot || !st_lookup(ctx->snapshot, key, &existing) ||
819
- existing != (st_data_t)rbce->multi_attachment_generation) {
959
+ if (rbce->multi_attachment_generation > ctx->attachment_sequence_before) {
820
960
  rb_ary_push(ctx->easies, (VALUE)val);
821
961
  }
822
962
 
823
963
  return ST_CONTINUE;
824
964
  }
825
965
 
826
- static void rb_curl_multi_remove_added_easies_since_snapshot(VALUE self, ruby_curl_multi *rbcm, st_table *snapshot) {
966
+ /* Only runs when a completion callback raised, so this walk over the
967
+ * attached table stays off the per-completion path. */
968
+ static void rb_curl_multi_remove_easies_attached_since(VALUE self, ruby_curl_multi *rbcm, unsigned long attachment_sequence_before) {
827
969
  struct collect_new_attached_easies_ctx ctx;
828
970
  VALUE easies = rb_ary_new();
829
971
  long index;
830
972
 
831
- if (!rbcm || !snapshot || !rbcm->attached) {
973
+ if (!rbcm || !rbcm->attached) {
832
974
  return;
833
975
  }
834
976
 
835
- ctx.snapshot = snapshot;
977
+ ctx.attachment_sequence_before = attachment_sequence_before;
836
978
  ctx.easies = easies;
837
979
  st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
838
980
 
@@ -869,7 +1011,7 @@ static void stash_and_raise_status_callback_error_if_unmasked(struct multi_compl
869
1011
  }
870
1012
 
871
1013
  stash_multi_exception_if_unset(args->self, exception, args->easy);
872
- rb_curl_multi_remove_added_easies_since_snapshot(args->self, args->rbcm, args->attached_snapshot);
1014
+ rb_curl_multi_remove_easies_attached_since(args->self, args->rbcm, args->attachment_sequence_before);
873
1015
  rb_exc_raise(exception);
874
1016
  }
875
1017
 
@@ -972,11 +1114,6 @@ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
972
1114
  args->rbce->multi = Qnil;
973
1115
  }
974
1116
 
975
- if (args->attached_snapshot) {
976
- st_free_table(args->attached_snapshot);
977
- args->attached_snapshot = NULL;
978
- }
979
-
980
1117
  return Qnil;
981
1118
  }
982
1119
 
@@ -1037,7 +1174,7 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
1037
1174
  rbcm,
1038
1175
  rbce,
1039
1176
  result,
1040
- capture_attached_easy_snapshot(rbcm)
1177
+ rbcm->attachment_sequence
1041
1178
  };
1042
1179
  rb_ensure(rb_curl_multi_run_completion_callbacks, (VALUE)&args,
1043
1180
  rb_curl_multi_finish_completion_callbacks, (VALUE)&args);
@@ -1084,21 +1221,14 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1084
1221
  CURLMcode mcode;
1085
1222
 
1086
1223
  /*
1087
- * curl_multi_perform will return CURLM_CALL_MULTI_PERFORM only when it wants to be called again immediately.
1088
- * When things are fine and there is nothing immediate it wants done, it'll return CURLM_OK.
1089
- *
1090
- * It will perform all pending actions on all added easy handles attached to this multi handle. We will loop
1091
- * here as long as mcode is CURLM_CALL_MULTIPERFORM.
1092
- */
1093
- do {
1094
- mcode = curl_multi_perform(multi_handle, still_running);
1095
- } while (mcode == CURLM_CALL_MULTI_PERFORM);
1096
-
1097
- /*
1098
- * Nothing more to do, check if an error occured in the loop above and raise an exception if necessary.
1224
+ * Perform all pending actions on every easy handle attached to this multi
1225
+ * handle. libcurl stopped returning CURLM_CALL_MULTI_PERFORM here in 7.20
1226
+ * (curb supports 7.58+); should it appear, treat it as success: the drive
1227
+ * loop calls curl_multi_perform again on its next pass.
1099
1228
  */
1229
+ mcode = curl_multi_perform(multi_handle, still_running);
1100
1230
 
1101
- if (mcode != CURLM_OK) {
1231
+ if (mcode != CURLM_OK && mcode != CURLM_CALL_MULTI_PERFORM) {
1102
1232
  raise_curl_multi_error_exception(mcode);
1103
1233
  }
1104
1234
 
@@ -1113,10 +1243,36 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1113
1243
 
1114
1244
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1115
1245
  /* ---- socket-action implementation (scheduler-friendly) ---- */
1246
+
1247
+ /*
1248
+ * Under a fiber scheduler, mirror libcurl's socket interest into a kernel
1249
+ * event queue (epoll or kqueue) and wait on that single descriptor with the
1250
+ * scheduler's io_wait hook. Every scheduler implements io_wait, so this avoids
1251
+ * the optional io_select hook (which Async implements by starting a thread
1252
+ * per call) and the one-descriptor fallback for schedulers without it.
1253
+ */
1254
+ #if defined(HAVE_SYS_EPOLL_H) && defined(HAVE_EPOLL_CREATE1)
1255
+ #include <sys/epoll.h>
1256
+ #define CURB_SOCKET_POLLER 1
1257
+ #define CURB_SOCKET_POLLER_EPOLL 1
1258
+ #define CURB_SOCKET_POLLER_NAME "epoll"
1259
+ #elif defined(HAVE_SYS_EVENT_H) && defined(HAVE_KQUEUE)
1260
+ #include <sys/types.h>
1261
+ #include <sys/event.h>
1262
+ #define CURB_SOCKET_POLLER 1
1263
+ #define CURB_SOCKET_POLLER_KQUEUE 1
1264
+ #define CURB_SOCKET_POLLER_NAME "kqueue"
1265
+ #endif
1266
+
1267
+ /* Events harvested per poller read; more are collected on following rounds. */
1268
+ #define CURB_SOCKET_POLLER_BATCH 64
1269
+
1116
1270
  typedef struct {
1117
1271
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
1118
1272
  long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
1119
1273
  VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
1274
+ int poller_fd; /* epoll/kqueue descriptor, or -1 when unused */
1275
+ VALUE poller_io; /* IO wrapper for poller_fd passed to io_wait */
1120
1276
  } multi_socket_ctx;
1121
1277
 
1122
1278
  static long long multi_socket_current_time_ms(void) {
@@ -1172,30 +1328,6 @@ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
1172
1328
  #define cselect_flags_str(...) ""
1173
1329
  #endif
1174
1330
 
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
1331
  #if defined(RB_INTEGER_TYPE_P)
1200
1332
  #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1201
1333
  #else
@@ -1231,12 +1363,77 @@ static int multi_socket_cselect_flags_for_wait_events(int events) {
1231
1363
  return flags;
1232
1364
  }
1233
1365
 
1366
+ #ifdef CURB_SOCKET_POLLER
1367
+ static int multi_socket_poller_create(void) {
1368
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1369
+ return epoll_create1(EPOLL_CLOEXEC);
1370
+ #else
1371
+ int fd = kqueue();
1372
+ if (fd >= 0) fcntl(fd, F_SETFD, FD_CLOEXEC);
1373
+ return fd;
1374
+ #endif
1375
+ }
1376
+
1377
+ /*
1378
+ * Bring the poller's interest for fd from old_what to new_what, where each is
1379
+ * a CURL_POLL_IN/OUT/INOUT mask and 0 means "not registered". Registration is
1380
+ * level-triggered so readiness left unconsumed is reported again. Errors are
1381
+ * ignored: the kernel drops closed descriptors from both epoll and kqueue on
1382
+ * its own, so a stale delete or a re-add after fd reuse is harmless.
1383
+ */
1384
+ static void multi_socket_poller_update(multi_socket_ctx *ctx, int fd, int old_what, int new_what) {
1385
+ if (!ctx || ctx->poller_fd < 0 || old_what == new_what) return;
1386
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1387
+ {
1388
+ struct epoll_event ev;
1389
+ memset(&ev, 0, sizeof(ev));
1390
+ ev.data.fd = fd;
1391
+ if (new_what & CURL_POLL_IN) ev.events |= EPOLLIN;
1392
+ if (new_what & CURL_POLL_OUT) ev.events |= EPOLLOUT;
1393
+
1394
+ if (new_what == 0) {
1395
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_DEL, fd, &ev);
1396
+ } else if (old_what == 0) {
1397
+ if (epoll_ctl(ctx->poller_fd, EPOLL_CTL_ADD, fd, &ev) != 0 && errno == EEXIST) {
1398
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_MOD, fd, &ev);
1399
+ }
1400
+ } else if (epoll_ctl(ctx->poller_fd, EPOLL_CTL_MOD, fd, &ev) != 0 && errno == ENOENT) {
1401
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_ADD, fd, &ev);
1402
+ }
1403
+ }
1404
+ #else
1405
+ {
1406
+ /* kqueue tracks read and write as separate filters. Apply each change on
1407
+ * its own so one failing change cannot skip the others. */
1408
+ struct kevent change;
1409
+ int old_read = (old_what & CURL_POLL_IN) != 0;
1410
+ int new_read = (new_what & CURL_POLL_IN) != 0;
1411
+ int old_write = (old_what & CURL_POLL_OUT) != 0;
1412
+ int new_write = (new_what & CURL_POLL_OUT) != 0;
1413
+
1414
+ if (old_read != new_read) {
1415
+ EV_SET(&change, fd, EVFILT_READ, new_read ? EV_ADD : EV_DELETE, 0, 0, NULL);
1416
+ kevent(ctx->poller_fd, &change, 1, NULL, 0, NULL);
1417
+ }
1418
+ if (old_write != new_write) {
1419
+ EV_SET(&change, fd, EVFILT_WRITE, new_write ? EV_ADD : EV_DELETE, 0, 0, NULL);
1420
+ kevent(ctx->poller_fd, &change, 1, NULL, 0, NULL);
1421
+ }
1422
+ }
1423
+ #endif
1424
+ }
1425
+ #endif
1426
+
1234
1427
  static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1235
1428
  st_data_t key = (st_data_t)fd;
1236
1429
  st_data_t rec;
1237
1430
 
1238
1431
  if (!ctx) return;
1239
- if (ctx->sock_map) st_delete(ctx->sock_map, &key, &rec);
1432
+ if (ctx->sock_map && st_delete(ctx->sock_map, &key, &rec)) {
1433
+ #ifdef CURB_SOCKET_POLLER
1434
+ multi_socket_poller_update(ctx, fd, (int)rec, 0);
1435
+ #endif
1436
+ }
1240
1437
  if (!NIL_P(ctx->io_cache)) rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1241
1438
  }
1242
1439
 
@@ -1266,10 +1463,14 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
1266
1463
  } else {
1267
1464
  /* store current interest mask for this fd */
1268
1465
  st_data_t key = (st_data_t)fd;
1269
- st_data_t old_what;
1270
- if (st_lookup(ctx->sock_map, key, &old_what) && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1466
+ st_data_t old_what = 0;
1467
+ int tracked = st_lookup(ctx->sock_map, key, &old_what);
1468
+ if (tracked && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1271
1469
  rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1272
1470
  }
1471
+ #ifdef CURB_SOCKET_POLLER
1472
+ multi_socket_poller_update(ctx, fd, tracked ? (int)old_what : 0, what);
1473
+ #endif
1273
1474
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1274
1475
  #if CURB_SOCKET_DEBUG
1275
1476
  {
@@ -1349,13 +1550,6 @@ static int st_pick_one_i(st_data_t key, st_data_t val, st_data_t argp) {
1349
1550
  s->found = 1;
1350
1551
  return ST_STOP;
1351
1552
  }
1352
- struct counter_state { int count; };
1353
- static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
1354
- (void)k; (void)v;
1355
- struct counter_state *c = (struct counter_state *)argp;
1356
- c->count++;
1357
- return ST_CONTINUE;
1358
- }
1359
1553
 
1360
1554
  static const char *multi_socket_io_mode_for_curl_poll(int what) {
1361
1555
  if (what == CURL_POLL_IN) return "r";
@@ -1380,6 +1574,112 @@ static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1380
1574
  return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
1381
1575
  }
1382
1576
 
1577
+ #ifdef CURB_SOCKET_POLLER
1578
+ static VALUE multi_socket_poller_io(VALUE self, multi_socket_ctx *ctx) {
1579
+ if (NIL_P(ctx->poller_io)) {
1580
+ VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(ctx->poller_fd), rb_str_new_cstr("r"));
1581
+ /* The drive loop's ensure closes poller_fd itself. */
1582
+ rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1583
+ ctx->poller_io = io;
1584
+ rb_ivar_set(self, id_socket_poller_io_ivar, io);
1585
+ }
1586
+ return ctx->poller_io;
1587
+ }
1588
+
1589
+ /*
1590
+ * Report readiness the way select() does: an error or hangup marks the socket
1591
+ * ready for whatever libcurl is waiting on, and libcurl then observes the
1592
+ * condition from the read or write itself. (CURL_CSELECT_ERR would make it
1593
+ * fail the transfer outright, even with response data still buffered.)
1594
+ */
1595
+ static int multi_socket_poller_flags(int ready_in, int ready_out, int failed, int what) {
1596
+ int flags = 0;
1597
+
1598
+ if (ready_in) flags |= CURL_CSELECT_IN;
1599
+ if (ready_out) flags |= CURL_CSELECT_OUT;
1600
+ if (failed) flags |= multi_socket_cselect_flags_for_curl_poll(what);
1601
+
1602
+ return flags;
1603
+ }
1604
+
1605
+ static void multi_socket_poller_act(ruby_curl_multi *rbcm, multi_socket_ctx *ctx, int fd, int ready_in, int ready_out, int failed, int *dispatched) {
1606
+ st_data_t what;
1607
+ int flags;
1608
+ CURLMcode mrc;
1609
+
1610
+ /* An earlier action in this batch may have made libcurl drop the socket. */
1611
+ if (!st_lookup(ctx->sock_map, (st_data_t)fd, &what)) return;
1612
+
1613
+ flags = multi_socket_poller_flags(ready_in, ready_out, failed, (int)what);
1614
+ if (!flags) return;
1615
+
1616
+ #if CURB_SOCKET_DEBUG
1617
+ {
1618
+ char b[32];
1619
+ curb_debugf("[curb.socket] poller socket_action fd=%d flags=%s", fd, cselect_flags_str(flags, b, sizeof(b)));
1620
+ }
1621
+ #endif
1622
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)fd, flags, &rbcm->running);
1623
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1624
+ (*dispatched)++;
1625
+ }
1626
+
1627
+ /* Collect pending poller events without blocking and hand each ready socket
1628
+ * to libcurl. Returns the number of socket actions performed. */
1629
+ static int multi_socket_poller_dispatch(ruby_curl_multi *rbcm, multi_socket_ctx *ctx) {
1630
+ int dispatched = 0;
1631
+ int rounds = 0;
1632
+ /* kqueue reports read and write readiness as separate events. */
1633
+ int max_rounds = (int)((ctx->sock_map->num_entries * 2) / CURB_SOCKET_POLLER_BATCH) + 1;
1634
+ int n, i;
1635
+
1636
+ do {
1637
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1638
+ struct epoll_event events[CURB_SOCKET_POLLER_BATCH];
1639
+ n = epoll_wait(ctx->poller_fd, events, CURB_SOCKET_POLLER_BATCH, 0);
1640
+ if (n < 0) {
1641
+ if (errno != EINTR) rb_sys_fail("epoll_wait");
1642
+ n = 0;
1643
+ }
1644
+ for (i = 0; i < n; i++) {
1645
+ uint32_t ev = events[i].events;
1646
+ multi_socket_poller_act(rbcm, ctx, events[i].data.fd,
1647
+ (ev & EPOLLIN) != 0, (ev & EPOLLOUT) != 0,
1648
+ (ev & (EPOLLERR | EPOLLHUP)) != 0, &dispatched);
1649
+ }
1650
+ #else
1651
+ struct kevent events[CURB_SOCKET_POLLER_BATCH];
1652
+ struct timespec no_wait = {0, 0};
1653
+ n = kevent(ctx->poller_fd, NULL, 0, events, CURB_SOCKET_POLLER_BATCH, &no_wait);
1654
+ if (n < 0) {
1655
+ if (errno != EINTR) rb_sys_fail("kevent");
1656
+ n = 0;
1657
+ }
1658
+ for (i = 0; i < n; i++) {
1659
+ if (events[i].flags & EV_ERROR) continue;
1660
+ multi_socket_poller_act(rbcm, ctx, (int)events[i].ident,
1661
+ events[i].filter == EVFILT_READ, events[i].filter == EVFILT_WRITE,
1662
+ 0, &dispatched);
1663
+ }
1664
+ #endif
1665
+ } while (n == CURB_SOCKET_POLLER_BATCH && ++rounds < max_rounds);
1666
+
1667
+ return dispatched;
1668
+ }
1669
+
1670
+ /* Wait for any tracked socket through the scheduler's io_wait hook on the
1671
+ * poller descriptor, then dispatch whatever became ready. */
1672
+ static int multi_socket_poller_wait(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE scheduler, struct timeval *tv) {
1673
+ VALUE io = multi_socket_poller_io(self, ctx);
1674
+ double timeout_s = (double)tv->tv_sec + ((double)tv->tv_usec / 1e6);
1675
+
1676
+ /* The hook's return value varies across schedulers; the non-blocking
1677
+ * dispatch below is the source of truth for what is ready. */
1678
+ curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(RB_WAITFD_IN), rb_float_new(timeout_s));
1679
+ return multi_socket_poller_dispatch(rbcm, ctx);
1680
+ }
1681
+ #endif
1682
+
1383
1683
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1384
1684
  struct build_io_select_arrays_args {
1385
1685
  multi_socket_ctx *ctx;
@@ -1387,6 +1687,7 @@ struct build_io_select_arrays_args {
1387
1687
  VALUE writables;
1388
1688
  VALUE exceptables;
1389
1689
  int failed;
1690
+ int state;
1390
1691
  };
1391
1692
 
1392
1693
  static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
@@ -1405,12 +1706,7 @@ static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp
1405
1706
  io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1406
1707
  if (io_state || NIL_P(io)) {
1407
1708
  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);
1709
+ a->state = io_state;
1414
1710
  }
1415
1711
  a->failed = 1;
1416
1712
  return ST_STOP;
@@ -1455,16 +1751,19 @@ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t arg
1455
1751
  #endif
1456
1752
 
1457
1753
  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);
1754
+ CURLMcode mrc;
1755
+
1756
+ do {
1757
+ /* Prime the state: let libcurl act on timeouts to set up sockets. */
1758
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1759
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1760
+ curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1761
+ rb_curl_multi_read_info(self, rbcm->handle);
1762
+ rb_curl_multi_yield_if_given(self, block);
1464
1763
 
1465
- while (rbcm->running) {
1764
+ while (rbcm->running) {
1466
1765
  struct timeval tv = {0, 0};
1467
- long wait_ms = cCurlMutiDefaulttimeout;
1766
+ long wait_ms = curb_multi_default_timeout();
1468
1767
 
1469
1768
  if (multi_socket_timer_due(ctx)) {
1470
1769
  ctx->timeout_deadline_ms = -1;
@@ -1473,6 +1772,10 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1473
1772
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1474
1773
  rb_curl_multi_read_info(self, rbcm->handle);
1475
1774
  rb_curl_multi_yield_if_given(self, block);
1775
+ if (rbcm->running && multi_socket_timer_due(ctx)) {
1776
+ /* A repeatedly rearmed 0ms timer must still let sibling fibers run. */
1777
+ curb_multi_scheduler_yield();
1778
+ }
1476
1779
  continue;
1477
1780
  }
1478
1781
 
@@ -1492,13 +1795,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1492
1795
  if (st.found) { wait_fd = st.fd; wait_what = st.what; }
1493
1796
  }
1494
1797
 
1495
- /* Count tracked fds for logging */
1496
- int count_tracked = 0;
1497
- if (ctx->sock_map) {
1498
- struct counter_state cs = { 0 };
1499
- st_foreach(ctx->sock_map, st_count_i, (st_data_t)&cs);
1500
- count_tracked = cs.count;
1501
- }
1798
+ int count_tracked = ctx->sock_map ? (int)ctx->sock_map->num_entries : 0;
1502
1799
 
1503
1800
  curb_debugf("[curb.socket] wait phase: tracked_fds=%d fd=%d what=%d tv=%ld.%06ld", count_tracked, wait_fd, wait_what, (long)tv.tv_sec, (long)tv.tv_usec);
1504
1801
 
@@ -1507,29 +1804,42 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1507
1804
  int ready_flags = 0;
1508
1805
 
1509
1806
  int handled_wait = 0;
1510
- if (count_tracked > 1) {
1807
+ #ifdef CURB_SOCKET_POLLER
1808
+ if (count_tracked > 0 && ctx->poller_fd >= 0) {
1809
+ VALUE scheduler = curb_fiber_scheduler_current();
1810
+ if (scheduler != Qnil) {
1811
+ /* Ready sockets are dispatched inside, so any_ready stays 0 and
1812
+ * the single-fd dispatch below is skipped. */
1813
+ int dispatched = multi_socket_poller_wait(self, rbcm, ctx, scheduler, &tv);
1814
+ curb_debugf("[curb.socket] poller wait dispatched=%d", dispatched);
1815
+ did_timeout = !dispatched && multi_socket_timer_due(ctx);
1816
+ handled_wait = 1;
1817
+ }
1818
+ }
1819
+ #endif
1820
+ if (handled_wait) {
1821
+ /* The poller already waited and dispatched. */
1822
+ } else if (count_tracked > 1) {
1511
1823
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1512
1824
  {
1513
- VALUE scheduler = rb_fiber_scheduler_current();
1825
+ VALUE scheduler = curb_fiber_scheduler_current();
1514
1826
  if (scheduler != Qnil) {
1515
1827
  VALUE readables = rb_ary_new();
1516
1828
  VALUE writables = rb_ary_new();
1517
1829
  VALUE exceptables = rb_ary_new();
1518
- struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
1830
+ struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0, 0 };
1519
1831
  st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
1832
+ if (build_args.state) rb_jump_tag(build_args.state);
1520
1833
  if (!build_args.failed) {
1521
1834
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1522
1835
  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);
1836
+ VALUE ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
1837
+ if (UNDEF_P(ready)) {
1838
+ /* The scheduler does not implement the optional io_select hook:
1839
+ * rb_fiber_scheduler_io_select returns Qundef immediately.
1840
+ * Leave handled_wait unset so the io_wait fallback below can
1841
+ * perform a cooperative wait. */
1842
+ curb_debugf("[curb.socket] scheduler io_select not implemented; falling back to io_wait");
1533
1843
  } else {
1534
1844
  handled_wait = 1;
1535
1845
  any_ready = RB_TYPE_P(ready, T_ARRAY);
@@ -1568,8 +1878,36 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1568
1878
  }
1569
1879
  }
1570
1880
  #endif
1881
+ /* Ruby versions without the public io_select C API, and newer
1882
+ * schedulers that omit the optional hook, wait cooperatively on one
1883
+ * representative descriptor, then use a zero-time select below to
1884
+ * collect readiness across the whole set. */
1885
+ if (!handled_wait && wait_fd >= 0) {
1886
+ VALUE scheduler = curb_fiber_scheduler_current();
1887
+ if (scheduler != Qnil) {
1888
+ if (!multi_socket_fd_valid_p(wait_fd)) {
1889
+ multi_socket_forget_fd(ctx, wait_fd);
1890
+ did_timeout = 1;
1891
+ handled_wait = 1;
1892
+ } else {
1893
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1894
+ int io_state = 0;
1895
+ VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1896
+ if (io_state) rb_jump_tag(io_state);
1897
+ if (!NIL_P(io)) {
1898
+ int events = multi_socket_wait_events_for_curl_poll(wait_what);
1899
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1900
+ curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), rb_float_new(timeout_s));
1901
+ tv.tv_sec = 0;
1902
+ tv.tv_usec = 0;
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1571
1907
  if (!handled_wait) {
1572
- /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1908
+ /* Multi-fd select. After the scheduler io_wait fallback above, tv is
1909
+ * zero and this only polls the complete descriptor set. Without a
1910
+ * scheduler it performs the normal GVL-releasing wait. */
1573
1911
  rb_fdset_t rfds, wfds, efds;
1574
1912
  rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1575
1913
  int maxfd = -1;
@@ -1608,9 +1946,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1608
1946
  handled_wait = 1;
1609
1947
  }
1610
1948
  } else if (count_tracked == 1) {
1611
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1612
1949
  {
1613
- VALUE scheduler = rb_fiber_scheduler_current();
1950
+ VALUE scheduler = curb_fiber_scheduler_current();
1614
1951
  if (scheduler != Qnil) {
1615
1952
  int scheduler_wait_handled = 0;
1616
1953
  int events = 0;
@@ -1620,11 +1957,9 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1620
1957
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1621
1958
  VALUE timeout = rb_float_new(timeout_s);
1622
1959
  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
1960
+ /* No pollable fd yet: sleep via the scheduler's kernel_sleep so
1961
+ * sibling fibers keep running while libcurl gets ready. */
1962
+ curb_multi_scheduler_sleep(&tv);
1628
1963
  did_timeout = multi_socket_timer_due(ctx);
1629
1964
  scheduler_wait_handled = 1;
1630
1965
  } else if (!multi_socket_fd_valid_p(wait_fd)) {
@@ -1635,44 +1970,25 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1635
1970
  struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1636
1971
  int io_state = 0;
1637
1972
  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
- }
1973
+ if (io_state) rb_jump_tag(io_state);
1974
+ if (NIL_P(io)) {
1647
1975
  any_ready = 0;
1648
1976
  } 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);
1977
+ VALUE ready = curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), timeout);
1978
+ scheduler_wait_handled = 1;
1979
+ any_ready = (ready != Qfalse && !NIL_P(ready));
1980
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1981
+ if (any_ready) {
1982
+ if (ready == Qtrue) {
1983
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1984
+ } else if (CURB_INTEGER_P(ready)) {
1985
+ ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1986
+ if (ready_flags == 0) {
1987
+ any_ready = 0;
1988
+ did_timeout = multi_socket_timer_due(ctx);
1675
1989
  }
1990
+ } else {
1991
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1676
1992
  }
1677
1993
  }
1678
1994
  }
@@ -1680,7 +1996,6 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1680
1996
  if (scheduler_wait_handled) handled_wait = 1;
1681
1997
  }
1682
1998
  }
1683
- #endif
1684
1999
  #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1685
2000
  if (!handled_wait && wait_fd >= 0) {
1686
2001
  int ev = multi_socket_wait_events_for_curl_poll(wait_what);
@@ -1724,14 +2039,13 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1724
2039
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1725
2040
  }
1726
2041
  } 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
2042
+ /* No sockets exposed yet (e.g. libcurl's threaded resolver doing DNS):
2043
+ * sleep via the scheduler's kernel_sleep when one is active so sibling
2044
+ * fibers keep running, otherwise a plain thread wait. */
2045
+ curb_multi_scheduler_sleep(&tv);
1732
2046
  /* 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. */
2047
+ * drive the timeout socket after the sleep so the state machine does
2048
+ * not stall indefinitely. */
1735
2049
  did_timeout = 1;
1736
2050
  }
1737
2051
 
@@ -1759,7 +2073,13 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1759
2073
  rb_curl_multi_read_info(self, rbcm->handle);
1760
2074
  curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
1761
2075
  rb_curl_multi_yield_if_given(self, block);
1762
- }
2076
+ }
2077
+
2078
+ /* Match the legacy perform contract: the block gets a final idle yield,
2079
+ * and work queued from that yield is driven before perform returns. */
2080
+ rb_curl_multi_read_info(self, rbcm->handle);
2081
+ rb_curl_multi_yield_if_given(self, block);
2082
+ } while (rbcm->running);
1763
2083
  }
1764
2084
 
1765
2085
  struct socket_drive_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; VALUE block; };
@@ -1769,6 +2089,9 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1769
2089
  return Qtrue;
1770
2090
  }
1771
2091
  struct socket_cleanup_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
2092
+ static VALUE multi_socket_close_io(VALUE io) {
2093
+ return rb_funcall(io, rb_intern("close"), 0);
2094
+ }
1772
2095
  static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1773
2096
  struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1774
2097
  if (c->rbcm && c->rbcm->handle) {
@@ -1786,10 +2109,27 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1786
2109
  rb_hash_clear(c->ctx->io_cache);
1787
2110
  }
1788
2111
  c->ctx->io_cache = Qnil;
2112
+ if (!NIL_P(c->ctx->poller_io)) {
2113
+ /* Let Ruby and the scheduler drop the wrapper before the descriptor
2114
+ * goes away. autoclose is off, so this leaves poller_fd open. Keep any
2115
+ * exception already propagating out of the drive loop. */
2116
+ VALUE errinfo = rb_errinfo();
2117
+ int state = 0;
2118
+ rb_protect(multi_socket_close_io, c->ctx->poller_io, &state);
2119
+ rb_set_errinfo(errinfo);
2120
+ c->ctx->poller_io = Qnil;
2121
+ }
2122
+ if (c->ctx->poller_fd >= 0) {
2123
+ close(c->ctx->poller_fd);
2124
+ c->ctx->poller_fd = -1;
2125
+ }
1789
2126
  }
1790
2127
  if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
1791
2128
  rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
1792
2129
  }
2130
+ if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_poller_io_ivar)) {
2131
+ rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_poller_io_ivar));
2132
+ }
1793
2133
  return Qnil;
1794
2134
  }
1795
2135
 
@@ -1808,7 +2148,17 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1808
2148
  ctx.sock_map = st_init_numtable();
1809
2149
  ctx.timeout_deadline_ms = -1;
1810
2150
  ctx.io_cache = rb_hash_new();
2151
+ ctx.poller_fd = -1;
2152
+ ctx.poller_io = Qnil;
1811
2153
  rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
2154
+ #ifdef CURB_SOCKET_POLLER
2155
+ /* Only fiber-scheduler waits use the poller; thread-level waits keep using
2156
+ * rb_thread_fd_select. If it cannot be created the loop falls back to the
2157
+ * io_select/io_wait paths. */
2158
+ if (curb_fiber_scheduler_current() != Qnil) {
2159
+ ctx.poller_fd = multi_socket_poller_create();
2160
+ }
2161
+ #endif
1812
2162
 
1813
2163
  /* install socket/timer callbacks */
1814
2164
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -1822,9 +2172,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1822
2172
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1823
2173
 
1824
2174
  /* finalize */
1825
- rb_curl_multi_read_info(self, rbcm->handle);
1826
- rb_curl_multi_yield_if_given(self, block);
1827
- if (cCurlMutiAutoClose == 1) {
2175
+ if (curb_multi_autoclose_enabled()) {
1828
2176
  rbcm->allow_close_during_perform = 1;
1829
2177
  rb_funcall(self, rb_intern("_autoclose"), 0);
1830
2178
  rbcm->allow_close_during_perform = 0;
@@ -1904,17 +2252,19 @@ static void *curb_select_without_gvl(void *args) {
1904
2252
  static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1905
2253
  CURLMcode mcode;
1906
2254
  ruby_curl_multi *rbcm;
2255
+ long timeout_milliseconds;
2256
+ VALUE block = Qnil;
2257
+ #ifndef CURB_USE_MULTI_POLL
1907
2258
  int maxfd, rc = -1;
1908
2259
  fd_set fdread, fdwrite, fdexcep;
1909
2260
  #ifdef _WIN32
1910
2261
  fd_set crt_fdread, crt_fdwrite, crt_fdexcep;
1911
2262
  #endif
1912
- long timeout_milliseconds;
1913
2263
  struct timeval tv = {0, 0};
1914
2264
  struct timeval tv_100ms = {0, 100000};
1915
- VALUE block = Qnil;
1916
2265
  #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1917
2266
  struct _select_set fdset_args;
2267
+ #endif
1918
2268
  #endif
1919
2269
 
1920
2270
  rb_scan_args(argc, argv, "0&", &block);
@@ -1925,7 +2275,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1925
2275
  clear_multi_deferred_exception_source_id_if_any(self);
1926
2276
  }
1927
2277
 
1928
- timeout_milliseconds = cCurlMutiDefaulttimeout;
2278
+ timeout_milliseconds = curb_multi_default_timeout();
1929
2279
 
1930
2280
  // Run curl_multi_perform for the first time to get the ball rolling
1931
2281
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
@@ -1959,31 +2309,53 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1959
2309
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1960
2310
  rb_curl_multi_read_info( self, rbcm->handle );
1961
2311
  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
2312
+ /* Yield the fiber so repeated immediate curl timeouts cannot starve
2313
+ * sibling work on this thread. */
2314
+ curb_multi_scheduler_yield();
1967
2315
  continue;
1968
2316
  }
1969
2317
 
1970
- if (timeout_milliseconds < 0 || timeout_milliseconds > cCurlMutiDefaulttimeout) {
1971
- timeout_milliseconds = cCurlMutiDefaulttimeout; /* libcurl doesn't know how long to wait, use a default timeout */
2318
+ if (timeout_milliseconds < 0 || timeout_milliseconds > curb_multi_default_timeout()) {
2319
+ timeout_milliseconds = curb_multi_default_timeout(); /* libcurl doesn't know how long to wait, use a default timeout */
1972
2320
  /* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
1973
2321
  }
1974
2322
 
1975
- #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
2323
+ #if defined(CURB_USE_MULTI_POLL)
2324
+ {
2325
+ struct multi_poll_args poll_args;
2326
+ poll_args.handle = rbcm->handle;
2327
+ poll_args.timeout_ms = timeout_milliseconds > INT_MAX ? INT_MAX : (int)timeout_milliseconds;
2328
+ poll_args.numfds = 0;
2329
+ poll_args.code = CURLM_OK;
2330
+ /*
2331
+ * Wait with the GVL released. curl_multi_poll sleeps for the full
2332
+ * timeout even when libcurl has no sockets to watch, so no separate
2333
+ * idle sleep is needed. Pending interrupts are checked when
2334
+ * rb_thread_call_without_gvl returns. Like rb_thread_fd_select, this
2335
+ * wait does not consult the fiber scheduler; perform routes to the
2336
+ * socket-action loop when a scheduler is active.
2337
+ */
2338
+ rb_thread_call_without_gvl(curb_multi_poll_without_gvl, &poll_args,
2339
+ curb_multi_poll_ubf, rbcm->handle);
2340
+ if (poll_args.code != CURLM_OK) {
2341
+ raise_curl_multi_error_exception(poll_args.code);
2342
+ }
2343
+ rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
2344
+ rb_curl_multi_read_info(self, rbcm->handle);
2345
+ rb_curl_multi_yield_if_given(self, block);
2346
+ }
2347
+ #elif defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
1976
2348
  {
1977
2349
  struct wait_args wait_args;
1978
2350
  wait_args.handle = rbcm->handle;
1979
2351
  wait_args.timeout_ms = timeout_milliseconds;
1980
2352
  wait_args.numfds = 0;
1981
2353
  /*
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.
2354
+ * Wait via curl_multi_wait with the GVL released so other Ruby
2355
+ * threads can continue to run. Like rb_thread_fd_select, this wait
2356
+ * does not consult the fiber scheduler; scheduler-aware waiting is
2357
+ * provided by the socket-action loop, which perform routes to when a
2358
+ * scheduler is active (see ruby_curl_multi_perform).
1987
2359
  */
1988
2360
  CURLMcode wait_rc;
1989
2361
  #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
@@ -1997,13 +2369,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1997
2369
  raise_curl_multi_error_exception(wait_rc);
1998
2370
  }
1999
2371
  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
2372
+ curb_multi_scheduler_sleep(&tv_100ms);
2007
2373
  }
2008
2374
  /* Process pending transfers after waiting */
2009
2375
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
@@ -2012,8 +2378,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2012
2378
  }
2013
2379
  #else
2014
2380
 
2015
- tv.tv_sec = 0; /* never wait longer than 1 second */
2016
- tv.tv_usec = (int)(timeout_milliseconds * 1000); /* XXX: int is the right type for OSX, what about linux? */
2381
+ /* Split into seconds and microseconds: a raw select() rejects a
2382
+ * tv_usec of 1000000 or more, which default_timeout > 1000 produced. */
2383
+ tv.tv_sec = timeout_milliseconds / 1000;
2384
+ tv.tv_usec = (timeout_milliseconds % 1000) * 1000;
2017
2385
 
2018
2386
  FD_ZERO(&fdread);
2019
2387
  FD_ZERO(&fdwrite);
@@ -2027,12 +2395,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2027
2395
 
2028
2396
  if (maxfd == -1) {
2029
2397
  /* 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
2398
+ curb_multi_scheduler_sleep(&tv_100ms);
2036
2399
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
2037
2400
  rb_curl_multi_read_info( self, rbcm->handle );
2038
2401
  rb_curl_multi_yield_if_given(self, block);
@@ -2055,7 +2418,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2055
2418
  #endif
2056
2419
 
2057
2420
  #ifdef HAVE_RB_THREAD_FD_SELECT
2058
- /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
2421
+ /* Wait with the GVL released. Note rb_thread_fd_select does not consult
2422
+ * the fiber scheduler — under a scheduler, perform routes to the
2423
+ * socket-action loop instead (see ruby_curl_multi_perform). Build
2424
+ * rb_fdset_t sets. */
2059
2425
  {
2060
2426
  rb_fdset_t rfds, wfds, efds;
2061
2427
  rb_fd_init(&rfds);
@@ -2115,7 +2481,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2115
2481
  rb_curl_multi_yield_if_given(self, block);
2116
2482
  } while( rbcm->running );
2117
2483
 
2118
- if (cCurlMutiAutoClose == 1) {
2484
+ if (curb_multi_autoclose_enabled()) {
2119
2485
  rbcm->allow_close_during_perform = 1;
2120
2486
  rb_funcall(self, rb_intern("_autoclose"), 0);
2121
2487
  rbcm->allow_close_during_perform = 0;
@@ -2167,9 +2533,34 @@ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE sel
2167
2533
  }
2168
2534
 
2169
2535
  VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2536
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2537
+ /*
2538
+ * The legacy fdset loop waits with rb_thread_fd_select, which releases the
2539
+ * GVL but never consults the fiber scheduler, stalling every other fiber on
2540
+ * this thread for the duration of the transfers. When the calling fiber
2541
+ * runs under a fiber scheduler, use the socket-action drive loop, which
2542
+ * waits through the scheduler's io_wait/io_select/kernel_sleep hooks.
2543
+ */
2544
+ if (curb_fiber_scheduler_current() != Qnil) {
2545
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
2546
+ }
2547
+ #endif
2170
2548
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2171
2549
  }
2172
2550
 
2551
+ /*
2552
+ * Name of the kernel event queue the socket-action loop waits on under a
2553
+ * fiber scheduler ("epoll" or "kqueue"), or nil when this build has none.
2554
+ */
2555
+ static VALUE ruby_curl_multi_socket_poller(VALUE klass) {
2556
+ (void)klass;
2557
+ #ifdef CURB_SOCKET_POLLER
2558
+ return rb_str_new_cstr(CURB_SOCKET_POLLER_NAME);
2559
+ #else
2560
+ return Qnil;
2561
+ #endif
2562
+ }
2563
+
2173
2564
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2174
2565
  VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
2175
2566
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
@@ -2233,10 +2624,31 @@ static void curl_multi_mark(void *ptr) {
2233
2624
  /* =================== INIT LIB =====================*/
2234
2625
  void init_curb_multi() {
2235
2626
  idCall = rb_intern("call");
2627
+ cRubyFiber = rb_const_get(rb_cObject, rb_intern("Fiber"));
2628
+ rb_global_variable(&cRubyFiber);
2629
+ cRubyRactor = Qnil;
2630
+ id_ractor_current = rb_intern("current");
2631
+ id_ractor_aref = rb_intern("[]");
2632
+ id_ractor_aset = rb_intern("[]=");
2633
+ id_ractor_default_timeout_key = rb_intern("__curb_multi_default_timeout");
2634
+ id_ractor_autoclose_key = rb_intern("__curb_multi_autoclose");
2635
+ if (rb_const_defined(rb_cObject, rb_intern("Ractor"))) {
2636
+ VALUE ractor = rb_const_get(rb_cObject, rb_intern("Ractor"));
2637
+ VALUE current = rb_funcall(ractor, id_ractor_current, 0);
2638
+ if (rb_respond_to(current, id_ractor_aref) && rb_respond_to(current, id_ractor_aset)) {
2639
+ cRubyRactor = ractor;
2640
+ }
2641
+ }
2642
+ rb_global_variable(&cRubyRactor);
2643
+ id_fiber_blocking_p = rb_intern("blocking?");
2644
+ id_fiber_scheduler = rb_intern("scheduler");
2645
+ id_kernel_sleep = rb_intern("kernel_sleep");
2646
+ id_io_wait = rb_intern("io_wait");
2236
2647
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
2237
2648
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2238
2649
  id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
2239
2650
  id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
2651
+ id_socket_poller_io_ivar = rb_intern("@__curb_socket_poller_io");
2240
2652
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
2241
2653
 
2242
2654
  rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
@@ -2254,14 +2666,16 @@ void init_curb_multi() {
2254
2666
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
2255
2667
  rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
2256
2668
  /*
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).
2669
+ * perform uses the legacy fdset loop by default, but routes to the
2670
+ * socket-action drive loop when the calling fiber runs under a fiber
2671
+ * scheduler so sibling fibers keep running during transfers. The
2672
+ * socket-action path is also exposed directly as _socket_perform.
2260
2673
  */
2261
2674
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
2262
2675
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2263
2676
  rb_define_private_method(cCurlMulti, "_socket_perform", ruby_curl_multi_socket_perform, -1);
2264
2677
  #endif
2678
+ rb_define_private_method(rb_singleton_class(cCurlMulti), "_socket_poller", ruby_curl_multi_socket_poller, 0);
2265
2679
  rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
2266
2680
  rb_define_private_method(cCurlMulti, "_mark_closed", ruby_curl_multi_mark_closed, 0);
2267
2681
  }