curb 1.3.4 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91423579fc22ce296ae84abfa27438de27d2e9bf6da58658ce86939aabecfe86
4
- data.tar.gz: 46f1a051e546934eee05283bdbed84635ae1be3f46adaf48625140f260feb27f
3
+ metadata.gz: a502fb4afad21a24a20302b7522b616b08afbbe3d4b5b2a3eeee46d83b53c50d
4
+ data.tar.gz: 5e68fd3f3380f2045dfc6d4e8e157ebbb9fc56e4b76601574c92a755665eeef2
5
5
  SHA512:
6
- metadata.gz: b863797088f6ade8e6a48c3018a7f24b4469fda0daa35cabdacc826452b31c028ba75a2d0fd9bfefeb90817a39421110979d7013b0a573c7b06016f27e7d93fd
7
- data.tar.gz: 851be0f60e8e7e53db42f2e6b03742c403c55c7d2242282dd4bab8b77f33c4b0eb206fb221c7f26d26dafa2f6fdddd7a0e84369ca94a0afd038510910cf7d5de
6
+ metadata.gz: 940f343ad1d926ddcf27b994bca191e131176e766dce3d7f211f9ed5b28917ffb9ebfa5b8f71247ac8ca8a0b916e6565b60fb2ae57a964770b3273d1bd7d5d69
7
+ data.tar.gz: 77a44dd831eea50e786d4b4871c2f1702e3bee2c74d0484f7b0812a91d19d4bcdd79c12e20dce7cd8ffcbaf0e06ae77cc6cc7734b7c71fa2feabc636af82b0d8
data/Rakefile CHANGED
@@ -106,7 +106,24 @@ else
106
106
  task :alltests => [:unittests, :bugtests]
107
107
  end
108
108
 
109
- RubyMemcheck.config(binary_name: 'curb_core')
109
+ ruby_memcheck_config = { binary_name: 'curb_core' }
110
+
111
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION == '4.0.4'
112
+ # Ruby 4.0.4 reports fiber/block-handler VM stack accesses under Valgrind.
113
+ # Keep reporting errors that originate in curb_core, but filter Ruby-side noise.
114
+ ruby_memcheck_config[:filter_all_errors] = true
115
+ if RubyMemcheck::Configuration.instance_method(:initialize).parameters.any? { |type, name|
116
+ type == :key && name == :use_only_ruby_free_at_exit
117
+ }
118
+ ruby_memcheck_config[:use_only_ruby_free_at_exit] = false
119
+ end
120
+ ruby_memcheck_config[:skipped_ruby_functions] =
121
+ RubyMemcheck::Configuration::DEFAULT_SKIPPED_RUBY_FUNCTIONS + [
122
+ /\Arb_vm_frame_block_handler\z/
123
+ ]
124
+ end
125
+
126
+ RubyMemcheck.config(**ruby_memcheck_config)
110
127
  namespace :test do
111
128
  RubyMemcheck::TestTask.new(valgrind: :compile) do|t|
112
129
  t.test_files = FileList['tests/tc_*.rb']
data/ext/curb.h CHANGED
@@ -28,11 +28,11 @@
28
28
  #include "curb_macros.h"
29
29
 
30
30
  // These should be managed from the Rake 'release' task.
31
- #define CURB_VERSION "1.3.4"
32
- #define CURB_VER_NUM 1034
31
+ #define CURB_VERSION "1.3.5"
32
+ #define CURB_VER_NUM 1035
33
33
  #define CURB_VER_MAJ 1
34
34
  #define CURB_VER_MIN 3
35
- #define CURB_VER_MIC 4
35
+ #define CURB_VER_MIC 5
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_multi.c CHANGED
@@ -28,6 +28,10 @@
28
28
 
29
29
  #include <errno.h>
30
30
  #include <fcntl.h>
31
+ #ifndef _WIN32
32
+ #include <sys/time.h>
33
+ #include <time.h>
34
+ #endif
31
35
  #include <stdint.h>
32
36
  #include <stdarg.h>
33
37
 
@@ -70,6 +74,7 @@ extern VALUE mCurl;
70
74
  static VALUE idCall;
71
75
  static ID id_deferred_exception_ivar;
72
76
  static ID id_deferred_exception_source_id_ivar;
77
+ static ID id_socket_io_cache_ivar;
73
78
 
74
79
  #ifdef RDOC_NEVER_DEFINED
75
80
  mCurl = rb_define_module("Curl");
@@ -1070,10 +1075,28 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
1070
1075
  /* ---- socket-action implementation (scheduler-friendly) ---- */
1071
1076
  typedef struct {
1072
1077
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
1073
- long timeout_ms; /* last timeout set by libcurl timer callback */
1078
+ long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
1074
1079
  VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
1075
1080
  } multi_socket_ctx;
1076
1081
 
1082
+ static long long multi_socket_current_time_ms(void) {
1083
+ #if defined(CLOCK_MONOTONIC)
1084
+ struct timespec ts;
1085
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
1086
+ return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000);
1087
+ }
1088
+ #endif
1089
+
1090
+ struct timeval tv;
1091
+ gettimeofday(&tv, NULL);
1092
+ return ((long long)tv.tv_sec * 1000) + (tv.tv_usec / 1000);
1093
+ }
1094
+
1095
+ static int multi_socket_timer_due(multi_socket_ctx *ctx) {
1096
+ return ctx && ctx->timeout_deadline_ms >= 0 &&
1097
+ multi_socket_current_time_ms() >= ctx->timeout_deadline_ms;
1098
+ }
1099
+
1077
1100
  #if CURB_SOCKET_DEBUG
1078
1101
  static void curb_debugf(const char *fmt, ...) {
1079
1102
  va_list ap;
@@ -1118,6 +1141,21 @@ static VALUE fiber_io_wait_protected(VALUE argp) {
1118
1141
  }
1119
1142
  #endif
1120
1143
 
1144
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1145
+ struct fiber_io_select_args {
1146
+ VALUE scheduler;
1147
+ VALUE readables;
1148
+ VALUE writables;
1149
+ VALUE exceptables;
1150
+ VALUE timeout;
1151
+ };
1152
+
1153
+ static VALUE fiber_io_select_protected(VALUE argp) {
1154
+ struct fiber_io_select_args *a = (struct fiber_io_select_args *)argp;
1155
+ return rb_fiber_scheduler_io_select(a->scheduler, a->readables, a->writables, a->exceptables, a->timeout);
1156
+ }
1157
+ #endif
1158
+
1121
1159
  #if defined(RB_INTEGER_TYPE_P)
1122
1160
  #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1123
1161
  #else
@@ -1187,6 +1225,11 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
1187
1225
  #endif
1188
1226
  } else {
1189
1227
  /* store current interest mask for this fd */
1228
+ st_data_t key = (st_data_t)fd;
1229
+ st_data_t old_what;
1230
+ if (st_lookup(ctx->sock_map, key, &old_what) && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1231
+ rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1232
+ }
1190
1233
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1191
1234
  #if CURB_SOCKET_DEBUG
1192
1235
  {
@@ -1201,7 +1244,9 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
1201
1244
  static int multi_timer_cb(CURLM *multi, long timeout_ms, void *userp) {
1202
1245
  (void)multi;
1203
1246
  multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
1204
- if (ctx) ctx->timeout_ms = timeout_ms;
1247
+ if (ctx) {
1248
+ ctx->timeout_deadline_ms = timeout_ms < 0 ? -1 : multi_socket_current_time_ms() + timeout_ms;
1249
+ }
1205
1250
  curb_debugf("[curb.socket] timer_cb timeout_ms=%ld", timeout_ms);
1206
1251
  return 0;
1207
1252
  }
@@ -1272,22 +1317,102 @@ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
1272
1317
  return ST_CONTINUE;
1273
1318
  }
1274
1319
 
1275
- static VALUE multi_socket_io_for_fd(multi_socket_ctx *ctx, int fd) {
1320
+ static const char *multi_socket_io_mode_for_curl_poll(int what) {
1321
+ if (what == CURL_POLL_IN) return "r";
1322
+ if (what == CURL_POLL_OUT) return "w";
1323
+ return "r+";
1324
+ }
1325
+
1326
+ static VALUE multi_socket_io_for_fd(multi_socket_ctx *ctx, int fd, int what) {
1276
1327
  VALUE key = INT2NUM(fd);
1277
1328
  VALUE io = rb_hash_aref(ctx->io_cache, key);
1278
1329
  if (NIL_P(io)) {
1279
- io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr("r+"));
1330
+ io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr(multi_socket_io_mode_for_curl_poll(what)));
1280
1331
  rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1281
1332
  rb_hash_aset(ctx->io_cache, key, io);
1282
1333
  }
1283
1334
  return io;
1284
1335
  }
1285
1336
 
1286
- struct io_for_fd_args { multi_socket_ctx *ctx; int fd; };
1337
+ struct io_for_fd_args { multi_socket_ctx *ctx; int fd; int what; };
1287
1338
  static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1288
1339
  struct io_for_fd_args *a = (struct io_for_fd_args *)argp;
1289
- return multi_socket_io_for_fd(a->ctx, a->fd);
1340
+ return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
1341
+ }
1342
+
1343
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1344
+ struct build_io_select_arrays_args {
1345
+ multi_socket_ctx *ctx;
1346
+ VALUE readables;
1347
+ VALUE writables;
1348
+ VALUE exceptables;
1349
+ int failed;
1350
+ };
1351
+
1352
+ static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
1353
+ struct build_io_select_arrays_args *a = (struct build_io_select_arrays_args *)argp;
1354
+ int fd = (int)key;
1355
+ int what = (int)val;
1356
+ struct io_for_fd_args io_args = { a->ctx, fd, what };
1357
+ int io_state = 0;
1358
+ VALUE io;
1359
+
1360
+ if (!multi_socket_fd_valid_p(fd)) {
1361
+ a->failed = 1;
1362
+ return ST_STOP;
1363
+ }
1364
+
1365
+ io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1366
+ if (io_state || NIL_P(io)) {
1367
+ if (io_state) {
1368
+ #if CURB_SOCKET_DEBUG
1369
+ VALUE err = rb_errinfo();
1370
+ VALUE msg = rb_obj_as_string(err);
1371
+ curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1372
+ #endif
1373
+ rb_set_errinfo(Qnil);
1374
+ }
1375
+ a->failed = 1;
1376
+ return ST_STOP;
1377
+ }
1378
+
1379
+ if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) rb_ary_push(a->readables, io);
1380
+ if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) rb_ary_push(a->writables, io);
1381
+ rb_ary_push(a->exceptables, io);
1382
+
1383
+ return ST_CONTINUE;
1384
+ }
1385
+
1386
+ struct collect_io_select_ready_args {
1387
+ multi_socket_ctx *ctx;
1388
+ VALUE readables;
1389
+ VALUE writables;
1390
+ VALUE exceptables;
1391
+ struct ready_fd *fds;
1392
+ int capacity;
1393
+ int count;
1394
+ };
1395
+
1396
+ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t argp) {
1397
+ (void)val;
1398
+ struct collect_io_select_ready_args *a = (struct collect_io_select_ready_args *)argp;
1399
+ VALUE io = rb_hash_aref(a->ctx->io_cache, INT2NUM((int)key));
1400
+ int flags = 0;
1401
+
1402
+ if (NIL_P(io)) return ST_CONTINUE;
1403
+ if (RTEST(rb_ary_includes(a->readables, io))) flags |= CURL_CSELECT_IN;
1404
+ if (RTEST(rb_ary_includes(a->writables, io))) flags |= CURL_CSELECT_OUT;
1405
+ if (RTEST(rb_ary_includes(a->exceptables, io))) flags |= CURL_CSELECT_ERR;
1406
+
1407
+ if (flags && a->count < a->capacity) {
1408
+ a->fds[a->count].fd = (int)key;
1409
+ a->fds[a->count].flags = flags;
1410
+ a->count++;
1411
+ }
1412
+
1413
+ return ST_CONTINUE;
1290
1414
  }
1415
+ #endif
1291
1416
 
1292
1417
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
1293
1418
  /* prime the state: let libcurl act on timeouts to setup sockets */
@@ -1299,17 +1424,24 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1299
1424
 
1300
1425
  while (rbcm->running) {
1301
1426
  struct timeval tv = {0, 0};
1302
- if (ctx->timeout_ms < 0) {
1303
- tv.tv_sec = cCurlMutiDefaulttimeout / 1000;
1304
- tv.tv_usec = (cCurlMutiDefaulttimeout % 1000) * 1000;
1305
- } else {
1306
- long t = ctx->timeout_ms;
1307
- if (t > cCurlMutiDefaulttimeout) t = cCurlMutiDefaulttimeout;
1308
- if (t < 0) t = 0;
1309
- tv.tv_sec = t / 1000;
1310
- tv.tv_usec = (t % 1000) * 1000;
1427
+ long wait_ms = cCurlMutiDefaulttimeout;
1428
+
1429
+ if (multi_socket_timer_due(ctx)) {
1430
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1431
+ curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
1432
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1433
+ rb_curl_multi_read_info(self, rbcm->handle);
1434
+ rb_curl_multi_yield_if_given(self, block);
1435
+ continue;
1311
1436
  }
1312
1437
 
1438
+ if (ctx->timeout_deadline_ms >= 0) {
1439
+ long long remaining_ms = ctx->timeout_deadline_ms - multi_socket_current_time_ms();
1440
+ if (remaining_ms < wait_ms) wait_ms = remaining_ms < 0 ? 0 : (long)remaining_ms;
1441
+ }
1442
+ tv.tv_sec = wait_ms / 1000;
1443
+ tv.tv_usec = (wait_ms % 1000) * 1000;
1444
+
1313
1445
  /* Find a representative fd to wait on (if any). */
1314
1446
  int wait_fd = -1;
1315
1447
  int wait_what = 0;
@@ -1333,50 +1465,113 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1333
1465
  int any_ready = 0;
1334
1466
  int ready_flags = 0;
1335
1467
 
1336
- int handled_wait = 0;
1337
- if (count_tracked > 1) {
1338
- /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1339
- rb_fdset_t rfds, wfds, efds;
1340
- rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1341
- int maxfd = -1;
1342
- rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
1343
- int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
1344
- curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
1345
- if (rc < 0) {
1346
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1347
- if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
1348
- continue;
1349
- }
1350
- any_ready = (rc > 0);
1351
- did_timeout = (rc == 0);
1352
- if (any_ready) {
1353
- struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
1354
- struct collect_ready_fd_args d;
1355
- int i;
1356
- d.r = &rfds;
1357
- d.w = &wfds;
1358
- d.e = &efds;
1359
- d.fds = ready_fds;
1360
- d.capacity = count_tracked;
1361
- d.count = 0;
1362
- st_foreach(ctx->sock_map, collect_ready_fd_i, (st_data_t)&d);
1363
- for (i = 0; i < d.count; i++) {
1364
- mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
1365
- if (mrc != CURLM_OK) {
1366
- xfree(ready_fds);
1367
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1368
- raise_curl_multi_error_exception(mrc);
1369
- }
1370
- }
1371
- xfree(ready_fds);
1372
- }
1373
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1374
- handled_wait = 1;
1375
- } else if (count_tracked == 1) {
1468
+ int handled_wait = 0;
1469
+ if (count_tracked > 1) {
1470
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1471
+ {
1472
+ VALUE scheduler = rb_fiber_scheduler_current();
1473
+ if (scheduler != Qnil) {
1474
+ VALUE readables = rb_ary_new();
1475
+ VALUE writables = rb_ary_new();
1476
+ VALUE exceptables = rb_ary_new();
1477
+ struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
1478
+ st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
1479
+ if (!build_args.failed) {
1480
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1481
+ VALUE timeout = rb_float_new(timeout_s);
1482
+ struct fiber_io_select_args select_args = { scheduler, readables, writables, exceptables, timeout };
1483
+ int state = 0;
1484
+ VALUE ready = rb_protect(fiber_io_select_protected, (VALUE)&select_args, &state);
1485
+ if (state) {
1486
+ #if CURB_SOCKET_DEBUG
1487
+ VALUE err = rb_errinfo();
1488
+ VALUE msg = rb_obj_as_string(err);
1489
+ curb_debugf("[curb.socket] scheduler io_select failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1490
+ #endif
1491
+ rb_set_errinfo(Qnil);
1492
+ } else {
1493
+ handled_wait = 1;
1494
+ any_ready = RB_TYPE_P(ready, T_ARRAY);
1495
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1496
+ if (any_ready) {
1497
+ VALUE ready_readables = rb_ary_entry(ready, 0);
1498
+ VALUE ready_writables = rb_ary_entry(ready, 1);
1499
+ VALUE ready_exceptables = rb_ary_entry(ready, 2);
1500
+ struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
1501
+ struct collect_io_select_ready_args d;
1502
+ int i;
1503
+ if (!RB_TYPE_P(ready_readables, T_ARRAY)) ready_readables = rb_ary_new();
1504
+ if (!RB_TYPE_P(ready_writables, T_ARRAY)) ready_writables = rb_ary_new();
1505
+ if (!RB_TYPE_P(ready_exceptables, T_ARRAY)) ready_exceptables = rb_ary_new();
1506
+ d.ctx = ctx;
1507
+ d.readables = ready_readables;
1508
+ d.writables = ready_writables;
1509
+ d.exceptables = ready_exceptables;
1510
+ d.fds = ready_fds;
1511
+ d.capacity = count_tracked;
1512
+ d.count = 0;
1513
+ st_foreach(ctx->sock_map, collect_io_select_ready_i, (st_data_t)&d);
1514
+ any_ready = (d.count > 0);
1515
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1516
+ for (i = 0; i < d.count; i++) {
1517
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
1518
+ if (mrc != CURLM_OK) {
1519
+ xfree(ready_fds);
1520
+ raise_curl_multi_error_exception(mrc);
1521
+ }
1522
+ }
1523
+ xfree(ready_fds);
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ #endif
1530
+ if (!handled_wait) {
1531
+ /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1532
+ rb_fdset_t rfds, wfds, efds;
1533
+ rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1534
+ int maxfd = -1;
1535
+ rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
1536
+ int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
1537
+ curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
1538
+ if (rc < 0) {
1539
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1540
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
1541
+ continue;
1542
+ }
1543
+ any_ready = (rc > 0);
1544
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1545
+ if (any_ready) {
1546
+ struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
1547
+ struct collect_ready_fd_args d;
1548
+ int i;
1549
+ d.r = &rfds;
1550
+ d.w = &wfds;
1551
+ d.e = &efds;
1552
+ d.fds = ready_fds;
1553
+ d.capacity = count_tracked;
1554
+ d.count = 0;
1555
+ st_foreach(ctx->sock_map, collect_ready_fd_i, (st_data_t)&d);
1556
+ for (i = 0; i < d.count; i++) {
1557
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
1558
+ if (mrc != CURLM_OK) {
1559
+ xfree(ready_fds);
1560
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1561
+ raise_curl_multi_error_exception(mrc);
1562
+ }
1563
+ }
1564
+ xfree(ready_fds);
1565
+ }
1566
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1567
+ handled_wait = 1;
1568
+ }
1569
+ } else if (count_tracked == 1) {
1376
1570
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1377
1571
  {
1378
1572
  VALUE scheduler = rb_fiber_scheduler_current();
1379
1573
  if (scheduler != Qnil) {
1574
+ int scheduler_wait_handled = 0;
1380
1575
  int events = 0;
1381
1576
  if (wait_fd >= 0) {
1382
1577
  events = multi_socket_wait_events_for_curl_poll(wait_what);
@@ -1389,30 +1584,42 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1389
1584
  #else
1390
1585
  rb_thread_wait_for(tv);
1391
1586
  #endif
1392
- did_timeout = 1;
1587
+ did_timeout = multi_socket_timer_due(ctx);
1588
+ scheduler_wait_handled = 1;
1393
1589
  } else if (!multi_socket_fd_valid_p(wait_fd)) {
1394
1590
  multi_socket_forget_fd(ctx, wait_fd);
1395
1591
  did_timeout = 1;
1592
+ scheduler_wait_handled = 1;
1396
1593
  } else {
1397
- struct io_for_fd_args io_args = { ctx, wait_fd };
1594
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1398
1595
  int io_state = 0;
1399
1596
  VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1400
1597
  if (io_state || NIL_P(io)) {
1401
- if (io_state) rb_set_errinfo(Qnil);
1402
- multi_socket_forget_fd(ctx, wait_fd);
1403
- did_timeout = 1;
1598
+ if (io_state) {
1599
+ #if CURB_SOCKET_DEBUG
1600
+ VALUE err = rb_errinfo();
1601
+ VALUE msg = rb_obj_as_string(err);
1602
+ curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1603
+ #endif
1604
+ rb_set_errinfo(Qnil);
1605
+ }
1404
1606
  any_ready = 0;
1405
1607
  } else {
1406
1608
  struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1407
1609
  int state = 0;
1408
1610
  VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1409
1611
  if (state) {
1612
+ #if CURB_SOCKET_DEBUG
1613
+ VALUE err = rb_errinfo();
1614
+ VALUE msg = rb_obj_as_string(err);
1615
+ curb_debugf("[curb.socket] scheduler io_wait failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1616
+ #endif
1410
1617
  rb_set_errinfo(Qnil);
1411
- did_timeout = 1;
1412
1618
  any_ready = 0;
1413
1619
  } else {
1620
+ scheduler_wait_handled = 1;
1414
1621
  any_ready = (ready != Qfalse && !NIL_P(ready));
1415
- did_timeout = !any_ready;
1622
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1416
1623
  if (any_ready) {
1417
1624
  if (ready == Qtrue) {
1418
1625
  ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
@@ -1420,7 +1627,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1420
1627
  ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1421
1628
  if (ready_flags == 0) {
1422
1629
  any_ready = 0;
1423
- did_timeout = 1;
1630
+ did_timeout = multi_socket_timer_due(ctx);
1424
1631
  }
1425
1632
  } else {
1426
1633
  ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
@@ -1429,7 +1636,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1429
1636
  }
1430
1637
  }
1431
1638
  }
1432
- handled_wait = 1;
1639
+ if (scheduler_wait_handled) handled_wait = 1;
1433
1640
  }
1434
1641
  }
1435
1642
  #endif
@@ -1443,7 +1650,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1443
1650
  continue;
1444
1651
  }
1445
1652
  any_ready = (rc != 0);
1446
- did_timeout = (rc == 0);
1653
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1447
1654
  if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
1448
1655
  handled_wait = 1;
1449
1656
  }
@@ -1467,7 +1674,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1467
1674
  continue;
1468
1675
  }
1469
1676
  any_ready = (rc > 0);
1470
- did_timeout = (rc == 0);
1677
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1471
1678
  if (any_ready && wait_fd >= 0) {
1472
1679
  if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
1473
1680
  if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
@@ -1481,7 +1688,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1481
1688
  #else
1482
1689
  rb_thread_wait_for(tv);
1483
1690
  #endif
1484
- did_timeout = 1;
1691
+ did_timeout = multi_socket_timer_due(ctx);
1485
1692
  }
1486
1693
 
1487
1694
  if (did_timeout) {
@@ -1516,7 +1723,7 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1516
1723
  rb_curl_multi_socket_drive(a->self, a->rbcm, a->ctx, a->block);
1517
1724
  return Qtrue;
1518
1725
  }
1519
- struct socket_cleanup_args { ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
1726
+ struct socket_cleanup_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
1520
1727
  static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1521
1728
  struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1522
1729
  if (c->rbcm && c->rbcm->handle) {
@@ -1532,10 +1739,12 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1532
1739
  if (c->ctx) {
1533
1740
  if (!NIL_P(c->ctx->io_cache)) {
1534
1741
  rb_hash_clear(c->ctx->io_cache);
1535
- rb_gc_unregister_address(&c->ctx->io_cache);
1536
1742
  }
1537
1743
  c->ctx->io_cache = Qnil;
1538
1744
  }
1745
+ if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
1746
+ rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
1747
+ }
1539
1748
  return Qnil;
1540
1749
  }
1541
1750
 
@@ -1552,9 +1761,9 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1552
1761
 
1553
1762
  multi_socket_ctx ctx;
1554
1763
  ctx.sock_map = st_init_numtable();
1555
- ctx.timeout_ms = -1;
1764
+ ctx.timeout_deadline_ms = -1;
1556
1765
  ctx.io_cache = rb_hash_new();
1557
- rb_gc_register_address(&ctx.io_cache);
1766
+ rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
1558
1767
 
1559
1768
  /* install socket/timer callbacks */
1560
1769
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -1564,7 +1773,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1564
1773
 
1565
1774
  /* run using socket action loop with ensure-cleanup */
1566
1775
  struct socket_drive_args body_args = { self, rbcm, &ctx, block };
1567
- struct socket_cleanup_args ensure_args = { rbcm, &ctx };
1776
+ struct socket_cleanup_args ensure_args = { self, rbcm, &ctx };
1568
1777
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1569
1778
 
1570
1779
  /* finalize */
@@ -1857,10 +2066,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1857
2066
  #endif /* disabled curl_multi_wait: use fdsets */
1858
2067
  }
1859
2068
 
2069
+ rb_curl_multi_read_info( self, rbcm->handle );
2070
+ rb_curl_multi_yield_if_given(self, block);
1860
2071
  } while( rbcm->running );
1861
2072
 
1862
- rb_curl_multi_read_info( self, rbcm->handle );
1863
- rb_curl_multi_yield_if_given(self, block);
1864
2073
  if (cCurlMutiAutoClose == 1) {
1865
2074
  rbcm->allow_close_during_perform = 1;
1866
2075
  rb_funcall(self, rb_intern("_autoclose"), 0);
@@ -1981,6 +2190,7 @@ void init_curb_multi() {
1981
2190
  idCall = rb_intern("call");
1982
2191
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
1983
2192
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2193
+ id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
1984
2194
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
1985
2195
 
1986
2196
  rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
data/ext/extconf.rb CHANGED
@@ -691,6 +691,7 @@ have_func('rb_wait_for_single_fd', 'ruby/io.h')
691
691
  have_header('ruby/fiber/scheduler.h')
692
692
  have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
693
693
  have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
694
+ have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
694
695
  have_func('rb_io_stdio_file')
695
696
  have_func('curl_multi_wait')
696
697
  have_func('curl_multi_socket_action')
@@ -249,6 +249,41 @@ class TestCurbCurlMulti < Test::Unit::TestCase
249
249
  m.close if m
250
250
  end
251
251
 
252
+ def test_multi_perform_runs_work_added_from_final_idle_yield
253
+ with_queue_refill_test_server do |port, hits|
254
+ previous_autoclose = Curl::Multi.autoclose
255
+ Curl::Multi.autoclose = true
256
+
257
+ multi = Curl::Multi.new
258
+ slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
259
+ queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
260
+ completions = []
261
+ empty_yields = 0
262
+ queued_added = false
263
+
264
+ slow.on_complete { completions << :slow }
265
+ queued.on_complete { completions << :queued }
266
+
267
+ multi.add(slow)
268
+ multi.perform do |performing_multi|
269
+ empty_yields += 1 if performing_multi.requests.empty?
270
+ if !queued_added && empty_yields >= 2
271
+ queued_added = true
272
+ performing_multi.add(queued)
273
+ end
274
+ end
275
+
276
+ assert queued_added, "test should add queued work from the final idle yield"
277
+ assert_equal [:slow, :queued], completions
278
+ assert_equal 1, hits[:slow]
279
+ assert_equal 1, hits[:queued]
280
+ assert_equal 0, multi.requests.length
281
+ ensure
282
+ Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
283
+ multi.close if defined?(multi) && multi
284
+ end
285
+ end
286
+
252
287
  def test_multi_easy_get
253
288
  n = 1
254
289
  urls = []
@@ -15,10 +15,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
15
15
  include TestServerMethods
16
16
 
17
17
  class RecordingScheduler
18
- attr_reader :io_wait_events
18
+ attr_reader :io_wait_events, :io_select_calls
19
19
 
20
20
  def initialize
21
21
  @io_wait_events = []
22
+ @io_select_calls = 0
22
23
  end
23
24
 
24
25
  def fiber(&block)
@@ -31,7 +32,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
31
32
 
32
33
  readers = (events & IO::READABLE) != 0 ? [io] : nil
33
34
  writers = (events & IO::WRITABLE) != 0 ? [io] : nil
34
- readable, writable = IO.select(readers, writers, nil, timeout)
35
+ readable, writable = blocking_io { IO.select(readers, writers, nil, timeout) }
35
36
 
36
37
  ready = 0
37
38
  ready |= IO::READABLE if readable && !readable.empty?
@@ -39,6 +40,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
39
40
  ready.zero? ? false : ready
40
41
  end
41
42
 
43
+ def io_select(readers, writers, excepts, timeout = nil)
44
+ @io_select_calls += 1
45
+ blocking_io { IO.select(readers, writers, excepts, timeout) }
46
+ end
47
+
42
48
  def kernel_sleep(duration = nil)
43
49
  sleep(duration || 0)
44
50
  end
@@ -56,6 +62,16 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
56
62
 
57
63
  def fiber_interrupt(*)
58
64
  end
65
+
66
+ private
67
+
68
+ def blocking_io(&block)
69
+ if Fiber.respond_to?(:blocking)
70
+ Fiber.blocking(&block)
71
+ else
72
+ block.call
73
+ end
74
+ end
59
75
  end
60
76
 
61
77
  ITERS = 4
@@ -208,9 +224,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
208
224
  end
209
225
 
210
226
  assert_equal 200, result
211
- assert_operator scheduler.io_wait_events.length, :>=, 1
212
- assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
213
- assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
227
+ assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1
228
+ unless scheduler.io_wait_events.empty?
229
+ assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
230
+ assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
231
+ end
214
232
  end
215
233
 
216
234
  def test_multi_reuse_after_scheduler_perform
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-12 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
109
  requirements: []
110
- rubygems_version: 4.0.6
110
+ rubygems_version: 4.0.10
111
111
  specification_version: 4
112
112
  summary: Ruby libcurl bindings
113
113
  test_files: