curb 1.3.3 → 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: 363701a514e690ec0d3daab68555267a6640caa7242583941b674b2712eac9e4
4
- data.tar.gz: 9cc165c1079d65dbff540ccdf10b4e456da09d21b81da52ecc9038b414e2c906
3
+ metadata.gz: a502fb4afad21a24a20302b7522b616b08afbbe3d4b5b2a3eeee46d83b53c50d
4
+ data.tar.gz: 5e68fd3f3380f2045dfc6d4e8e157ebbb9fc56e4b76601574c92a755665eeef2
5
5
  SHA512:
6
- metadata.gz: 04cd7fece28ee5166ccf853d73686ff083b8ffac74c225f09f7795a39db27753a523736cfe604ad9880816f750774ba7f03f2ac0b2771276e8a3cefb75c60cf1
7
- data.tar.gz: 7cf816b75ca9cafdd36bd9a7138735f19d609b9d2287066aff29f541f9581c5c10dcd70ac915a962e001d745ee5e8b7f37b2328819ac455f680f835225b64a75
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.3"
32
- #define CURB_VER_NUM 1033
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 3
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
  }
@@ -1225,18 +1270,32 @@ static void rb_fdset_from_sockmap(st_table *map, rb_fdset_t *rfds, rb_fdset_t *w
1225
1270
  *maxfd_out = a.maxfd;
1226
1271
  }
1227
1272
 
1228
- struct dispatch_args { CURLM *mh; int *running; CURLMcode mrc; rb_fdset_t *r; rb_fdset_t *w; rb_fdset_t *e; };
1229
- static int dispatch_ready_fd_i(st_data_t key, st_data_t val, st_data_t argp) {
1273
+ struct ready_fd {
1274
+ int fd;
1275
+ int flags;
1276
+ };
1277
+
1278
+ struct collect_ready_fd_args {
1279
+ rb_fdset_t *r;
1280
+ rb_fdset_t *w;
1281
+ rb_fdset_t *e;
1282
+ struct ready_fd *fds;
1283
+ int capacity;
1284
+ int count;
1285
+ };
1286
+
1287
+ static int collect_ready_fd_i(st_data_t key, st_data_t val, st_data_t argp) {
1230
1288
  (void)val;
1231
- struct dispatch_args *dp = (struct dispatch_args *)argp;
1289
+ struct collect_ready_fd_args *a = (struct collect_ready_fd_args *)argp;
1232
1290
  int fd = (int)key;
1233
1291
  int flags = 0;
1234
- if (rb_fd_isset(fd, dp->r)) flags |= CURL_CSELECT_IN;
1235
- if (rb_fd_isset(fd, dp->w)) flags |= CURL_CSELECT_OUT;
1236
- if (rb_fd_isset(fd, dp->e)) flags |= CURL_CSELECT_ERR;
1237
- if (flags) {
1238
- dp->mrc = curl_multi_socket_action(dp->mh, (curl_socket_t)fd, flags, dp->running);
1239
- if (dp->mrc != CURLM_OK) return ST_STOP;
1292
+ if (rb_fd_isset(fd, a->r)) flags |= CURL_CSELECT_IN;
1293
+ if (rb_fd_isset(fd, a->w)) flags |= CURL_CSELECT_OUT;
1294
+ if (rb_fd_isset(fd, a->e)) flags |= CURL_CSELECT_ERR;
1295
+ if (flags && a->count < a->capacity) {
1296
+ a->fds[a->count].fd = fd;
1297
+ a->fds[a->count].flags = flags;
1298
+ a->count++;
1240
1299
  }
1241
1300
  return ST_CONTINUE;
1242
1301
  }
@@ -1258,23 +1317,103 @@ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
1258
1317
  return ST_CONTINUE;
1259
1318
  }
1260
1319
 
1261
- 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) {
1262
1327
  VALUE key = INT2NUM(fd);
1263
1328
  VALUE io = rb_hash_aref(ctx->io_cache, key);
1264
1329
  if (NIL_P(io)) {
1265
- 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)));
1266
1331
  rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1267
1332
  rb_hash_aset(ctx->io_cache, key, io);
1268
1333
  }
1269
1334
  return io;
1270
1335
  }
1271
1336
 
1272
- 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; };
1273
1338
  static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1274
1339
  struct io_for_fd_args *a = (struct io_for_fd_args *)argp;
1275
- 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;
1276
1384
  }
1277
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;
1414
+ }
1415
+ #endif
1416
+
1278
1417
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
1279
1418
  /* prime the state: let libcurl act on timeouts to setup sockets */
1280
1419
  CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
@@ -1285,17 +1424,24 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1285
1424
 
1286
1425
  while (rbcm->running) {
1287
1426
  struct timeval tv = {0, 0};
1288
- if (ctx->timeout_ms < 0) {
1289
- tv.tv_sec = cCurlMutiDefaulttimeout / 1000;
1290
- tv.tv_usec = (cCurlMutiDefaulttimeout % 1000) * 1000;
1291
- } else {
1292
- long t = ctx->timeout_ms;
1293
- if (t > cCurlMutiDefaulttimeout) t = cCurlMutiDefaulttimeout;
1294
- if (t < 0) t = 0;
1295
- tv.tv_sec = t / 1000;
1296
- 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;
1297
1436
  }
1298
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
+
1299
1445
  /* Find a representative fd to wait on (if any). */
1300
1446
  int wait_fd = -1;
1301
1447
  int wait_what = 0;
@@ -1319,37 +1465,113 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1319
1465
  int any_ready = 0;
1320
1466
  int ready_flags = 0;
1321
1467
 
1322
- int handled_wait = 0;
1323
- if (count_tracked > 1) {
1324
- /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1325
- rb_fdset_t rfds, wfds, efds;
1326
- rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1327
- int maxfd = -1;
1328
- rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
1329
- int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
1330
- curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
1331
- if (rc < 0) {
1332
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1333
- if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
1334
- continue;
1335
- }
1336
- any_ready = (rc > 0);
1337
- did_timeout = (rc == 0);
1338
- if (any_ready) {
1339
- struct dispatch_args d; d.mh = rbcm->handle; d.running = &rbcm->running; d.mrc = CURLM_OK; d.r = &rfds; d.w = &wfds; d.e = &efds;
1340
- st_foreach(ctx->sock_map, dispatch_ready_fd_i, (st_data_t)&d);
1341
- if (d.mrc != CURLM_OK) {
1342
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1343
- raise_curl_multi_error_exception(d.mrc);
1344
- }
1345
- }
1346
- rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1347
- handled_wait = 1;
1348
- } 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) {
1349
1570
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1350
1571
  {
1351
1572
  VALUE scheduler = rb_fiber_scheduler_current();
1352
1573
  if (scheduler != Qnil) {
1574
+ int scheduler_wait_handled = 0;
1353
1575
  int events = 0;
1354
1576
  if (wait_fd >= 0) {
1355
1577
  events = multi_socket_wait_events_for_curl_poll(wait_what);
@@ -1362,30 +1584,42 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1362
1584
  #else
1363
1585
  rb_thread_wait_for(tv);
1364
1586
  #endif
1365
- did_timeout = 1;
1587
+ did_timeout = multi_socket_timer_due(ctx);
1588
+ scheduler_wait_handled = 1;
1366
1589
  } else if (!multi_socket_fd_valid_p(wait_fd)) {
1367
1590
  multi_socket_forget_fd(ctx, wait_fd);
1368
1591
  did_timeout = 1;
1592
+ scheduler_wait_handled = 1;
1369
1593
  } else {
1370
- struct io_for_fd_args io_args = { ctx, wait_fd };
1594
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1371
1595
  int io_state = 0;
1372
1596
  VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1373
1597
  if (io_state || NIL_P(io)) {
1374
- if (io_state) rb_set_errinfo(Qnil);
1375
- multi_socket_forget_fd(ctx, wait_fd);
1376
- 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
+ }
1377
1606
  any_ready = 0;
1378
1607
  } else {
1379
1608
  struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1380
1609
  int state = 0;
1381
1610
  VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1382
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
1383
1617
  rb_set_errinfo(Qnil);
1384
- did_timeout = 1;
1385
1618
  any_ready = 0;
1386
1619
  } else {
1620
+ scheduler_wait_handled = 1;
1387
1621
  any_ready = (ready != Qfalse && !NIL_P(ready));
1388
- did_timeout = !any_ready;
1622
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1389
1623
  if (any_ready) {
1390
1624
  if (ready == Qtrue) {
1391
1625
  ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
@@ -1393,7 +1627,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1393
1627
  ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1394
1628
  if (ready_flags == 0) {
1395
1629
  any_ready = 0;
1396
- did_timeout = 1;
1630
+ did_timeout = multi_socket_timer_due(ctx);
1397
1631
  }
1398
1632
  } else {
1399
1633
  ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
@@ -1402,7 +1636,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1402
1636
  }
1403
1637
  }
1404
1638
  }
1405
- handled_wait = 1;
1639
+ if (scheduler_wait_handled) handled_wait = 1;
1406
1640
  }
1407
1641
  }
1408
1642
  #endif
@@ -1416,7 +1650,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1416
1650
  continue;
1417
1651
  }
1418
1652
  any_ready = (rc != 0);
1419
- did_timeout = (rc == 0);
1653
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1420
1654
  if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
1421
1655
  handled_wait = 1;
1422
1656
  }
@@ -1440,7 +1674,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1440
1674
  continue;
1441
1675
  }
1442
1676
  any_ready = (rc > 0);
1443
- did_timeout = (rc == 0);
1677
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1444
1678
  if (any_ready && wait_fd >= 0) {
1445
1679
  if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
1446
1680
  if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
@@ -1454,7 +1688,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1454
1688
  #else
1455
1689
  rb_thread_wait_for(tv);
1456
1690
  #endif
1457
- did_timeout = 1;
1691
+ did_timeout = multi_socket_timer_due(ctx);
1458
1692
  }
1459
1693
 
1460
1694
  if (did_timeout) {
@@ -1489,7 +1723,7 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1489
1723
  rb_curl_multi_socket_drive(a->self, a->rbcm, a->ctx, a->block);
1490
1724
  return Qtrue;
1491
1725
  }
1492
- 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; };
1493
1727
  static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1494
1728
  struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1495
1729
  if (c->rbcm && c->rbcm->handle) {
@@ -1503,8 +1737,14 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1503
1737
  c->ctx->sock_map = NULL;
1504
1738
  }
1505
1739
  if (c->ctx) {
1740
+ if (!NIL_P(c->ctx->io_cache)) {
1741
+ rb_hash_clear(c->ctx->io_cache);
1742
+ }
1506
1743
  c->ctx->io_cache = Qnil;
1507
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
+ }
1508
1748
  return Qnil;
1509
1749
  }
1510
1750
 
@@ -1521,8 +1761,9 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1521
1761
 
1522
1762
  multi_socket_ctx ctx;
1523
1763
  ctx.sock_map = st_init_numtable();
1524
- ctx.timeout_ms = -1;
1764
+ ctx.timeout_deadline_ms = -1;
1525
1765
  ctx.io_cache = rb_hash_new();
1766
+ rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
1526
1767
 
1527
1768
  /* install socket/timer callbacks */
1528
1769
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -1532,7 +1773,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1532
1773
 
1533
1774
  /* run using socket action loop with ensure-cleanup */
1534
1775
  struct socket_drive_args body_args = { self, rbcm, &ctx, block };
1535
- struct socket_cleanup_args ensure_args = { rbcm, &ctx };
1776
+ struct socket_cleanup_args ensure_args = { self, rbcm, &ctx };
1536
1777
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1537
1778
 
1538
1779
  /* finalize */
@@ -1825,10 +2066,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1825
2066
  #endif /* disabled curl_multi_wait: use fdsets */
1826
2067
  }
1827
2068
 
2069
+ rb_curl_multi_read_info( self, rbcm->handle );
2070
+ rb_curl_multi_yield_if_given(self, block);
1828
2071
  } while( rbcm->running );
1829
2072
 
1830
- rb_curl_multi_read_info( self, rbcm->handle );
1831
- rb_curl_multi_yield_if_given(self, block);
1832
2073
  if (cCurlMutiAutoClose == 1) {
1833
2074
  rbcm->allow_close_during_perform = 1;
1834
2075
  rb_funcall(self, rb_intern("_autoclose"), 0);
@@ -1949,6 +2190,7 @@ void init_curb_multi() {
1949
2190
  idCall = rb_intern("call");
1950
2191
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
1951
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");
1952
2194
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
1953
2195
 
1954
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
@@ -66,7 +82,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
66
82
  SERIAL_TIME_WOULD_BE_ABOUT = MIN_S * ITERS
67
83
 
68
84
  def setup
69
- @port = 9993
85
+ @port = unused_local_port
70
86
 
71
87
  @response_proc = lambda do |res|
72
88
  res['Content-Type'] = 'text/plain'
@@ -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.3
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-11 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: