curb 1.3.6 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +71 -14
- data/Rakefile +12 -1
- data/ext/curb.c +26 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +178 -77
- data/ext/curb_easy.h +1 -0
- data/ext/curb_multi.c +639 -225
- data/ext/curb_multi.h +4 -0
- data/ext/curb_postfield.c +31 -31
- data/ext/extconf.rb +10 -0
- data/lib/curl/easy.rb +18 -9
- data/lib/curl.rb +49 -12
- data/tests/helper.rb +300 -4
- data/tests/io_select_less_scheduler_probe.rb +105 -0
- data/tests/tc_curl_easy.rb +60 -4
- data/tests/tc_curl_easy_cookielist.rb +8 -4
- data/tests/tc_curl_easy_resolve.rb +2 -0
- data/tests/tc_curl_multi.rb +29 -0
- data/tests/tc_curl_multi_poll.rb +223 -0
- data/tests/tc_curl_multi_socket_poller.rb +374 -0
- data/tests/tc_curl_native_coverage.rb +1 -0
- data/tests/tc_curl_postfield.rb +39 -0
- data/tests/tc_fiber_scheduler.rb +433 -6
- data/tests/tc_ftp_options.rb +58 -0
- data/tests/tc_gc_compact.rb +53 -0
- data/tests/tc_ractor.rb +94 -0
- data/tests/tc_test_server_methods.rb +15 -0
- metadata +10 -2
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -46,11 +46,12 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def kernel_sleep(duration = nil)
|
|
49
|
-
sleep
|
|
49
|
+
# A bare sleep here would re-enter this hook through the scheduler.
|
|
50
|
+
blocking_io { sleep(duration || 0) }
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def block(_blocker, timeout = nil)
|
|
53
|
-
sleep(timeout || 0)
|
|
54
|
+
blocking_io { sleep(timeout || 0) }
|
|
54
55
|
false
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -69,7 +70,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
69
70
|
if Fiber.respond_to?(:blocking)
|
|
70
71
|
Fiber.blocking(&block)
|
|
71
72
|
else
|
|
72
|
-
block.
|
|
73
|
+
Fiber.new(blocking: true, &block).resume
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
|
@@ -117,7 +118,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
117
118
|
|
|
118
119
|
duration = Time.now - started
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
assert_ran_concurrently(duration)
|
|
121
122
|
assert_equal ITERS, results.size
|
|
122
123
|
assert_equal ITERS.times.map {200}, results
|
|
123
124
|
end
|
|
@@ -148,7 +149,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
148
149
|
|
|
149
150
|
duration = Time.now - started
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
assert_ran_concurrently(duration)
|
|
152
153
|
assert_equal ITERS, results.size
|
|
153
154
|
assert_equal ITERS.times.map {200}, results
|
|
154
155
|
end
|
|
@@ -215,7 +216,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
215
216
|
c = Curl::Easy.new(url)
|
|
216
217
|
c.on_complete { result = c.code }
|
|
217
218
|
m.add(c)
|
|
218
|
-
unless
|
|
219
|
+
unless socket_perform_supported?
|
|
219
220
|
omit('socket-action perform path is not available in this build')
|
|
220
221
|
end
|
|
221
222
|
m.send(:_socket_perform)
|
|
@@ -231,6 +232,249 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
231
232
|
end
|
|
232
233
|
end
|
|
233
234
|
|
|
235
|
+
# Multi#perform must not fall back to the thread-blocking legacy fdset loop
|
|
236
|
+
# when the calling fiber runs under a scheduler: the waits have to go
|
|
237
|
+
# through the scheduler's io_wait/io_select hooks so sibling fibers run.
|
|
238
|
+
def test_multi_perform_waits_via_scheduler_hooks
|
|
239
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
240
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
241
|
+
|
|
242
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
243
|
+
scheduler = RecordingScheduler.new
|
|
244
|
+
result = nil
|
|
245
|
+
|
|
246
|
+
with_scheduler(scheduler) do
|
|
247
|
+
m = Curl::Multi.new
|
|
248
|
+
unless socket_perform_supported?
|
|
249
|
+
omit('socket-action perform path is not available in this build')
|
|
250
|
+
end
|
|
251
|
+
c = Curl::Easy.new(url)
|
|
252
|
+
c.on_complete { result = c.code }
|
|
253
|
+
m.add(c)
|
|
254
|
+
m.perform
|
|
255
|
+
ensure
|
|
256
|
+
m.close if m
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
assert_equal 200, result
|
|
260
|
+
assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1,
|
|
261
|
+
'Multi#perform under a fiber scheduler should wait through the scheduler hooks'
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_multi_perform_propagates_io_wrapper_errors
|
|
265
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
266
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
267
|
+
omit('socket-action perform path is not available in this build') unless socket_perform_supported?
|
|
268
|
+
|
|
269
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
270
|
+
scheduler = RecordingScheduler.new
|
|
271
|
+
wrapper_error = Class.new(StandardError)
|
|
272
|
+
original_for_fd = IO.method(:for_fd)
|
|
273
|
+
redefine_io_for_fd do |*_args|
|
|
274
|
+
raise wrapper_error, 'IO wrapper failed'
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
error = assert_raise(wrapper_error) do
|
|
278
|
+
with_scheduler(scheduler) do
|
|
279
|
+
m = Curl::Multi.new
|
|
280
|
+
m.add(Curl::Easy.new(url))
|
|
281
|
+
m.perform
|
|
282
|
+
ensure
|
|
283
|
+
m.close if m
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
assert_equal 'IO wrapper failed', error.message
|
|
288
|
+
ensure
|
|
289
|
+
redefine_io_for_fd(original_for_fd) if original_for_fd
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Probe-style regression tests: all previous concurrency assertions routed N
|
|
293
|
+
# requests through one multi handle, so libcurl provided the parallelism and
|
|
294
|
+
# they passed even when the wait loop blocked the whole thread. Here a
|
|
295
|
+
# sibling fiber ticks every 10ms while a single request is in flight; a
|
|
296
|
+
# blocking wait loop caps the ticks at roughly one per 100ms select chunk,
|
|
297
|
+
# while a scheduler-friendly loop lets most of the ~MIN_S/0.01 ticks happen.
|
|
298
|
+
def test_sibling_fibers_progress_during_multi_perform
|
|
299
|
+
return if skip_no_async
|
|
300
|
+
return if skip_no_socket_perform
|
|
301
|
+
|
|
302
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
303
|
+
result = nil
|
|
304
|
+
|
|
305
|
+
ticks = probe_sibling_ticks do
|
|
306
|
+
m = Curl::Multi.new
|
|
307
|
+
begin
|
|
308
|
+
c = Curl::Easy.new(url)
|
|
309
|
+
c.on_complete { result = c.code }
|
|
310
|
+
m.add(c)
|
|
311
|
+
m.perform
|
|
312
|
+
ensure
|
|
313
|
+
m.close
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
assert_equal 200, result
|
|
318
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
319
|
+
"sibling fiber starved during Multi#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_sibling_fibers_progress_during_easy_perform
|
|
323
|
+
return if skip_no_async
|
|
324
|
+
return if skip_no_socket_perform
|
|
325
|
+
|
|
326
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
327
|
+
result = nil
|
|
328
|
+
|
|
329
|
+
ticks = probe_sibling_ticks do
|
|
330
|
+
c = Curl::Easy.new(url)
|
|
331
|
+
c.perform
|
|
332
|
+
result = c.code
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
assert_equal 200, result
|
|
336
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
337
|
+
"sibling fiber starved during Easy#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_sibling_fibers_progress_during_multi_perform_with_multiple_sockets
|
|
341
|
+
return if skip_no_async
|
|
342
|
+
return if skip_no_socket_perform
|
|
343
|
+
|
|
344
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
345
|
+
results = []
|
|
346
|
+
|
|
347
|
+
ticks = probe_sibling_ticks do
|
|
348
|
+
m = Curl::Multi.new
|
|
349
|
+
begin
|
|
350
|
+
2.times do
|
|
351
|
+
c = Curl::Easy.new(url)
|
|
352
|
+
c.on_complete { results << c.code }
|
|
353
|
+
m.add(c)
|
|
354
|
+
end
|
|
355
|
+
m.perform
|
|
356
|
+
ensure
|
|
357
|
+
m.close
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
assert_equal [200, 200], results.sort
|
|
362
|
+
assert_operator ticks, :>=, SIBLING_TICKS_MIN,
|
|
363
|
+
"sibling fiber starved during multi-fd Multi#perform: #{ticks} ticks in a #{MIN_S}s transfer (~#{SIBLING_TICKS_POSSIBLE} possible)"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_scheduler_timeout_interrupts_single_socket_multi_perform
|
|
367
|
+
return if skip_no_async
|
|
368
|
+
return if skip_no_socket_perform
|
|
369
|
+
|
|
370
|
+
assert_scheduler_timeout_interrupts_multi_perform(1)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def test_scheduler_timeout_interrupts_multi_socket_multi_perform
|
|
374
|
+
return if skip_no_async
|
|
375
|
+
return if skip_no_socket_perform
|
|
376
|
+
|
|
377
|
+
assert_scheduler_timeout_interrupts_multi_perform(2)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def test_multi_perform_runs_work_added_from_final_idle_yield_under_scheduler
|
|
381
|
+
return if skip_no_async
|
|
382
|
+
return if skip_no_socket_perform
|
|
383
|
+
|
|
384
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
385
|
+
completions = []
|
|
386
|
+
empty_yields = 0
|
|
387
|
+
queued_added = false
|
|
388
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
389
|
+
Curl::Multi.autoclose = true
|
|
390
|
+
|
|
391
|
+
begin
|
|
392
|
+
async_run do
|
|
393
|
+
m = Curl::Multi.new
|
|
394
|
+
begin
|
|
395
|
+
first = Curl::Easy.new(url)
|
|
396
|
+
first.on_complete { completions << :first }
|
|
397
|
+
m.add(first)
|
|
398
|
+
|
|
399
|
+
m.perform do |performing_multi|
|
|
400
|
+
empty_yields += 1 if performing_multi.requests.empty?
|
|
401
|
+
if !queued_added && empty_yields >= 2
|
|
402
|
+
queued_added = true
|
|
403
|
+
queued = Curl::Easy.new(url)
|
|
404
|
+
queued.on_complete { completions << :queued }
|
|
405
|
+
performing_multi.add(queued)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
ensure
|
|
409
|
+
m.close
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
assert queued_added, 'test should add queued work from the final idle yield'
|
|
414
|
+
assert_equal [:first, :queued], completions
|
|
415
|
+
ensure
|
|
416
|
+
Curl::Multi.autoclose = previous_autoclose
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# A scheduler that implements io_wait but not the optional io_select hook
|
|
421
|
+
# must neither busy-loop nor fall back to a thread-blocking wait. The broken
|
|
422
|
+
# loop spins without an interrupt checkpoint, so the probe runs in a
|
|
423
|
+
# subprocess guarded by a parent-side SIGKILL fuse.
|
|
424
|
+
def test_socket_perform_progresses_when_scheduler_lacks_io_select
|
|
425
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
426
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
427
|
+
unless socket_perform_supported?
|
|
428
|
+
omit('socket-action perform path is not available in this build')
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
script = File.join(File.dirname(__FILE__), 'io_select_less_scheduler_probe.rb')
|
|
432
|
+
out_r, out_w = IO.pipe
|
|
433
|
+
pid = Process.spawn(RbConfig.ruby, '-I', $LIBDIR, '-I', $EXTDIR, script, @port.to_s,
|
|
434
|
+
:out => out_w, :err => out_w)
|
|
435
|
+
out_w.close
|
|
436
|
+
|
|
437
|
+
fuse_seconds = 30
|
|
438
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + fuse_seconds
|
|
439
|
+
status = nil
|
|
440
|
+
loop do
|
|
441
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
442
|
+
break if status
|
|
443
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
444
|
+
begin
|
|
445
|
+
Process.kill('KILL', pid)
|
|
446
|
+
rescue Errno::ESRCH
|
|
447
|
+
end
|
|
448
|
+
begin
|
|
449
|
+
Process.waitpid(pid)
|
|
450
|
+
rescue Errno::ECHILD
|
|
451
|
+
end
|
|
452
|
+
flunk "scheduler without io_select busy-looped _socket_perform (no exit within #{fuse_seconds}s; killed)"
|
|
453
|
+
end
|
|
454
|
+
sleep 0.05
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
output = out_r.read
|
|
458
|
+
assert status.success?, "io_select-less scheduler subprocess failed: #{output}"
|
|
459
|
+
match = /^OK ticks=(\d+)$/.match(output)
|
|
460
|
+
assert_not_nil match, "unexpected io_select-less scheduler output: #{output}"
|
|
461
|
+
assert_operator match[1].to_i, :>=, 2,
|
|
462
|
+
"scheduler without io_select did not make sibling-fiber progress: #{output}"
|
|
463
|
+
ensure
|
|
464
|
+
if pid && !status
|
|
465
|
+
begin
|
|
466
|
+
Process.kill('KILL', pid)
|
|
467
|
+
rescue Errno::ESRCH
|
|
468
|
+
end
|
|
469
|
+
begin
|
|
470
|
+
Process.waitpid(pid)
|
|
471
|
+
rescue Errno::ECHILD
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
out_r.close if out_r && !out_r.closed?
|
|
475
|
+
out_w.close if out_w && !out_w.closed?
|
|
476
|
+
end
|
|
477
|
+
|
|
234
478
|
def test_multi_reuse_after_scheduler_perform
|
|
235
479
|
unless HAS_ASYNC
|
|
236
480
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
@@ -490,6 +734,97 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
490
734
|
end
|
|
491
735
|
end
|
|
492
736
|
|
|
737
|
+
# Records unblock calls in place of a real scheduler.
|
|
738
|
+
class UnblockRecorder
|
|
739
|
+
attr_reader :unblocked
|
|
740
|
+
|
|
741
|
+
def initialize
|
|
742
|
+
@unblocked = []
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def unblock(blocker, _fiber)
|
|
746
|
+
@unblocked << blocker
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Enumerating every waiter on every drive-loop pass made release cost
|
|
751
|
+
# O(waiters) per pass; only the queued completions should be visited.
|
|
752
|
+
class NonEnumerableWaiters < Hash
|
|
753
|
+
%i[each each_pair each_value each_key].each do |name|
|
|
754
|
+
define_method(name) { |*| raise 'release_scheduler_waiters must not enumerate all waiters' }
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def scheduler_test_waiter(scheduler)
|
|
759
|
+
{completed: true, done: false, error: nil, fiber: Fiber.new {}, scheduler: scheduler}
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def test_release_scheduler_waiters_visits_only_queued_completions
|
|
763
|
+
state = Curl.scheduler_state
|
|
764
|
+
recorder = UnblockRecorder.new
|
|
765
|
+
waiters = NonEnumerableWaiters.new
|
|
766
|
+
1_000.times { |i| waiters[i] = scheduler_test_waiter(recorder).merge(completed: false) }
|
|
767
|
+
finished = scheduler_test_waiter(recorder)
|
|
768
|
+
waiters[:finished] = finished
|
|
769
|
+
state[:waiters] = waiters
|
|
770
|
+
state[:completed] << [:finished, finished]
|
|
771
|
+
|
|
772
|
+
Curl.release_scheduler_waiters(state)
|
|
773
|
+
|
|
774
|
+
assert_equal [finished], recorder.unblocked
|
|
775
|
+
assert finished[:done]
|
|
776
|
+
assert_empty state[:completed]
|
|
777
|
+
ensure
|
|
778
|
+
cleanup_scheduler_state
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# A waiter whose perform already returned (it raised) is unregistered and its
|
|
782
|
+
# fiber may be blocked on something else, so a late completion must not
|
|
783
|
+
# unblock it.
|
|
784
|
+
def test_release_scheduler_waiters_skips_unregistered_waiter
|
|
785
|
+
state = Curl.scheduler_state
|
|
786
|
+
recorder = UnblockRecorder.new
|
|
787
|
+
stale = scheduler_test_waiter(recorder)
|
|
788
|
+
state[:completed] << [:stale, stale]
|
|
789
|
+
|
|
790
|
+
Curl.release_scheduler_waiters(state)
|
|
791
|
+
|
|
792
|
+
assert_empty recorder.unblocked
|
|
793
|
+
assert_equal false, stale[:done]
|
|
794
|
+
assert_empty state[:completed]
|
|
795
|
+
ensure
|
|
796
|
+
cleanup_scheduler_state
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# The transfer whose callback raised the deferred exception receives that
|
|
800
|
+
# error rather than a normal completion; it stays queued so it is completed
|
|
801
|
+
# normally if the exception source changes.
|
|
802
|
+
def test_release_scheduler_waiters_holds_back_deferred_exception_source
|
|
803
|
+
state = Curl.scheduler_state
|
|
804
|
+
recorder = UnblockRecorder.new
|
|
805
|
+
source = scheduler_test_waiter(recorder)
|
|
806
|
+
sibling = scheduler_test_waiter(recorder)
|
|
807
|
+
state[:waiters][:source] = source
|
|
808
|
+
state[:waiters][:sibling] = sibling
|
|
809
|
+
state[:completed] << [:source, source] << [:sibling, sibling]
|
|
810
|
+
state[:multi].instance_variable_set(:@__curb_deferred_exception_source_id, :source)
|
|
811
|
+
|
|
812
|
+
Curl.release_scheduler_waiters(state)
|
|
813
|
+
|
|
814
|
+
assert_equal [sibling], recorder.unblocked
|
|
815
|
+
assert_equal false, source[:done]
|
|
816
|
+
assert_equal [[:source, source]], state[:completed]
|
|
817
|
+
|
|
818
|
+
state[:multi].remove_instance_variable(:@__curb_deferred_exception_source_id)
|
|
819
|
+
Curl.release_scheduler_waiters(state)
|
|
820
|
+
|
|
821
|
+
assert_equal [sibling, source], recorder.unblocked
|
|
822
|
+
assert source[:done]
|
|
823
|
+
assert_empty state[:completed]
|
|
824
|
+
ensure
|
|
825
|
+
cleanup_scheduler_state
|
|
826
|
+
end
|
|
827
|
+
|
|
493
828
|
def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
|
|
494
829
|
state = Curl.scheduler_state
|
|
495
830
|
easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
@@ -508,7 +843,79 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
508
843
|
cleanup_scheduler_state
|
|
509
844
|
end
|
|
510
845
|
|
|
846
|
+
# Sibling-fiber probe bounds: ~MIN_S/0.01 ticks are possible while the
|
|
847
|
+
# request is in flight. A thread-blocking wait loop yields only one tick per
|
|
848
|
+
# 100ms select chunk (2-3 for MIN_S=0.25); require a comfortably higher
|
|
849
|
+
# count while leaving slack for slow CI hosts.
|
|
850
|
+
SIBLING_TICKS_POSSIBLE = (MIN_S / 0.01).to_i
|
|
851
|
+
SIBLING_TICKS_MIN = 8
|
|
852
|
+
|
|
511
853
|
private
|
|
854
|
+
def redefine_io_for_fd(callable = nil, &block)
|
|
855
|
+
previous_verbose = $VERBOSE
|
|
856
|
+
$VERBOSE = nil
|
|
857
|
+
if callable
|
|
858
|
+
IO.define_singleton_method(:for_fd, callable)
|
|
859
|
+
else
|
|
860
|
+
IO.define_singleton_method(:for_fd, &block)
|
|
861
|
+
end
|
|
862
|
+
ensure
|
|
863
|
+
$VERBOSE = previous_verbose
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def assert_scheduler_timeout_interrupts_multi_perform(easy_count)
|
|
867
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
868
|
+
timeout_error = nil
|
|
869
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
870
|
+
|
|
871
|
+
async_run do |task|
|
|
872
|
+
begin
|
|
873
|
+
task.with_timeout(0.05) do
|
|
874
|
+
m = Curl::Multi.new
|
|
875
|
+
begin
|
|
876
|
+
easy_count.times { m.add(Curl::Easy.new(url)) }
|
|
877
|
+
m.perform
|
|
878
|
+
ensure
|
|
879
|
+
m.close
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
rescue Async::TimeoutError => error
|
|
883
|
+
timeout_error = error
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
888
|
+
assert_kind_of Async::TimeoutError, timeout_error,
|
|
889
|
+
"scheduler timeout was swallowed for #{easy_count} easy handle(s)"
|
|
890
|
+
assert_operator elapsed, :<, MIN_S * 0.8,
|
|
891
|
+
"scheduler cancellation took #{elapsed}s for #{easy_count} easy handle(s)"
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Runs the block in one fiber while a sibling fiber ticks every 10ms;
|
|
895
|
+
# returns how many ticks the sibling managed before the block finished.
|
|
896
|
+
def probe_sibling_ticks
|
|
897
|
+
ticks = 0
|
|
898
|
+
done = false
|
|
899
|
+
|
|
900
|
+
async_run do |top|
|
|
901
|
+
ticker = top.async do
|
|
902
|
+
until done
|
|
903
|
+
sleep 0.01
|
|
904
|
+
ticks += 1 unless done
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
top.async do
|
|
909
|
+
yield
|
|
910
|
+
ensure
|
|
911
|
+
done = true
|
|
912
|
+
end.wait
|
|
913
|
+
ticker.wait
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
ticks
|
|
917
|
+
end
|
|
918
|
+
|
|
512
919
|
def skip_no_async
|
|
513
920
|
if WINDOWS
|
|
514
921
|
warn 'Skipping fiber scheduler tests on Windows'
|
|
@@ -525,6 +932,26 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
525
932
|
false
|
|
526
933
|
end
|
|
527
934
|
|
|
935
|
+
def skip_no_socket_perform
|
|
936
|
+
unless socket_perform_supported?
|
|
937
|
+
warn 'Skipping fiber scheduler test (socket-action perform path unavailable)'
|
|
938
|
+
return true
|
|
939
|
+
end
|
|
940
|
+
false
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# Wall-clock bounds are not reliable under valgrind, which slows and
|
|
944
|
+
# serializes threads unpredictably; there the requests and their results are
|
|
945
|
+
# still checked under memcheck, only the duration bound is skipped.
|
|
946
|
+
def assert_ran_concurrently(duration)
|
|
947
|
+
return if curb_under_valgrind?
|
|
948
|
+
assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def socket_perform_supported?
|
|
952
|
+
Curl::Multi.private_method_defined?(:_socket_perform)
|
|
953
|
+
end
|
|
954
|
+
|
|
528
955
|
def async_run(&block)
|
|
529
956
|
# Prefer newer Async.run to avoid deprecated scheduler.async path.
|
|
530
957
|
if defined?(Async) && Async.respond_to?(:run)
|
data/tests/tc_ftp_options.rb
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
2
|
|
|
3
3
|
class TestCurbFtpOptions < Test::Unit::TestCase
|
|
4
|
+
SCALAR_AND_STRING_OPTIONS = {
|
|
5
|
+
:ftpport => "-",
|
|
6
|
+
:dirlistonly => true,
|
|
7
|
+
:append => true,
|
|
8
|
+
:ftp_use_eprt => false,
|
|
9
|
+
:ftp_use_epsv => false,
|
|
10
|
+
:ftp_use_pret => true,
|
|
11
|
+
:ftp_create_missing_dirs => true,
|
|
12
|
+
:ftp_response_timeout => 30,
|
|
13
|
+
:ftp_alternative_to_user => "anonymous",
|
|
14
|
+
:ftp_skip_pasv_ip => true,
|
|
15
|
+
:ftpsslauth => 0,
|
|
16
|
+
:ftp_ssl_ccc => 0,
|
|
17
|
+
:ftp_account => "account",
|
|
18
|
+
:ftp_filemethod => 0
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
4
21
|
# Ensure FTP-related set(:option, ...) mappings are accepted and do not raise
|
|
5
22
|
# a TypeError (they used to be unsupported in setopt dispatch).
|
|
6
23
|
def test_can_set_ftp_listing_related_flags
|
|
@@ -15,6 +32,47 @@ class TestCurbFtpOptions < Test::Unit::TestCase
|
|
|
15
32
|
end
|
|
16
33
|
end
|
|
17
34
|
|
|
35
|
+
def test_can_set_detected_scalar_and_string_ftp_options
|
|
36
|
+
c = Curl::Easy.new('ftp://example.com/')
|
|
37
|
+
|
|
38
|
+
SCALAR_AND_STRING_OPTIONS.each do |name, value|
|
|
39
|
+
constant = "CURLOPT_#{name.to_s.upcase}"
|
|
40
|
+
next unless Curl.const_defined?(constant)
|
|
41
|
+
|
|
42
|
+
assert_nothing_raised("setting #{name}") { c.set(name, value) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_issue_481_ftp_create_missing_dirs_accessor_and_set
|
|
47
|
+
omit('CURLOPT_FTP_CREATE_MISSING_DIRS is not available in this build') unless
|
|
48
|
+
Curl.const_defined?(:CURLOPT_FTP_CREATE_MISSING_DIRS)
|
|
49
|
+
|
|
50
|
+
c = Curl::Easy.new('ftp://example.com/')
|
|
51
|
+
|
|
52
|
+
assert_respond_to(c, :ftp_create_missing_dirs=)
|
|
53
|
+
assert_respond_to(c, :ftp_create_missing_dirs)
|
|
54
|
+
assert_nil(c.ftp_create_missing_dirs)
|
|
55
|
+
|
|
56
|
+
c.ftp_create_missing_dirs = true
|
|
57
|
+
assert_equal(true, c.ftp_create_missing_dirs)
|
|
58
|
+
|
|
59
|
+
assert_equal(1, c.set(:ftp_create_missing_dirs, 1))
|
|
60
|
+
assert_equal(1, c.ftp_create_missing_dirs)
|
|
61
|
+
|
|
62
|
+
retry_mode = if Curl.const_defined?(:CURLFTP_CREATE_DIR_RETRY)
|
|
63
|
+
Curl::CURLFTP_CREATE_DIR_RETRY
|
|
64
|
+
else
|
|
65
|
+
2
|
|
66
|
+
end
|
|
67
|
+
c.set(:ftp_create_missing_dirs, retry_mode)
|
|
68
|
+
assert_equal(retry_mode, c.ftp_create_missing_dirs)
|
|
69
|
+
assert_equal(retry_mode, c.clone.ftp_create_missing_dirs)
|
|
70
|
+
|
|
71
|
+
settings = c.reset
|
|
72
|
+
assert_equal(retry_mode, settings[:ftp_create_missing_dirs])
|
|
73
|
+
assert_nil(c.ftp_create_missing_dirs)
|
|
74
|
+
end
|
|
75
|
+
|
|
18
76
|
# Setting ftp_commands remains supported for control-connection commands.
|
|
19
77
|
def test_can_assign_ftp_commands
|
|
20
78
|
c = Curl::Easy.new('ftp://example.com/')
|
data/tests/tc_gc_compact.rb
CHANGED
|
@@ -96,6 +96,59 @@ class TestGcCompact < Test::Unit::TestCase
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Regression: after compaction relocates a persistent Curl::Easy and its old
|
|
100
|
+
# heap slot is recycled by another T_DATA object, completion dispatch (which
|
|
101
|
+
# reads the rbce->self back-reference via CURLINFO_PRIVATE) must still
|
|
102
|
+
# resolve to the correct Curl::Easy. Without pinning in curl_easy_mark this
|
|
103
|
+
# raises "TypeError: wrong argument type Curl::Upload (expected Curl::Easy)"
|
|
104
|
+
# on the first round, or defers it onto the multi and raises it from a later
|
|
105
|
+
# request.
|
|
106
|
+
def test_completion_dispatch_survives_compact_and_slot_reuse
|
|
107
|
+
omit('needs GC.verify_compaction_references') unless GC.respond_to?(:verify_compaction_references)
|
|
108
|
+
|
|
109
|
+
# Heap-only reference: holding the easy in a local would pin it to the
|
|
110
|
+
# stack and mask the bug.
|
|
111
|
+
holder = { easy: Curl::Easy.new($TEST_URL) }
|
|
112
|
+
holder[:easy].timeout = 5
|
|
113
|
+
completed = nil
|
|
114
|
+
holder[:easy].on_complete { |e| completed = e }
|
|
115
|
+
holder[:easy].http(:GET)
|
|
116
|
+
assert_same holder[:easy], completed
|
|
117
|
+
expected_body = holder[:easy].body_str
|
|
118
|
+
refute_empty expected_body
|
|
119
|
+
|
|
120
|
+
ruby_version = RUBY_VERSION.split('.').first(2).map(&:to_i)
|
|
121
|
+
heap_expansion = if (ruby_version <=> [3, 2]) >= 0
|
|
122
|
+
{ expand_heap: true }
|
|
123
|
+
else
|
|
124
|
+
{ double_heap: true }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
3.times do
|
|
128
|
+
# Relocate every movable object, then recycle freed slots with other
|
|
129
|
+
# T_DATA instances so a stale back-reference cannot fail soft.
|
|
130
|
+
GC.verify_compaction_references(toward: :empty, **heap_expansion)
|
|
131
|
+
churn = Array.new(50_000) { Curl::Upload.new }
|
|
132
|
+
|
|
133
|
+
completed = nil
|
|
134
|
+
holder[:easy].http(:GET)
|
|
135
|
+
|
|
136
|
+
multi = holder[:easy].multi
|
|
137
|
+
if multi&.instance_variable_defined?(:@__curb_deferred_exception)
|
|
138
|
+
raise multi.instance_variable_get(:@__curb_deferred_exception)
|
|
139
|
+
end
|
|
140
|
+
assert_same holder[:easy], completed
|
|
141
|
+
assert_equal expected_body, holder[:easy].body_str
|
|
142
|
+
churn.clear
|
|
143
|
+
end
|
|
144
|
+
ensure
|
|
145
|
+
begin
|
|
146
|
+
holder[:easy]&.close
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
99
152
|
private
|
|
100
153
|
|
|
101
154
|
def run_multi_perform_compact_iteration
|