curb 1.3.6 → 1.3.7
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 +33 -13
- data/ext/curb.c +9 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +107 -46
- data/ext/curb_multi.c +211 -130
- data/ext/extconf.rb +4 -0
- data/tests/helper.rb +178 -1
- data/tests/io_select_less_scheduler_probe.rb +105 -0
- data/tests/tc_curl_easy.rb +1 -0
- data/tests/tc_curl_easy_resolve.rb +2 -0
- data/tests/tc_curl_native_coverage.rb +1 -0
- data/tests/tc_fiber_scheduler.rb +332 -4
- data/tests/tc_ftp_options.rb +58 -0
- data/tests/tc_gc_compact.rb +53 -0
- data/tests/tc_test_server_methods.rb +15 -0
- metadata +4 -2
data/tests/helper.rb
CHANGED
|
@@ -238,6 +238,85 @@ module BugTestServerSetupTeardown
|
|
|
238
238
|
end
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
+
# Test::Unit creates a new test instance for every test method. Keep the
|
|
242
|
+
# common WEBrick fixture process-scoped so those instances can share it while
|
|
243
|
+
# still allowing an explicit stop_test_server call to exercise lifecycle code.
|
|
244
|
+
module CurbTestServerRegistry
|
|
245
|
+
Entry = Struct.new(:key, :port, :server, :thread, :lock_file, :pid)
|
|
246
|
+
|
|
247
|
+
@mutex = Mutex.new
|
|
248
|
+
@entries = {}
|
|
249
|
+
|
|
250
|
+
class << self
|
|
251
|
+
def acquire(port, servlet)
|
|
252
|
+
key = [port, servlet]
|
|
253
|
+
|
|
254
|
+
@mutex.synchronize do
|
|
255
|
+
entry = @entries[key]
|
|
256
|
+
return entry if active?(entry)
|
|
257
|
+
|
|
258
|
+
@entries.delete(key)
|
|
259
|
+
entry = yield
|
|
260
|
+
@entries[key] = entry if entry
|
|
261
|
+
entry
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def stop(entry, timeout)
|
|
266
|
+
return false unless remove(entry)
|
|
267
|
+
|
|
268
|
+
shutdown_entry(entry, timeout)
|
|
269
|
+
true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def register_shutdown_hook(entry)
|
|
273
|
+
# Test::Unit can run a directly executed test file from its own at_exit
|
|
274
|
+
# callback. Register after startup so this hook runs after that callback.
|
|
275
|
+
at_exit { stop(entry, 5) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def active?(entry)
|
|
281
|
+
entry && entry.server && entry.thread && entry.thread.alive? && entry.server.status == :Running
|
|
282
|
+
rescue StandardError
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def remove(entry)
|
|
287
|
+
@mutex.synchronize do
|
|
288
|
+
return false unless @entries[entry.key].equal?(entry)
|
|
289
|
+
|
|
290
|
+
@entries.delete(entry.key)
|
|
291
|
+
true
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def shutdown_entry(entry, timeout)
|
|
296
|
+
return unless Process.pid == entry.pid
|
|
297
|
+
|
|
298
|
+
begin
|
|
299
|
+
entry.server.shutdown if entry.server
|
|
300
|
+
rescue StandardError
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
thread = entry.thread
|
|
305
|
+
if thread
|
|
306
|
+
thread.join(timeout)
|
|
307
|
+
if thread.alive?
|
|
308
|
+
thread.kill
|
|
309
|
+
thread.join(timeout)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
File.unlink(entry.lock_file) if entry.lock_file && File.exist?(entry.lock_file)
|
|
314
|
+
rescue Errno::ENOENT
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
241
320
|
module TestServerMethods
|
|
242
321
|
def server_responding?(port)
|
|
243
322
|
socket = TCPSocket.new('127.0.0.1', port)
|
|
@@ -267,6 +346,22 @@ module TestServerMethods
|
|
|
267
346
|
end
|
|
268
347
|
end
|
|
269
348
|
|
|
349
|
+
# Waiting on WEBrick's state avoids opening a probe connection for every
|
|
350
|
+
# startup. On Windows those short-lived connections make server teardown
|
|
351
|
+
# disproportionately expensive.
|
|
352
|
+
def wait_for_server_started(server, port, thread: nil)
|
|
353
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_startup_timeout
|
|
354
|
+
|
|
355
|
+
loop do
|
|
356
|
+
return true if server.status == :Running
|
|
357
|
+
return false if thread && !thread.alive?
|
|
358
|
+
|
|
359
|
+
raise "Failed to startup test server on port #{port}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
360
|
+
|
|
361
|
+
sleep 0.01
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
270
365
|
def server_shutdown_timeout
|
|
271
366
|
[server_startup_timeout, 0.25].max
|
|
272
367
|
end
|
|
@@ -337,6 +432,13 @@ module TestServerMethods
|
|
|
337
432
|
end
|
|
338
433
|
|
|
339
434
|
def stop_test_server
|
|
435
|
+
shared_entry = instance_variable_defined?(:@__shared_test_server_entry) ? @__shared_test_server_entry : nil
|
|
436
|
+
if shared_entry
|
|
437
|
+
CurbTestServerRegistry.stop(shared_entry, server_shutdown_timeout)
|
|
438
|
+
release_shared_test_server
|
|
439
|
+
return
|
|
440
|
+
end
|
|
441
|
+
|
|
340
442
|
server = instance_variable_defined?(:@server) ? @server : nil
|
|
341
443
|
pid = instance_variable_defined?(:@__pid) ? @__pid : nil
|
|
342
444
|
return unless server || pid
|
|
@@ -382,11 +484,28 @@ module TestServerMethods
|
|
|
382
484
|
def teardown
|
|
383
485
|
super
|
|
384
486
|
ensure
|
|
385
|
-
|
|
487
|
+
if instance_variable_defined?(:@__shared_test_server_entry) && @__shared_test_server_entry
|
|
488
|
+
release_shared_test_server
|
|
489
|
+
else
|
|
490
|
+
stop_test_server
|
|
491
|
+
end
|
|
386
492
|
end
|
|
387
493
|
|
|
388
494
|
def server_setup(port=9129,servlet=TestServlet)
|
|
389
495
|
@__port = port
|
|
496
|
+
|
|
497
|
+
unless TEST_SINGLE_THREADED
|
|
498
|
+
entry = CurbTestServerRegistry.acquire(port, servlet) do
|
|
499
|
+
start_shared_test_server(port, servlet)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if entry
|
|
503
|
+
attach_shared_test_server(entry)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
return
|
|
507
|
+
end
|
|
508
|
+
|
|
390
509
|
@server = nil unless instance_variable_defined?(:@server)
|
|
391
510
|
@__pid = nil unless instance_variable_defined?(:@__pid)
|
|
392
511
|
@test_thread = nil unless instance_variable_defined?(:@test_thread)
|
|
@@ -474,6 +593,64 @@ module TestServerMethods
|
|
|
474
593
|
end
|
|
475
594
|
rescue Errno::EADDRINUSE
|
|
476
595
|
end
|
|
596
|
+
|
|
597
|
+
def attach_shared_test_server(entry)
|
|
598
|
+
@server = entry.server
|
|
599
|
+
@test_thread = entry.thread
|
|
600
|
+
@__pid = nil
|
|
601
|
+
@__shared_test_server_entry = entry
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def release_shared_test_server
|
|
605
|
+
@server = nil
|
|
606
|
+
@test_thread = nil
|
|
607
|
+
@__pid = nil
|
|
608
|
+
@__shared_test_server_entry = nil
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def start_shared_test_server(port, servlet)
|
|
612
|
+
clear_stale_server_lock(port)
|
|
613
|
+
|
|
614
|
+
if File.exist?(locked_file)
|
|
615
|
+
begin
|
|
616
|
+
wait_for_server_ready(port)
|
|
617
|
+
return nil
|
|
618
|
+
rescue RuntimeError
|
|
619
|
+
clear_stale_server_lock(port)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
write_server_lock
|
|
624
|
+
server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
625
|
+
|
|
626
|
+
server.mount(servlet.path, servlet)
|
|
627
|
+
server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
|
|
628
|
+
|
|
629
|
+
thread = Thread.new(server) { |srv| srv.start }
|
|
630
|
+
unless wait_for_server_started(server, port, thread: thread)
|
|
631
|
+
server.shutdown
|
|
632
|
+
thread.join(server_shutdown_timeout)
|
|
633
|
+
raise "Failed to startup test server on port #{port}"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
entry = CurbTestServerRegistry::Entry.new([port, servlet], port, server, thread, locked_file, Process.pid)
|
|
637
|
+
CurbTestServerRegistry.register_shutdown_hook(entry)
|
|
638
|
+
entry
|
|
639
|
+
rescue Errno::EADDRINUSE
|
|
640
|
+
File.unlink(locked_file) if File.exist?(locked_file)
|
|
641
|
+
return nil if server_responding?(port)
|
|
642
|
+
|
|
643
|
+
raise
|
|
644
|
+
rescue StandardError
|
|
645
|
+
begin
|
|
646
|
+
server.shutdown if defined?(server) && server
|
|
647
|
+
thread.join(server_shutdown_timeout) if defined?(thread) && thread
|
|
648
|
+
rescue StandardError
|
|
649
|
+
nil
|
|
650
|
+
end
|
|
651
|
+
File.unlink(locked_file) if File.exist?(locked_file)
|
|
652
|
+
raise
|
|
653
|
+
end
|
|
477
654
|
end
|
|
478
655
|
|
|
479
656
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Reproduces the FIBER_TODO.md [Critical] finding: a Fiber scheduler that
|
|
2
|
+
# implements io_wait/kernel_sleep/block/unblock but NOT the optional io_select
|
|
3
|
+
# hook. rb_fiber_scheduler_io_select returns Qundef (without waiting) for such
|
|
4
|
+
# schedulers; if the multi-fd wait path of Curl::Multi#_socket_perform treats
|
|
5
|
+
# that as a completed wait, the drive loop spins at 100% CPU with no interrupt
|
|
6
|
+
# checkpoint and this script never exits — it cannot even service SIGTERM.
|
|
7
|
+
#
|
|
8
|
+
# Run by tc_fiber_scheduler.rb in a subprocess guarded by a SIGKILL fuse.
|
|
9
|
+
# Usage: ruby io_select_less_scheduler_probe.rb <port>
|
|
10
|
+
# <port> must serve HTTP GET /test (see BugTestServerSetupTeardown).
|
|
11
|
+
|
|
12
|
+
require 'fiber'
|
|
13
|
+
require 'curb'
|
|
14
|
+
|
|
15
|
+
port = Integer(ARGV[0])
|
|
16
|
+
url = "http://127.0.0.1:#{port}/test"
|
|
17
|
+
|
|
18
|
+
class NoIoSelectScheduler
|
|
19
|
+
def fiber(&block)
|
|
20
|
+
Fiber.new(blocking: false, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def io_wait(io, events, timeout = nil)
|
|
24
|
+
readers = (events & IO::READABLE) != 0 ? [io] : nil
|
|
25
|
+
writers = (events & IO::WRITABLE) != 0 ? [io] : nil
|
|
26
|
+
deadline = monotonic_now + timeout if timeout
|
|
27
|
+
|
|
28
|
+
loop do
|
|
29
|
+
readable, writable = IO.select(readers, writers, nil, 0)
|
|
30
|
+
ready = 0
|
|
31
|
+
ready |= IO::READABLE if readable && !readable.empty?
|
|
32
|
+
ready |= IO::WRITABLE if writable && !writable.empty?
|
|
33
|
+
return ready unless ready.zero?
|
|
34
|
+
return false if deadline && monotonic_now >= deadline
|
|
35
|
+
|
|
36
|
+
Fiber.yield
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def kernel_sleep(duration = nil)
|
|
41
|
+
deadline = monotonic_now + (duration || 0)
|
|
42
|
+
Fiber.yield while monotonic_now < deadline
|
|
43
|
+
duration
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def block(_blocker, timeout = nil)
|
|
47
|
+
kernel_sleep(timeout)
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unblock(*); end
|
|
52
|
+
|
|
53
|
+
def close; end
|
|
54
|
+
|
|
55
|
+
def fiber_interrupt(*); end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def monotonic_now
|
|
60
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
results = []
|
|
65
|
+
ticks = 0
|
|
66
|
+
done = false
|
|
67
|
+
|
|
68
|
+
Fiber.set_scheduler(NoIoSelectScheduler.new)
|
|
69
|
+
fiber = Fiber.schedule do
|
|
70
|
+
multi = Curl::Multi.new
|
|
71
|
+
begin
|
|
72
|
+
# Two easies so the drive loop tracks more than one socket and takes the
|
|
73
|
+
# multi-fd (io_select) wait branch.
|
|
74
|
+
2.times do
|
|
75
|
+
easy = Curl::Easy.new(url)
|
|
76
|
+
easy.on_complete { results << easy.response_code }
|
|
77
|
+
multi.add(easy)
|
|
78
|
+
end
|
|
79
|
+
multi.send(:_socket_perform)
|
|
80
|
+
ensure
|
|
81
|
+
multi.close
|
|
82
|
+
done = true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
ticker = Fiber.schedule do
|
|
86
|
+
until done
|
|
87
|
+
sleep 0.005
|
|
88
|
+
ticks += 1 unless done
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
while fiber.alive? || ticker.alive?
|
|
93
|
+
fiber.resume if fiber.alive?
|
|
94
|
+
ticker.resume if ticker.alive?
|
|
95
|
+
sleep 0.001
|
|
96
|
+
end
|
|
97
|
+
Fiber.set_scheduler(nil)
|
|
98
|
+
|
|
99
|
+
if results == [200, 200]
|
|
100
|
+
puts "OK ticks=#{ticks}"
|
|
101
|
+
exit 0
|
|
102
|
+
else
|
|
103
|
+
puts "FAILED results=#{results.inspect}"
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -22,6 +22,7 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
|
|
|
22
22
|
mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
|
|
23
23
|
|
|
24
24
|
@easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
|
|
25
|
+
@easy.proxy_url = ""
|
|
25
26
|
@easy.dns_cache_timeout = 0
|
|
26
27
|
@easy.set(:resolve, [mapping])
|
|
27
28
|
|
|
@@ -39,6 +40,7 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
|
|
|
39
40
|
entry.define_singleton_method(:to_s) { mapping }
|
|
40
41
|
|
|
41
42
|
@easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
|
|
43
|
+
@easy.proxy_url = ""
|
|
42
44
|
@easy.dns_cache_timeout = 0
|
|
43
45
|
@easy.resolve = [entry]
|
|
44
46
|
|
|
@@ -70,6 +70,7 @@ class TestCurbCurlNativeCoverage < Test::Unit::TestCase
|
|
|
70
70
|
|
|
71
71
|
def test_clone_preserves_native_lists_after_original_handle_closes
|
|
72
72
|
easy = Curl::Easy.new("http://curb.invalid:#{TestServlet.port}#{TestServlet.path}")
|
|
73
|
+
easy.proxy_url = ""
|
|
73
74
|
easy.headers['X-Test'] = '1'
|
|
74
75
|
easy.proxy_headers['X-Proxy'] = '2'
|
|
75
76
|
easy.ftp_commands = ['PWD']
|