curb 1.3.7 → 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
 
@@ -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);
@@ -563,12 +630,18 @@ static VALUE ruby_curl_multi_initialize(VALUE self) {
563
630
  * call-seq:
564
631
  * Curl::Multi.default_timeout = 4 => 4
565
632
  *
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.
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.
568
636
  *
569
637
  */
570
638
  VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
571
- 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
+ }
572
645
  return timeout;
573
646
  }
574
647
 
@@ -576,23 +649,29 @@ VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
576
649
  * call-seq:
577
650
  * Curl::Multi.default_timeout = 4 => 4
578
651
  *
579
- * Get the global default time out for all Curl::Multi Handles.
652
+ * Get the current Ractor's default timeout for Curl::Multi handles.
580
653
  *
581
654
  */
582
655
  VALUE ruby_curl_multi_get_default_timeout(VALUE klass) {
583
- return LONG2NUM(cCurlMutiDefaulttimeout);
656
+ return LONG2NUM(curb_multi_default_timeout());
584
657
  }
585
658
 
586
659
  /*
587
660
  * call-seq:
588
661
  * Curl::Multi.autoclose = true => true
589
662
  *
590
- * Automatically close open connections after each request. Otherwise, the connection will remain open
591
- * 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.
592
666
  *
593
667
  */
594
668
  VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
595
- 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
+ }
596
675
  return onoff;
597
676
  }
598
677
 
@@ -600,11 +679,11 @@ VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
600
679
  * call-seq:
601
680
  * Curl::Multi.autoclose => true|false
602
681
  *
603
- * Get the global default autoclose setting for all Curl::Multi Handles.
682
+ * Get the current Ractor's default autoclose setting for Curl::Multi handles.
604
683
  *
605
684
  */
606
685
  VALUE ruby_curl_multi_get_autoclose(VALUE klass) {
607
- return cCurlMutiAutoClose == 1 ? Qtrue : Qfalse;
686
+ return curb_multi_autoclose_enabled() ? Qtrue : Qfalse;
608
687
  }
609
688
 
610
689
  /*
@@ -739,7 +818,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
739
818
  }
740
819
  }
741
820
 
742
- rbce->multi_attachment_generation++;
821
+ rbce->multi_attachment_generation = ++rbcm->attachment_sequence;
743
822
  st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
744
823
 
745
824
  /* track a reference to associated multi handle */
@@ -859,42 +938,17 @@ struct multi_complete_callback_args {
859
938
  ruby_curl_multi *rbcm;
860
939
  ruby_curl_easy *rbce;
861
940
  int result;
862
- 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;
863
944
  };
864
945
 
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
946
  struct collect_new_attached_easies_ctx {
892
- st_table *snapshot;
947
+ unsigned long attachment_sequence_before;
893
948
  VALUE easies;
894
949
  };
895
950
 
896
951
  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
952
  ruby_curl_easy *rbce = (ruby_curl_easy *)key;
899
953
  struct collect_new_attached_easies_ctx *ctx = (struct collect_new_attached_easies_ctx *)arg;
900
954
 
@@ -902,24 +956,25 @@ static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t
902
956
  return ST_CONTINUE;
903
957
  }
904
958
 
905
- if (!ctx->snapshot || !st_lookup(ctx->snapshot, key, &existing) ||
906
- existing != (st_data_t)rbce->multi_attachment_generation) {
959
+ if (rbce->multi_attachment_generation > ctx->attachment_sequence_before) {
907
960
  rb_ary_push(ctx->easies, (VALUE)val);
908
961
  }
909
962
 
910
963
  return ST_CONTINUE;
911
964
  }
912
965
 
913
- 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) {
914
969
  struct collect_new_attached_easies_ctx ctx;
915
970
  VALUE easies = rb_ary_new();
916
971
  long index;
917
972
 
918
- if (!rbcm || !snapshot || !rbcm->attached) {
973
+ if (!rbcm || !rbcm->attached) {
919
974
  return;
920
975
  }
921
976
 
922
- ctx.snapshot = snapshot;
977
+ ctx.attachment_sequence_before = attachment_sequence_before;
923
978
  ctx.easies = easies;
924
979
  st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
925
980
 
@@ -956,7 +1011,7 @@ static void stash_and_raise_status_callback_error_if_unmasked(struct multi_compl
956
1011
  }
957
1012
 
958
1013
  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);
1014
+ rb_curl_multi_remove_easies_attached_since(args->self, args->rbcm, args->attachment_sequence_before);
960
1015
  rb_exc_raise(exception);
961
1016
  }
962
1017
 
@@ -1059,11 +1114,6 @@ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
1059
1114
  args->rbce->multi = Qnil;
1060
1115
  }
1061
1116
 
1062
- if (args->attached_snapshot) {
1063
- st_free_table(args->attached_snapshot);
1064
- args->attached_snapshot = NULL;
1065
- }
1066
-
1067
1117
  return Qnil;
1068
1118
  }
1069
1119
 
@@ -1124,7 +1174,7 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
1124
1174
  rbcm,
1125
1175
  rbce,
1126
1176
  result,
1127
- capture_attached_easy_snapshot(rbcm)
1177
+ rbcm->attachment_sequence
1128
1178
  };
1129
1179
  rb_ensure(rb_curl_multi_run_completion_callbacks, (VALUE)&args,
1130
1180
  rb_curl_multi_finish_completion_callbacks, (VALUE)&args);
@@ -1171,21 +1221,14 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1171
1221
  CURLMcode mcode;
1172
1222
 
1173
1223
  /*
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.
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.
1179
1228
  */
1180
- do {
1181
- mcode = curl_multi_perform(multi_handle, still_running);
1182
- } while (mcode == CURLM_CALL_MULTI_PERFORM);
1229
+ mcode = curl_multi_perform(multi_handle, still_running);
1183
1230
 
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) {
1231
+ if (mcode != CURLM_OK && mcode != CURLM_CALL_MULTI_PERFORM) {
1189
1232
  raise_curl_multi_error_exception(mcode);
1190
1233
  }
1191
1234
 
@@ -1200,10 +1243,36 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1200
1243
 
1201
1244
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1202
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
+
1203
1270
  typedef struct {
1204
1271
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
1205
1272
  long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
1206
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 */
1207
1276
  } multi_socket_ctx;
1208
1277
 
1209
1278
  static long long multi_socket_current_time_ms(void) {
@@ -1294,12 +1363,77 @@ static int multi_socket_cselect_flags_for_wait_events(int events) {
1294
1363
  return flags;
1295
1364
  }
1296
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
+
1297
1427
  static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1298
1428
  st_data_t key = (st_data_t)fd;
1299
1429
  st_data_t rec;
1300
1430
 
1301
1431
  if (!ctx) return;
1302
- 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
+ }
1303
1437
  if (!NIL_P(ctx->io_cache)) rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1304
1438
  }
1305
1439
 
@@ -1329,10 +1463,14 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
1329
1463
  } else {
1330
1464
  /* store current interest mask for this fd */
1331
1465
  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)) {
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)) {
1334
1469
  rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1335
1470
  }
1471
+ #ifdef CURB_SOCKET_POLLER
1472
+ multi_socket_poller_update(ctx, fd, tracked ? (int)old_what : 0, what);
1473
+ #endif
1336
1474
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1337
1475
  #if CURB_SOCKET_DEBUG
1338
1476
  {
@@ -1412,13 +1550,6 @@ static int st_pick_one_i(st_data_t key, st_data_t val, st_data_t argp) {
1412
1550
  s->found = 1;
1413
1551
  return ST_STOP;
1414
1552
  }
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
1553
 
1423
1554
  static const char *multi_socket_io_mode_for_curl_poll(int what) {
1424
1555
  if (what == CURL_POLL_IN) return "r";
@@ -1443,6 +1574,112 @@ static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1443
1574
  return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
1444
1575
  }
1445
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
+
1446
1683
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1447
1684
  struct build_io_select_arrays_args {
1448
1685
  multi_socket_ctx *ctx;
@@ -1526,7 +1763,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1526
1763
 
1527
1764
  while (rbcm->running) {
1528
1765
  struct timeval tv = {0, 0};
1529
- long wait_ms = cCurlMutiDefaulttimeout;
1766
+ long wait_ms = curb_multi_default_timeout();
1530
1767
 
1531
1768
  if (multi_socket_timer_due(ctx)) {
1532
1769
  ctx->timeout_deadline_ms = -1;
@@ -1558,13 +1795,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1558
1795
  if (st.found) { wait_fd = st.fd; wait_what = st.what; }
1559
1796
  }
1560
1797
 
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
- }
1798
+ int count_tracked = ctx->sock_map ? (int)ctx->sock_map->num_entries : 0;
1568
1799
 
1569
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);
1570
1801
 
@@ -1573,7 +1804,22 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1573
1804
  int ready_flags = 0;
1574
1805
 
1575
1806
  int handled_wait = 0;
1576
- 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) {
1577
1823
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1578
1824
  {
1579
1825
  VALUE scheduler = curb_fiber_scheduler_current();
@@ -1843,6 +2089,9 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1843
2089
  return Qtrue;
1844
2090
  }
1845
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
+ }
1846
2095
  static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1847
2096
  struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1848
2097
  if (c->rbcm && c->rbcm->handle) {
@@ -1860,10 +2109,27 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1860
2109
  rb_hash_clear(c->ctx->io_cache);
1861
2110
  }
1862
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
+ }
1863
2126
  }
1864
2127
  if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
1865
2128
  rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
1866
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
+ }
1867
2133
  return Qnil;
1868
2134
  }
1869
2135
 
@@ -1882,7 +2148,17 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1882
2148
  ctx.sock_map = st_init_numtable();
1883
2149
  ctx.timeout_deadline_ms = -1;
1884
2150
  ctx.io_cache = rb_hash_new();
2151
+ ctx.poller_fd = -1;
2152
+ ctx.poller_io = Qnil;
1885
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
1886
2162
 
1887
2163
  /* install socket/timer callbacks */
1888
2164
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -1896,7 +2172,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1896
2172
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1897
2173
 
1898
2174
  /* finalize */
1899
- if (cCurlMutiAutoClose == 1) {
2175
+ if (curb_multi_autoclose_enabled()) {
1900
2176
  rbcm->allow_close_during_perform = 1;
1901
2177
  rb_funcall(self, rb_intern("_autoclose"), 0);
1902
2178
  rbcm->allow_close_during_perform = 0;
@@ -1976,17 +2252,19 @@ static void *curb_select_without_gvl(void *args) {
1976
2252
  static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1977
2253
  CURLMcode mcode;
1978
2254
  ruby_curl_multi *rbcm;
2255
+ long timeout_milliseconds;
2256
+ VALUE block = Qnil;
2257
+ #ifndef CURB_USE_MULTI_POLL
1979
2258
  int maxfd, rc = -1;
1980
2259
  fd_set fdread, fdwrite, fdexcep;
1981
2260
  #ifdef _WIN32
1982
2261
  fd_set crt_fdread, crt_fdwrite, crt_fdexcep;
1983
2262
  #endif
1984
- long timeout_milliseconds;
1985
2263
  struct timeval tv = {0, 0};
1986
2264
  struct timeval tv_100ms = {0, 100000};
1987
- VALUE block = Qnil;
1988
2265
  #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1989
2266
  struct _select_set fdset_args;
2267
+ #endif
1990
2268
  #endif
1991
2269
 
1992
2270
  rb_scan_args(argc, argv, "0&", &block);
@@ -1997,7 +2275,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1997
2275
  clear_multi_deferred_exception_source_id_if_any(self);
1998
2276
  }
1999
2277
 
2000
- timeout_milliseconds = cCurlMutiDefaulttimeout;
2278
+ timeout_milliseconds = curb_multi_default_timeout();
2001
2279
 
2002
2280
  // Run curl_multi_perform for the first time to get the ball rolling
2003
2281
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
@@ -2037,12 +2315,36 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2037
2315
  continue;
2038
2316
  }
2039
2317
 
2040
- if (timeout_milliseconds < 0 || timeout_milliseconds > cCurlMutiDefaulttimeout) {
2041
- 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 */
2042
2320
  /* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
2043
2321
  }
2044
2322
 
2045
- #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)
2046
2348
  {
2047
2349
  struct wait_args wait_args;
2048
2350
  wait_args.handle = rbcm->handle;
@@ -2076,8 +2378,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2076
2378
  }
2077
2379
  #else
2078
2380
 
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? */
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;
2081
2385
 
2082
2386
  FD_ZERO(&fdread);
2083
2387
  FD_ZERO(&fdwrite);
@@ -2177,7 +2481,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2177
2481
  rb_curl_multi_yield_if_given(self, block);
2178
2482
  } while( rbcm->running );
2179
2483
 
2180
- if (cCurlMutiAutoClose == 1) {
2484
+ if (curb_multi_autoclose_enabled()) {
2181
2485
  rbcm->allow_close_during_perform = 1;
2182
2486
  rb_funcall(self, rb_intern("_autoclose"), 0);
2183
2487
  rbcm->allow_close_during_perform = 0;
@@ -2244,6 +2548,19 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2244
2548
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2245
2549
  }
2246
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
+
2247
2564
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2248
2565
  VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
2249
2566
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
@@ -2309,6 +2626,20 @@ void init_curb_multi() {
2309
2626
  idCall = rb_intern("call");
2310
2627
  cRubyFiber = rb_const_get(rb_cObject, rb_intern("Fiber"));
2311
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);
2312
2643
  id_fiber_blocking_p = rb_intern("blocking?");
2313
2644
  id_fiber_scheduler = rb_intern("scheduler");
2314
2645
  id_kernel_sleep = rb_intern("kernel_sleep");
@@ -2317,6 +2648,7 @@ void init_curb_multi() {
2317
2648
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2318
2649
  id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
2319
2650
  id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
2651
+ id_socket_poller_io_ivar = rb_intern("@__curb_socket_poller_io");
2320
2652
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
2321
2653
 
2322
2654
  rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
@@ -2343,6 +2675,7 @@ void init_curb_multi() {
2343
2675
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2344
2676
  rb_define_private_method(cCurlMulti, "_socket_perform", ruby_curl_multi_socket_perform, -1);
2345
2677
  #endif
2678
+ rb_define_private_method(rb_singleton_class(cCurlMulti), "_socket_poller", ruby_curl_multi_socket_poller, 0);
2346
2679
  rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
2347
2680
  rb_define_private_method(cCurlMulti, "_mark_closed", ruby_curl_multi_mark_closed, 0);
2348
2681
  }