curb 1.3.7 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
 
@@ -62,7 +63,40 @@
62
63
  #include <fcntl.h>
63
64
  #endif
64
65
 
65
- #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)
66
100
  struct wait_args {
67
101
  CURLM *handle;
68
102
  long timeout_ms;
@@ -76,10 +110,16 @@ static void *curl_multi_wait_wrapper(void *p) {
76
110
  #endif
77
111
 
78
112
  static VALUE cRubyFiber;
113
+ static VALUE cRubyRactor;
79
114
  static ID id_fiber_blocking_p;
80
115
  static ID id_fiber_scheduler;
81
116
  static ID id_kernel_sleep;
82
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;
83
123
 
84
124
  /*
85
125
  * Ruby 3.0 introduced the Fiber scheduler at the Ruby level, but did not ship
@@ -163,6 +203,7 @@ static ID id_deferred_exception_ivar;
163
203
  static ID id_deferred_exception_source_id_ivar;
164
204
  static ID id_native_safety_signatures_ivar;
165
205
  static ID id_socket_io_cache_ivar;
206
+ static ID id_socket_poller_io_ivar;
166
207
 
167
208
  #ifdef RDOC_NEVER_DEFINED
168
209
  mCurl = rb_define_module("Curl");
@@ -173,6 +214,32 @@ VALUE cCurlMulti;
173
214
  static long cCurlMutiDefaulttimeout = 100; /* milliseconds */
174
215
  static char cCurlMutiAutoClose = 0;
175
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
+
176
243
  static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result);
177
244
  static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
178
245
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
@@ -183,7 +250,7 @@ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
183
250
  static void curl_multi_mark(void *ptr);
184
251
  static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm);
185
252
  static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce);
186
- static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
253
+ void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
187
254
  static VALUE ruby_curl_multi_mark_closed(VALUE self);
188
255
  static VALUE ruby_curl_multi_alloc(VALUE klass);
189
256
  static VALUE ruby_curl_multi_initialize(VALUE self);
@@ -427,7 +494,29 @@ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
427
494
  return st_lookup(rbcm->attached, (st_data_t)rbce, &value);
428
495
  }
429
496
 
430
- static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
497
+ /* Attachment, rather than easy.multi alone, distinguishes a live transfer
498
+ * from a completed easy whose status callback still sees its former multi. */
499
+ int rb_curl_multi_easy_performing(void *rbce_ptr) {
500
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
501
+ ruby_curl_multi *rbcm = ruby_curl_multi_pointer_if_compatible(rbce->multi);
502
+ return rbcm && rbcm->perform_active && rb_curl_multi_has_easy(rbcm, rbce);
503
+ }
504
+
505
+ static void ruby_curl_multi_check_thread(ruby_curl_multi *rbcm) {
506
+ if (rbcm->perform_active && rbcm->perform_thread != rb_thread_current()) {
507
+ rb_raise(rb_eRuntimeError, "Cannot access a Curl::Multi handle performing on another thread");
508
+ }
509
+ }
510
+
511
+ void rb_curl_multi_check_easy_thread(void *rbce_ptr) {
512
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
513
+ ruby_curl_multi *rbcm = ruby_curl_multi_pointer_if_compatible(rbce->multi);
514
+ if (rbcm && rb_curl_multi_has_easy(rbcm, rbce)) {
515
+ ruby_curl_multi_check_thread(rbcm);
516
+ }
517
+ }
518
+
519
+ void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
431
520
  VALUE requests;
432
521
  VALUE object_id;
433
522
  VALUE safety_signatures;
@@ -502,6 +591,7 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
502
591
  rbcm->running = 0;
503
592
  rbcm->closed = 0;
504
593
  rbcm->perform_active = 0;
594
+ rbcm->perform_thread = Qnil;
505
595
  rbcm->callback_active = 0;
506
596
  rbcm->allow_close_during_perform = 0;
507
597
 
@@ -519,6 +609,7 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
519
609
  }
520
610
 
521
611
  static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm) {
612
+ ruby_curl_multi_check_thread(rbcm);
522
613
  if (!rbcm->handle) {
523
614
  if (rbcm->closed) {
524
615
  raise_curl_multi_error_exception(CURLM_BAD_HANDLE);
@@ -548,6 +639,10 @@ static VALUE ruby_curl_multi_initialize(VALUE self) {
548
639
 
549
640
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
550
641
 
642
+ if (rbcm->perform_active) {
643
+ rb_raise(rb_eRuntimeError, "Cannot initialize an active Curl::Multi handle during perform");
644
+ }
645
+
551
646
  ruby_curl_multi_init(rbcm);
552
647
 
553
648
  /*
@@ -563,12 +658,18 @@ static VALUE ruby_curl_multi_initialize(VALUE self) {
563
658
  * call-seq:
564
659
  * Curl::Multi.default_timeout = 4 => 4
565
660
  *
566
- * Set the global default time out for all Curl::Multi Handles. This value is used
567
- * when libcurl cannot determine a timeout value when calling curl_multi_timeout.
661
+ * Set the default timeout for Curl::Multi handles in the current Ractor. On
662
+ * Rubies without Ractor this remains process-wide. This value is used when
663
+ * libcurl cannot determine a timeout value when calling curl_multi_timeout.
568
664
  *
569
665
  */
570
666
  VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
571
- cCurlMutiDefaulttimeout = NUM2LONG(timeout);
667
+ long value = NUM2LONG(timeout);
668
+ if (NIL_P(cRubyRactor)) {
669
+ cCurlMutiDefaulttimeout = value;
670
+ } else {
671
+ curb_multi_set_ractor_setting(id_ractor_default_timeout_key, LONG2NUM(value));
672
+ }
572
673
  return timeout;
573
674
  }
574
675
 
@@ -576,23 +677,29 @@ VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
576
677
  * call-seq:
577
678
  * Curl::Multi.default_timeout = 4 => 4
578
679
  *
579
- * Get the global default time out for all Curl::Multi Handles.
680
+ * Get the current Ractor's default timeout for Curl::Multi handles.
580
681
  *
581
682
  */
582
683
  VALUE ruby_curl_multi_get_default_timeout(VALUE klass) {
583
- return LONG2NUM(cCurlMutiDefaulttimeout);
684
+ return LONG2NUM(curb_multi_default_timeout());
584
685
  }
585
686
 
586
687
  /*
587
688
  * call-seq:
588
689
  * Curl::Multi.autoclose = true => true
589
690
  *
590
- * Automatically close open connections after each request. Otherwise, the connection will remain open
591
- * for reuse until the next GC
691
+ * Automatically close open connections after each request in the current
692
+ * Ractor. Otherwise, the connection will remain open for reuse until the next
693
+ * GC.
592
694
  *
593
695
  */
594
696
  VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
595
- cCurlMutiAutoClose = ((onoff == Qtrue) ? 1 : 0);
697
+ VALUE value = onoff == Qtrue ? Qtrue : Qfalse;
698
+ if (NIL_P(cRubyRactor)) {
699
+ cCurlMutiAutoClose = RTEST(value) ? 1 : 0;
700
+ } else {
701
+ curb_multi_set_ractor_setting(id_ractor_autoclose_key, value);
702
+ }
596
703
  return onoff;
597
704
  }
598
705
 
@@ -600,11 +707,11 @@ VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
600
707
  * call-seq:
601
708
  * Curl::Multi.autoclose => true|false
602
709
  *
603
- * Get the global default autoclose setting for all Curl::Multi Handles.
710
+ * Get the current Ractor's default autoclose setting for Curl::Multi handles.
604
711
  *
605
712
  */
606
713
  VALUE ruby_curl_multi_get_autoclose(VALUE klass) {
607
- return cCurlMutiAutoClose == 1 ? Qtrue : Qfalse;
714
+ return curb_multi_autoclose_enabled() ? Qtrue : Qfalse;
608
715
  }
609
716
 
610
717
  /*
@@ -702,7 +809,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
702
809
  ruby_curl_multi *existing_rbcm;
703
810
 
704
811
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
705
- TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
812
+ rbce = ruby_curl_easy_get_handle(easy);
706
813
  ruby_curl_multi_ensure_handle(rbcm);
707
814
 
708
815
  if (rb_curl_multi_has_easy(rbcm, rbce)) {
@@ -739,7 +846,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
739
846
  }
740
847
  }
741
848
 
742
- rbce->multi_attachment_generation++;
849
+ rbce->multi_attachment_generation = ++rbcm->attachment_sequence;
743
850
  st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
744
851
 
745
852
  /* track a reference to associated multi handle */
@@ -767,6 +874,7 @@ VALUE ruby_curl_multi_remove(VALUE self, VALUE rb_easy_handle) {
767
874
 
768
875
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
769
876
 
877
+ ruby_curl_multi_check_thread(rbcm);
770
878
  rb_curl_multi_remove(rbcm, rb_easy_handle);
771
879
 
772
880
  return self;
@@ -776,7 +884,7 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
776
884
  CURLMcode result;
777
885
  ruby_curl_easy *rbce;
778
886
 
779
- TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
887
+ rbce = ruby_curl_easy_get_handle(easy);
780
888
  result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
781
889
  if (result != 0) {
782
890
  raise_curl_multi_error_exception(result);
@@ -859,42 +967,17 @@ struct multi_complete_callback_args {
859
967
  ruby_curl_multi *rbcm;
860
968
  ruby_curl_easy *rbce;
861
969
  int result;
862
- st_table *attached_snapshot;
970
+ /* rbcm->attachment_sequence before the callbacks ran; easies stamped later
971
+ * were attached (or re-attached) by the callbacks. */
972
+ unsigned long attachment_sequence_before;
863
973
  };
864
974
 
865
- static int snapshot_attached_easy_i(st_data_t key, st_data_t val, st_data_t arg) {
866
- st_table *snapshot = (st_table *)arg;
867
- ruby_curl_easy *rbce = (ruby_curl_easy *)key;
868
-
869
- if (!rbce) {
870
- return ST_CONTINUE;
871
- }
872
-
873
- st_insert(snapshot, key, (st_data_t)rbce->multi_attachment_generation);
874
- return ST_CONTINUE;
875
- }
876
-
877
- static st_table *capture_attached_easy_snapshot(ruby_curl_multi *rbcm) {
878
- st_table *snapshot = st_init_numtable();
879
-
880
- if (!snapshot) {
881
- rb_raise(rb_eNoMemError, "Failed to allocate multi callback snapshot table");
882
- }
883
-
884
- if (rbcm && rbcm->attached) {
885
- st_foreach(rbcm->attached, snapshot_attached_easy_i, (st_data_t)snapshot);
886
- }
887
-
888
- return snapshot;
889
- }
890
-
891
975
  struct collect_new_attached_easies_ctx {
892
- st_table *snapshot;
976
+ unsigned long attachment_sequence_before;
893
977
  VALUE easies;
894
978
  };
895
979
 
896
980
  static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t arg) {
897
- st_data_t existing = 0;
898
981
  ruby_curl_easy *rbce = (ruby_curl_easy *)key;
899
982
  struct collect_new_attached_easies_ctx *ctx = (struct collect_new_attached_easies_ctx *)arg;
900
983
 
@@ -902,24 +985,25 @@ static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t
902
985
  return ST_CONTINUE;
903
986
  }
904
987
 
905
- if (!ctx->snapshot || !st_lookup(ctx->snapshot, key, &existing) ||
906
- existing != (st_data_t)rbce->multi_attachment_generation) {
988
+ if (rbce->multi_attachment_generation > ctx->attachment_sequence_before) {
907
989
  rb_ary_push(ctx->easies, (VALUE)val);
908
990
  }
909
991
 
910
992
  return ST_CONTINUE;
911
993
  }
912
994
 
913
- static void rb_curl_multi_remove_added_easies_since_snapshot(VALUE self, ruby_curl_multi *rbcm, st_table *snapshot) {
995
+ /* Only runs when a completion callback raised, so this walk over the
996
+ * attached table stays off the per-completion path. */
997
+ static void rb_curl_multi_remove_easies_attached_since(VALUE self, ruby_curl_multi *rbcm, unsigned long attachment_sequence_before) {
914
998
  struct collect_new_attached_easies_ctx ctx;
915
999
  VALUE easies = rb_ary_new();
916
1000
  long index;
917
1001
 
918
- if (!rbcm || !snapshot || !rbcm->attached) {
1002
+ if (!rbcm || !rbcm->attached) {
919
1003
  return;
920
1004
  }
921
1005
 
922
- ctx.snapshot = snapshot;
1006
+ ctx.attachment_sequence_before = attachment_sequence_before;
923
1007
  ctx.easies = easies;
924
1008
  st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
925
1009
 
@@ -956,7 +1040,7 @@ static void stash_and_raise_status_callback_error_if_unmasked(struct multi_compl
956
1040
  }
957
1041
 
958
1042
  stash_multi_exception_if_unset(args->self, exception, args->easy);
959
- rb_curl_multi_remove_added_easies_since_snapshot(args->self, args->rbcm, args->attached_snapshot);
1043
+ rb_curl_multi_remove_easies_attached_since(args->self, args->rbcm, args->attachment_sequence_before);
960
1044
  rb_exc_raise(exception);
961
1045
  }
962
1046
 
@@ -967,7 +1051,8 @@ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
967
1051
  VALUE easy_callback_error = Qnil;
968
1052
  VALUE callargs;
969
1053
  VALUE did_raise = rb_hash_new();
970
- long redirect_count;
1054
+ long redirect_count = 0;
1055
+ CURLcode info_result;
971
1056
 
972
1057
  args->rbcm->callback_active = 1;
973
1058
 
@@ -985,12 +1070,18 @@ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
985
1070
  }
986
1071
 
987
1072
  #ifdef HAVE_CURLINFO_RESPONSE_CODE
988
- curl_easy_getinfo(args->rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
1073
+ info_result = curl_easy_getinfo(args->rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
989
1074
  #else /* use fdsets path for waiting */
990
1075
  // old libcurl
991
- curl_easy_getinfo(args->rbce->curl, CURLINFO_HTTP_CODE, &response_code);
1076
+ info_result = curl_easy_getinfo(args->rbce->curl, CURLINFO_HTTP_CODE, &response_code);
992
1077
  #endif
993
- curl_easy_getinfo(args->rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
1078
+ if (info_result != CURLE_OK) {
1079
+ raise_curl_easy_error_exception(info_result);
1080
+ }
1081
+ info_result = curl_easy_getinfo(args->rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
1082
+ if (info_result != CURLE_OK) {
1083
+ raise_curl_easy_error_exception(info_result);
1084
+ }
994
1085
 
995
1086
  if (args->result != 0) {
996
1087
  if (!rb_easy_nil("failure_proc")) {
@@ -1059,11 +1150,6 @@ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
1059
1150
  args->rbce->multi = Qnil;
1060
1151
  }
1061
1152
 
1062
- if (args->attached_snapshot) {
1063
- st_free_table(args->attached_snapshot);
1064
- args->attached_snapshot = NULL;
1065
- }
1066
-
1067
1153
  return Qnil;
1068
1154
  }
1069
1155
 
@@ -1124,7 +1210,7 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
1124
1210
  rbcm,
1125
1211
  rbce,
1126
1212
  result,
1127
- capture_attached_easy_snapshot(rbcm)
1213
+ rbcm->attachment_sequence
1128
1214
  };
1129
1215
  rb_ensure(rb_curl_multi_run_completion_callbacks, (VALUE)&args,
1130
1216
  rb_curl_multi_finish_completion_callbacks, (VALUE)&args);
@@ -1171,21 +1257,14 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1171
1257
  CURLMcode mcode;
1172
1258
 
1173
1259
  /*
1174
- * curl_multi_perform will return CURLM_CALL_MULTI_PERFORM only when it wants to be called again immediately.
1175
- * When things are fine and there is nothing immediate it wants done, it'll return CURLM_OK.
1176
- *
1177
- * It will perform all pending actions on all added easy handles attached to this multi handle. We will loop
1178
- * here as long as mcode is CURLM_CALL_MULTIPERFORM.
1260
+ * Perform all pending actions on every easy handle attached to this multi
1261
+ * handle. libcurl stopped returning CURLM_CALL_MULTI_PERFORM here in 7.20
1262
+ * (curb supports 7.58+); should it appear, treat it as success: the drive
1263
+ * loop calls curl_multi_perform again on its next pass.
1179
1264
  */
1180
- do {
1181
- mcode = curl_multi_perform(multi_handle, still_running);
1182
- } while (mcode == CURLM_CALL_MULTI_PERFORM);
1265
+ mcode = curl_multi_perform(multi_handle, still_running);
1183
1266
 
1184
- /*
1185
- * Nothing more to do, check if an error occured in the loop above and raise an exception if necessary.
1186
- */
1187
-
1188
- if (mcode != CURLM_OK) {
1267
+ if (mcode != CURLM_OK && mcode != CURLM_CALL_MULTI_PERFORM) {
1189
1268
  raise_curl_multi_error_exception(mcode);
1190
1269
  }
1191
1270
 
@@ -1200,10 +1279,36 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1200
1279
 
1201
1280
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1202
1281
  /* ---- socket-action implementation (scheduler-friendly) ---- */
1282
+
1283
+ /*
1284
+ * Under a fiber scheduler, mirror libcurl's socket interest into a kernel
1285
+ * event queue (epoll or kqueue) and wait on that single descriptor with the
1286
+ * scheduler's io_wait hook. Every scheduler implements io_wait, so this avoids
1287
+ * the optional io_select hook (which Async implements by starting a thread
1288
+ * per call) and the one-descriptor fallback for schedulers without it.
1289
+ */
1290
+ #if defined(HAVE_SYS_EPOLL_H) && defined(HAVE_EPOLL_CREATE1)
1291
+ #include <sys/epoll.h>
1292
+ #define CURB_SOCKET_POLLER 1
1293
+ #define CURB_SOCKET_POLLER_EPOLL 1
1294
+ #define CURB_SOCKET_POLLER_NAME "epoll"
1295
+ #elif defined(HAVE_SYS_EVENT_H) && defined(HAVE_KQUEUE)
1296
+ #include <sys/types.h>
1297
+ #include <sys/event.h>
1298
+ #define CURB_SOCKET_POLLER 1
1299
+ #define CURB_SOCKET_POLLER_KQUEUE 1
1300
+ #define CURB_SOCKET_POLLER_NAME "kqueue"
1301
+ #endif
1302
+
1303
+ /* Events harvested per poller read; more are collected on following rounds. */
1304
+ #define CURB_SOCKET_POLLER_BATCH 64
1305
+
1203
1306
  typedef struct {
1204
1307
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
1205
1308
  long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
1206
1309
  VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
1310
+ int poller_fd; /* epoll/kqueue descriptor, or -1 when unused */
1311
+ VALUE poller_io; /* IO wrapper for poller_fd passed to io_wait */
1207
1312
  } multi_socket_ctx;
1208
1313
 
1209
1314
  static long long multi_socket_current_time_ms(void) {
@@ -1294,12 +1399,77 @@ static int multi_socket_cselect_flags_for_wait_events(int events) {
1294
1399
  return flags;
1295
1400
  }
1296
1401
 
1402
+ #ifdef CURB_SOCKET_POLLER
1403
+ static int multi_socket_poller_create(void) {
1404
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1405
+ return epoll_create1(EPOLL_CLOEXEC);
1406
+ #else
1407
+ int fd = kqueue();
1408
+ if (fd >= 0) fcntl(fd, F_SETFD, FD_CLOEXEC);
1409
+ return fd;
1410
+ #endif
1411
+ }
1412
+
1413
+ /*
1414
+ * Bring the poller's interest for fd from old_what to new_what, where each is
1415
+ * a CURL_POLL_IN/OUT/INOUT mask and 0 means "not registered". Registration is
1416
+ * level-triggered so readiness left unconsumed is reported again. Errors are
1417
+ * ignored: the kernel drops closed descriptors from both epoll and kqueue on
1418
+ * its own, so a stale delete or a re-add after fd reuse is harmless.
1419
+ */
1420
+ static void multi_socket_poller_update(multi_socket_ctx *ctx, int fd, int old_what, int new_what) {
1421
+ if (!ctx || ctx->poller_fd < 0 || old_what == new_what) return;
1422
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1423
+ {
1424
+ struct epoll_event ev;
1425
+ memset(&ev, 0, sizeof(ev));
1426
+ ev.data.fd = fd;
1427
+ if (new_what & CURL_POLL_IN) ev.events |= EPOLLIN;
1428
+ if (new_what & CURL_POLL_OUT) ev.events |= EPOLLOUT;
1429
+
1430
+ if (new_what == 0) {
1431
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_DEL, fd, &ev);
1432
+ } else if (old_what == 0) {
1433
+ if (epoll_ctl(ctx->poller_fd, EPOLL_CTL_ADD, fd, &ev) != 0 && errno == EEXIST) {
1434
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_MOD, fd, &ev);
1435
+ }
1436
+ } else if (epoll_ctl(ctx->poller_fd, EPOLL_CTL_MOD, fd, &ev) != 0 && errno == ENOENT) {
1437
+ epoll_ctl(ctx->poller_fd, EPOLL_CTL_ADD, fd, &ev);
1438
+ }
1439
+ }
1440
+ #else
1441
+ {
1442
+ /* kqueue tracks read and write as separate filters. Apply each change on
1443
+ * its own so one failing change cannot skip the others. */
1444
+ struct kevent change;
1445
+ int old_read = (old_what & CURL_POLL_IN) != 0;
1446
+ int new_read = (new_what & CURL_POLL_IN) != 0;
1447
+ int old_write = (old_what & CURL_POLL_OUT) != 0;
1448
+ int new_write = (new_what & CURL_POLL_OUT) != 0;
1449
+
1450
+ if (old_read != new_read) {
1451
+ EV_SET(&change, fd, EVFILT_READ, new_read ? EV_ADD : EV_DELETE, 0, 0, NULL);
1452
+ kevent(ctx->poller_fd, &change, 1, NULL, 0, NULL);
1453
+ }
1454
+ if (old_write != new_write) {
1455
+ EV_SET(&change, fd, EVFILT_WRITE, new_write ? EV_ADD : EV_DELETE, 0, 0, NULL);
1456
+ kevent(ctx->poller_fd, &change, 1, NULL, 0, NULL);
1457
+ }
1458
+ }
1459
+ #endif
1460
+ }
1461
+ #endif
1462
+
1297
1463
  static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1298
1464
  st_data_t key = (st_data_t)fd;
1299
1465
  st_data_t rec;
1300
1466
 
1301
1467
  if (!ctx) return;
1302
- if (ctx->sock_map) st_delete(ctx->sock_map, &key, &rec);
1468
+ if (ctx->sock_map && st_delete(ctx->sock_map, &key, &rec)) {
1469
+ #ifdef CURB_SOCKET_POLLER
1470
+ multi_socket_poller_update(ctx, fd, (int)rec, 0);
1471
+ #endif
1472
+ }
1303
1473
  if (!NIL_P(ctx->io_cache)) rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1304
1474
  }
1305
1475
 
@@ -1329,10 +1499,14 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
1329
1499
  } else {
1330
1500
  /* store current interest mask for this fd */
1331
1501
  st_data_t key = (st_data_t)fd;
1332
- st_data_t old_what;
1333
- if (st_lookup(ctx->sock_map, key, &old_what) && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1502
+ st_data_t old_what = 0;
1503
+ int tracked = st_lookup(ctx->sock_map, key, &old_what);
1504
+ if (tracked && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1334
1505
  rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1335
1506
  }
1507
+ #ifdef CURB_SOCKET_POLLER
1508
+ multi_socket_poller_update(ctx, fd, tracked ? (int)old_what : 0, what);
1509
+ #endif
1336
1510
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1337
1511
  #if CURB_SOCKET_DEBUG
1338
1512
  {
@@ -1412,13 +1586,6 @@ static int st_pick_one_i(st_data_t key, st_data_t val, st_data_t argp) {
1412
1586
  s->found = 1;
1413
1587
  return ST_STOP;
1414
1588
  }
1415
- struct counter_state { int count; };
1416
- static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
1417
- (void)k; (void)v;
1418
- struct counter_state *c = (struct counter_state *)argp;
1419
- c->count++;
1420
- return ST_CONTINUE;
1421
- }
1422
1589
 
1423
1590
  static const char *multi_socket_io_mode_for_curl_poll(int what) {
1424
1591
  if (what == CURL_POLL_IN) return "r";
@@ -1443,6 +1610,112 @@ static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1443
1610
  return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
1444
1611
  }
1445
1612
 
1613
+ #ifdef CURB_SOCKET_POLLER
1614
+ static VALUE multi_socket_poller_io(VALUE self, multi_socket_ctx *ctx) {
1615
+ if (NIL_P(ctx->poller_io)) {
1616
+ VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(ctx->poller_fd), rb_str_new_cstr("r"));
1617
+ /* The drive loop's ensure closes poller_fd itself. */
1618
+ rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1619
+ ctx->poller_io = io;
1620
+ rb_ivar_set(self, id_socket_poller_io_ivar, io);
1621
+ }
1622
+ return ctx->poller_io;
1623
+ }
1624
+
1625
+ /*
1626
+ * Report readiness the way select() does: an error or hangup marks the socket
1627
+ * ready for whatever libcurl is waiting on, and libcurl then observes the
1628
+ * condition from the read or write itself. (CURL_CSELECT_ERR would make it
1629
+ * fail the transfer outright, even with response data still buffered.)
1630
+ */
1631
+ static int multi_socket_poller_flags(int ready_in, int ready_out, int failed, int what) {
1632
+ int flags = 0;
1633
+
1634
+ if (ready_in) flags |= CURL_CSELECT_IN;
1635
+ if (ready_out) flags |= CURL_CSELECT_OUT;
1636
+ if (failed) flags |= multi_socket_cselect_flags_for_curl_poll(what);
1637
+
1638
+ return flags;
1639
+ }
1640
+
1641
+ 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) {
1642
+ st_data_t what;
1643
+ int flags;
1644
+ CURLMcode mrc;
1645
+
1646
+ /* An earlier action in this batch may have made libcurl drop the socket. */
1647
+ if (!st_lookup(ctx->sock_map, (st_data_t)fd, &what)) return;
1648
+
1649
+ flags = multi_socket_poller_flags(ready_in, ready_out, failed, (int)what);
1650
+ if (!flags) return;
1651
+
1652
+ #if CURB_SOCKET_DEBUG
1653
+ {
1654
+ char b[32];
1655
+ curb_debugf("[curb.socket] poller socket_action fd=%d flags=%s", fd, cselect_flags_str(flags, b, sizeof(b)));
1656
+ }
1657
+ #endif
1658
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)fd, flags, &rbcm->running);
1659
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1660
+ (*dispatched)++;
1661
+ }
1662
+
1663
+ /* Collect pending poller events without blocking and hand each ready socket
1664
+ * to libcurl. Returns the number of socket actions performed. */
1665
+ static int multi_socket_poller_dispatch(ruby_curl_multi *rbcm, multi_socket_ctx *ctx) {
1666
+ int dispatched = 0;
1667
+ int rounds = 0;
1668
+ /* kqueue reports read and write readiness as separate events. */
1669
+ int max_rounds = (int)((ctx->sock_map->num_entries * 2) / CURB_SOCKET_POLLER_BATCH) + 1;
1670
+ int n, i;
1671
+
1672
+ do {
1673
+ #if defined(CURB_SOCKET_POLLER_EPOLL)
1674
+ struct epoll_event events[CURB_SOCKET_POLLER_BATCH];
1675
+ n = epoll_wait(ctx->poller_fd, events, CURB_SOCKET_POLLER_BATCH, 0);
1676
+ if (n < 0) {
1677
+ if (errno != EINTR) rb_sys_fail("epoll_wait");
1678
+ n = 0;
1679
+ }
1680
+ for (i = 0; i < n; i++) {
1681
+ uint32_t ev = events[i].events;
1682
+ multi_socket_poller_act(rbcm, ctx, events[i].data.fd,
1683
+ (ev & EPOLLIN) != 0, (ev & EPOLLOUT) != 0,
1684
+ (ev & (EPOLLERR | EPOLLHUP)) != 0, &dispatched);
1685
+ }
1686
+ #else
1687
+ struct kevent events[CURB_SOCKET_POLLER_BATCH];
1688
+ struct timespec no_wait = {0, 0};
1689
+ n = kevent(ctx->poller_fd, NULL, 0, events, CURB_SOCKET_POLLER_BATCH, &no_wait);
1690
+ if (n < 0) {
1691
+ if (errno != EINTR) rb_sys_fail("kevent");
1692
+ n = 0;
1693
+ }
1694
+ for (i = 0; i < n; i++) {
1695
+ if (events[i].flags & EV_ERROR) continue;
1696
+ multi_socket_poller_act(rbcm, ctx, (int)events[i].ident,
1697
+ events[i].filter == EVFILT_READ, events[i].filter == EVFILT_WRITE,
1698
+ 0, &dispatched);
1699
+ }
1700
+ #endif
1701
+ } while (n == CURB_SOCKET_POLLER_BATCH && ++rounds < max_rounds);
1702
+
1703
+ return dispatched;
1704
+ }
1705
+
1706
+ /* Wait for any tracked socket through the scheduler's io_wait hook on the
1707
+ * poller descriptor, then dispatch whatever became ready. */
1708
+ static int multi_socket_poller_wait(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE scheduler, struct timeval *tv) {
1709
+ VALUE io = multi_socket_poller_io(self, ctx);
1710
+ double timeout_s = (double)tv->tv_sec + ((double)tv->tv_usec / 1e6);
1711
+
1712
+ /* The hook's return value varies across schedulers; the non-blocking
1713
+ * dispatch below is the source of truth for what is ready. */
1714
+ curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(RB_WAITFD_IN), rb_float_new(timeout_s));
1715
+ return multi_socket_poller_dispatch(rbcm, ctx);
1716
+ }
1717
+ #endif
1718
+
1446
1719
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1447
1720
  struct build_io_select_arrays_args {
1448
1721
  multi_socket_ctx *ctx;
@@ -1526,7 +1799,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1526
1799
 
1527
1800
  while (rbcm->running) {
1528
1801
  struct timeval tv = {0, 0};
1529
- long wait_ms = cCurlMutiDefaulttimeout;
1802
+ long wait_ms = curb_multi_default_timeout();
1530
1803
 
1531
1804
  if (multi_socket_timer_due(ctx)) {
1532
1805
  ctx->timeout_deadline_ms = -1;
@@ -1558,13 +1831,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1558
1831
  if (st.found) { wait_fd = st.fd; wait_what = st.what; }
1559
1832
  }
1560
1833
 
1561
- /* Count tracked fds for logging */
1562
- int count_tracked = 0;
1563
- if (ctx->sock_map) {
1564
- struct counter_state cs = { 0 };
1565
- st_foreach(ctx->sock_map, st_count_i, (st_data_t)&cs);
1566
- count_tracked = cs.count;
1567
- }
1834
+ int count_tracked = ctx->sock_map ? (int)ctx->sock_map->num_entries : 0;
1568
1835
 
1569
1836
  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);
1570
1837
 
@@ -1573,7 +1840,22 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1573
1840
  int ready_flags = 0;
1574
1841
 
1575
1842
  int handled_wait = 0;
1576
- if (count_tracked > 1) {
1843
+ #ifdef CURB_SOCKET_POLLER
1844
+ if (count_tracked > 0 && ctx->poller_fd >= 0) {
1845
+ VALUE scheduler = curb_fiber_scheduler_current();
1846
+ if (scheduler != Qnil) {
1847
+ /* Ready sockets are dispatched inside, so any_ready stays 0 and
1848
+ * the single-fd dispatch below is skipped. */
1849
+ int dispatched = multi_socket_poller_wait(self, rbcm, ctx, scheduler, &tv);
1850
+ curb_debugf("[curb.socket] poller wait dispatched=%d", dispatched);
1851
+ did_timeout = !dispatched && multi_socket_timer_due(ctx);
1852
+ handled_wait = 1;
1853
+ }
1854
+ }
1855
+ #endif
1856
+ if (handled_wait) {
1857
+ /* The poller already waited and dispatched. */
1858
+ } else if (count_tracked > 1) {
1577
1859
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1578
1860
  {
1579
1861
  VALUE scheduler = curb_fiber_scheduler_current();
@@ -1843,6 +2125,9 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1843
2125
  return Qtrue;
1844
2126
  }
1845
2127
  struct socket_cleanup_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
2128
+ static VALUE multi_socket_close_io(VALUE io) {
2129
+ return rb_funcall(io, rb_intern("close"), 0);
2130
+ }
1846
2131
  static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1847
2132
  struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1848
2133
  if (c->rbcm && c->rbcm->handle) {
@@ -1860,10 +2145,27 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1860
2145
  rb_hash_clear(c->ctx->io_cache);
1861
2146
  }
1862
2147
  c->ctx->io_cache = Qnil;
2148
+ if (!NIL_P(c->ctx->poller_io)) {
2149
+ /* Let Ruby and the scheduler drop the wrapper before the descriptor
2150
+ * goes away. autoclose is off, so this leaves poller_fd open. Keep any
2151
+ * exception already propagating out of the drive loop. */
2152
+ VALUE errinfo = rb_errinfo();
2153
+ int state = 0;
2154
+ rb_protect(multi_socket_close_io, c->ctx->poller_io, &state);
2155
+ rb_set_errinfo(errinfo);
2156
+ c->ctx->poller_io = Qnil;
2157
+ }
2158
+ if (c->ctx->poller_fd >= 0) {
2159
+ close(c->ctx->poller_fd);
2160
+ c->ctx->poller_fd = -1;
2161
+ }
1863
2162
  }
1864
2163
  if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
1865
2164
  rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
1866
2165
  }
2166
+ if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_poller_io_ivar)) {
2167
+ rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_poller_io_ivar));
2168
+ }
1867
2169
  return Qnil;
1868
2170
  }
1869
2171
 
@@ -1882,7 +2184,17 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1882
2184
  ctx.sock_map = st_init_numtable();
1883
2185
  ctx.timeout_deadline_ms = -1;
1884
2186
  ctx.io_cache = rb_hash_new();
2187
+ ctx.poller_fd = -1;
2188
+ ctx.poller_io = Qnil;
1885
2189
  rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
2190
+ #ifdef CURB_SOCKET_POLLER
2191
+ /* Only fiber-scheduler waits use the poller; thread-level waits keep using
2192
+ * rb_thread_fd_select. If it cannot be created the loop falls back to the
2193
+ * io_select/io_wait paths. */
2194
+ if (curb_fiber_scheduler_current() != Qnil) {
2195
+ ctx.poller_fd = multi_socket_poller_create();
2196
+ }
2197
+ #endif
1886
2198
 
1887
2199
  /* install socket/timer callbacks */
1888
2200
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -1896,7 +2208,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1896
2208
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1897
2209
 
1898
2210
  /* finalize */
1899
- if (cCurlMutiAutoClose == 1) {
2211
+ if (curb_multi_autoclose_enabled()) {
1900
2212
  rbcm->allow_close_during_perform = 1;
1901
2213
  rb_funcall(self, rb_intern("_autoclose"), 0);
1902
2214
  rbcm->allow_close_during_perform = 0;
@@ -1976,17 +2288,19 @@ static void *curb_select_without_gvl(void *args) {
1976
2288
  static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1977
2289
  CURLMcode mcode;
1978
2290
  ruby_curl_multi *rbcm;
2291
+ long timeout_milliseconds;
2292
+ VALUE block = Qnil;
2293
+ #ifndef CURB_USE_MULTI_POLL
1979
2294
  int maxfd, rc = -1;
1980
2295
  fd_set fdread, fdwrite, fdexcep;
1981
2296
  #ifdef _WIN32
1982
2297
  fd_set crt_fdread, crt_fdwrite, crt_fdexcep;
1983
2298
  #endif
1984
- long timeout_milliseconds;
1985
2299
  struct timeval tv = {0, 0};
1986
2300
  struct timeval tv_100ms = {0, 100000};
1987
- VALUE block = Qnil;
1988
2301
  #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1989
2302
  struct _select_set fdset_args;
2303
+ #endif
1990
2304
  #endif
1991
2305
 
1992
2306
  rb_scan_args(argc, argv, "0&", &block);
@@ -1997,7 +2311,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1997
2311
  clear_multi_deferred_exception_source_id_if_any(self);
1998
2312
  }
1999
2313
 
2000
- timeout_milliseconds = cCurlMutiDefaulttimeout;
2314
+ timeout_milliseconds = curb_multi_default_timeout();
2001
2315
 
2002
2316
  // Run curl_multi_perform for the first time to get the ball rolling
2003
2317
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
@@ -2037,12 +2351,36 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2037
2351
  continue;
2038
2352
  }
2039
2353
 
2040
- if (timeout_milliseconds < 0 || timeout_milliseconds > cCurlMutiDefaulttimeout) {
2041
- timeout_milliseconds = cCurlMutiDefaulttimeout; /* libcurl doesn't know how long to wait, use a default timeout */
2354
+ if (timeout_milliseconds < 0 || timeout_milliseconds > curb_multi_default_timeout()) {
2355
+ timeout_milliseconds = curb_multi_default_timeout(); /* libcurl doesn't know how long to wait, use a default timeout */
2042
2356
  /* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
2043
2357
  }
2044
2358
 
2045
- #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
2359
+ #if defined(CURB_USE_MULTI_POLL)
2360
+ {
2361
+ struct multi_poll_args poll_args;
2362
+ poll_args.handle = rbcm->handle;
2363
+ poll_args.timeout_ms = timeout_milliseconds > INT_MAX ? INT_MAX : (int)timeout_milliseconds;
2364
+ poll_args.numfds = 0;
2365
+ poll_args.code = CURLM_OK;
2366
+ /*
2367
+ * Wait with the GVL released. curl_multi_poll sleeps for the full
2368
+ * timeout even when libcurl has no sockets to watch, so no separate
2369
+ * idle sleep is needed. Pending interrupts are checked when
2370
+ * rb_thread_call_without_gvl returns. Like rb_thread_fd_select, this
2371
+ * wait does not consult the fiber scheduler; perform routes to the
2372
+ * socket-action loop when a scheduler is active.
2373
+ */
2374
+ rb_thread_call_without_gvl(curb_multi_poll_without_gvl, &poll_args,
2375
+ curb_multi_poll_ubf, rbcm->handle);
2376
+ if (poll_args.code != CURLM_OK) {
2377
+ raise_curl_multi_error_exception(poll_args.code);
2378
+ }
2379
+ rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
2380
+ rb_curl_multi_read_info(self, rbcm->handle);
2381
+ rb_curl_multi_yield_if_given(self, block);
2382
+ }
2383
+ #elif defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
2046
2384
  {
2047
2385
  struct wait_args wait_args;
2048
2386
  wait_args.handle = rbcm->handle;
@@ -2076,8 +2414,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2076
2414
  }
2077
2415
  #else
2078
2416
 
2079
- tv.tv_sec = 0; /* never wait longer than 1 second */
2080
- tv.tv_usec = (int)(timeout_milliseconds * 1000); /* XXX: int is the right type for OSX, what about linux? */
2417
+ /* Split into seconds and microseconds: a raw select() rejects a
2418
+ * tv_usec of 1000000 or more, which default_timeout > 1000 produced. */
2419
+ tv.tv_sec = timeout_milliseconds / 1000;
2420
+ tv.tv_usec = (timeout_milliseconds % 1000) * 1000;
2081
2421
 
2082
2422
  FD_ZERO(&fdread);
2083
2423
  FD_ZERO(&fdwrite);
@@ -2177,7 +2517,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2177
2517
  rb_curl_multi_yield_if_given(self, block);
2178
2518
  } while( rbcm->running );
2179
2519
 
2180
- if (cCurlMutiAutoClose == 1) {
2520
+ if (curb_multi_autoclose_enabled()) {
2181
2521
  rbcm->allow_close_during_perform = 1;
2182
2522
  rb_funcall(self, rb_intern("_autoclose"), 0);
2183
2523
  rbcm->allow_close_during_perform = 0;
@@ -2203,6 +2543,7 @@ static VALUE ruby_curl_multi_perform_guard_ensure(VALUE self) {
2203
2543
  ruby_curl_multi *rbcm;
2204
2544
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2205
2545
  rbcm->perform_active = 0;
2546
+ rbcm->perform_thread = Qnil;
2206
2547
  rbcm->callback_active = 0;
2207
2548
  rbcm->allow_close_during_perform = 0;
2208
2549
  return Qnil;
@@ -2217,7 +2558,11 @@ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE sel
2217
2558
  rb_raise(rb_eRuntimeError, "Cannot recursively perform an active Curl::Multi handle");
2218
2559
  }
2219
2560
 
2561
+ /* Reopening an autoclosed multi initializes its guard fields. Do this before
2562
+ * claiming ownership so repeated performs retain the full-lifetime guard. */
2563
+ ruby_curl_multi_ensure_handle(rbcm);
2220
2564
  rbcm->perform_active = 1;
2565
+ rbcm->perform_thread = rb_thread_current();
2221
2566
  args.argc = argc;
2222
2567
  args.argv = argv;
2223
2568
  args.self = self;
@@ -2244,6 +2589,19 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2244
2589
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2245
2590
  }
2246
2591
 
2592
+ /*
2593
+ * Name of the kernel event queue the socket-action loop waits on under a
2594
+ * fiber scheduler ("epoll" or "kqueue"), or nil when this build has none.
2595
+ */
2596
+ static VALUE ruby_curl_multi_socket_poller(VALUE klass) {
2597
+ (void)klass;
2598
+ #ifdef CURB_SOCKET_POLLER
2599
+ return rb_str_new_cstr(CURB_SOCKET_POLLER_NAME);
2600
+ #else
2601
+ return Qnil;
2602
+ #endif
2603
+ }
2604
+
2247
2605
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2248
2606
  VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
2249
2607
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
@@ -2261,6 +2619,7 @@ VALUE ruby_curl_multi_close(VALUE self) {
2261
2619
  ruby_curl_multi *rbcm;
2262
2620
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2263
2621
 
2622
+ ruby_curl_multi_check_thread(rbcm);
2264
2623
  if ((rbcm->perform_active || rbcm->callback_active) && !rbcm->allow_close_during_perform) {
2265
2624
  rb_raise(rb_eRuntimeError, "Cannot close an active Curl::Multi handle during perform");
2266
2625
  }
@@ -2298,6 +2657,7 @@ static int mark_attached_i(st_data_t key, st_data_t val, st_data_t arg) {
2298
2657
  static void curl_multi_mark(void *ptr) {
2299
2658
  ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
2300
2659
  if (!rbcm) return;
2660
+ rb_gc_mark(rbcm->perform_thread);
2301
2661
  if (rbcm->attached) {
2302
2662
  st_foreach(rbcm->attached, mark_attached_i, (st_data_t)0);
2303
2663
  }
@@ -2309,6 +2669,20 @@ void init_curb_multi() {
2309
2669
  idCall = rb_intern("call");
2310
2670
  cRubyFiber = rb_const_get(rb_cObject, rb_intern("Fiber"));
2311
2671
  rb_global_variable(&cRubyFiber);
2672
+ cRubyRactor = Qnil;
2673
+ id_ractor_current = rb_intern("current");
2674
+ id_ractor_aref = rb_intern("[]");
2675
+ id_ractor_aset = rb_intern("[]=");
2676
+ id_ractor_default_timeout_key = rb_intern("__curb_multi_default_timeout");
2677
+ id_ractor_autoclose_key = rb_intern("__curb_multi_autoclose");
2678
+ if (rb_const_defined(rb_cObject, rb_intern("Ractor"))) {
2679
+ VALUE ractor = rb_const_get(rb_cObject, rb_intern("Ractor"));
2680
+ VALUE current = rb_funcall(ractor, id_ractor_current, 0);
2681
+ if (rb_respond_to(current, id_ractor_aref) && rb_respond_to(current, id_ractor_aset)) {
2682
+ cRubyRactor = ractor;
2683
+ }
2684
+ }
2685
+ rb_global_variable(&cRubyRactor);
2312
2686
  id_fiber_blocking_p = rb_intern("blocking?");
2313
2687
  id_fiber_scheduler = rb_intern("scheduler");
2314
2688
  id_kernel_sleep = rb_intern("kernel_sleep");
@@ -2317,6 +2691,7 @@ void init_curb_multi() {
2317
2691
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2318
2692
  id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
2319
2693
  id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
2694
+ id_socket_poller_io_ivar = rb_intern("@__curb_socket_poller_io");
2320
2695
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
2321
2696
 
2322
2697
  rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
@@ -2343,6 +2718,7 @@ void init_curb_multi() {
2343
2718
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2344
2719
  rb_define_private_method(cCurlMulti, "_socket_perform", ruby_curl_multi_socket_perform, -1);
2345
2720
  #endif
2721
+ rb_define_private_method(rb_singleton_class(cCurlMulti), "_socket_poller", ruby_curl_multi_socket_poller, 0);
2346
2722
  rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
2347
2723
  rb_define_private_method(cCurlMulti, "_mark_closed", ruby_curl_multi_mark_closed, 0);
2348
2724
  }