curb 1.2.2 → 1.3.2
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/Rakefile +22 -0
- data/ext/curb.c +282 -231
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +766 -299
- data/ext/curb_easy.h +5 -0
- data/ext/curb_errors.c +5 -5
- data/ext/curb_errors.h +1 -1
- data/ext/curb_macros.h +14 -14
- data/ext/curb_multi.c +612 -142
- data/ext/curb_multi.h +3 -1
- data/ext/curb_postfield.c +48 -21
- data/ext/curb_postfield.h +1 -0
- data/ext/curb_upload.c +32 -9
- data/ext/curb_upload.h +2 -0
- data/ext/extconf.rb +42 -1
- data/lib/curl/easy.rb +154 -13
- data/lib/curl/multi.rb +69 -9
- data/lib/curl.rb +193 -0
- data/tests/helper.rb +222 -36
- data/tests/leak_trace.rb +237 -0
- data/tests/tc_curl_download.rb +6 -2
- data/tests/tc_curl_easy.rb +509 -1
- data/tests/tc_curl_multi.rb +573 -59
- data/tests/tc_curl_native_coverage.rb +145 -0
- data/tests/tc_curl_postfield.rb +176 -0
- data/tests/tc_fiber_scheduler.rb +342 -7
- data/tests/tc_gc_compact.rb +178 -16
- data/tests/tc_test_server_methods.rb +110 -0
- metadata +10 -14
- data/tests/test_basic.rb +0 -29
- data/tests/test_fiber_debug.rb +0 -69
- data/tests/test_fiber_simple.rb +0 -65
- data/tests/test_real_url.rb +0 -65
- data/tests/test_simple_fiber.rb +0 -34
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -12,6 +12,51 @@ end
|
|
|
12
12
|
# by running multiple requests concurrently in a single thread using the Async gem.
|
|
13
13
|
class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
14
14
|
include BugTestServerSetupTeardown
|
|
15
|
+
include TestServerMethods
|
|
16
|
+
|
|
17
|
+
class RecordingScheduler
|
|
18
|
+
attr_reader :io_wait_events
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@io_wait_events = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fiber(&block)
|
|
25
|
+
Fiber.new(blocking: false, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def io_wait(io, events, timeout = nil)
|
|
29
|
+
@io_wait_events << events
|
|
30
|
+
raise TypeError, "expected Integer events, got #{events.class}" unless events.is_a?(Integer)
|
|
31
|
+
|
|
32
|
+
readers = (events & IO::READABLE) != 0 ? [io] : nil
|
|
33
|
+
writers = (events & IO::WRITABLE) != 0 ? [io] : nil
|
|
34
|
+
readable, writable = IO.select(readers, writers, nil, timeout)
|
|
35
|
+
|
|
36
|
+
ready = 0
|
|
37
|
+
ready |= IO::READABLE if readable && !readable.empty?
|
|
38
|
+
ready |= IO::WRITABLE if writable && !writable.empty?
|
|
39
|
+
ready.zero? ? false : ready
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def kernel_sleep(duration = nil)
|
|
43
|
+
sleep(duration || 0)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def block(_blocker, timeout = nil)
|
|
47
|
+
sleep(timeout || 0)
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unblock(*)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fiber_interrupt(*)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
15
60
|
|
|
16
61
|
ITERS = 4
|
|
17
62
|
MIN_S = 0.25
|
|
@@ -50,6 +95,8 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
50
95
|
m.add(c)
|
|
51
96
|
end
|
|
52
97
|
m.perform
|
|
98
|
+
ensure
|
|
99
|
+
m.close if m
|
|
53
100
|
end
|
|
54
101
|
|
|
55
102
|
duration = Time.now - started
|
|
@@ -109,6 +156,8 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
109
156
|
m.perform do
|
|
110
157
|
yielded += 1
|
|
111
158
|
end
|
|
159
|
+
ensure
|
|
160
|
+
m.close if m
|
|
112
161
|
end
|
|
113
162
|
|
|
114
163
|
assert_operator yielded, :>=, 1, 'perform did not yield block while waiting under scheduler'
|
|
@@ -130,20 +179,49 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
130
179
|
c.on_complete { result = c.code }
|
|
131
180
|
m.add(c)
|
|
132
181
|
m.perform
|
|
182
|
+
ensure
|
|
183
|
+
m.close if m
|
|
133
184
|
end
|
|
134
185
|
|
|
135
186
|
assert_equal 200, result
|
|
136
187
|
end
|
|
137
188
|
|
|
189
|
+
def test_multi_single_request_socket_perform_passes_integer_events_to_io_wait
|
|
190
|
+
omit('Skipping custom scheduler test on Windows') if WINDOWS
|
|
191
|
+
omit('Fiber scheduler API unavailable on this Ruby') unless fiber_scheduler_supported?
|
|
192
|
+
|
|
193
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
194
|
+
scheduler = RecordingScheduler.new
|
|
195
|
+
result = nil
|
|
196
|
+
|
|
197
|
+
with_scheduler(scheduler) do
|
|
198
|
+
m = Curl::Multi.new
|
|
199
|
+
c = Curl::Easy.new(url)
|
|
200
|
+
c.on_complete { result = c.code }
|
|
201
|
+
m.add(c)
|
|
202
|
+
unless m.respond_to?(:_socket_perform, true)
|
|
203
|
+
omit('socket-action perform path is not available in this build')
|
|
204
|
+
end
|
|
205
|
+
m.send(:_socket_perform)
|
|
206
|
+
ensure
|
|
207
|
+
m.close if m
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
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 }
|
|
214
|
+
end
|
|
215
|
+
|
|
138
216
|
def test_multi_reuse_after_scheduler_perform
|
|
139
217
|
unless HAS_ASYNC
|
|
140
218
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
141
219
|
return
|
|
142
220
|
end
|
|
143
|
-
|
|
221
|
+
|
|
144
222
|
url = "http://127.0.0.1:#{@port}/test"
|
|
145
223
|
results = []
|
|
146
|
-
|
|
224
|
+
|
|
147
225
|
async_run do
|
|
148
226
|
m = Curl::Multi.new
|
|
149
227
|
# First round
|
|
@@ -151,15 +229,224 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
151
229
|
c1.on_complete { results << c1.code }
|
|
152
230
|
m.add(c1)
|
|
153
231
|
m.perform
|
|
154
|
-
|
|
232
|
+
|
|
155
233
|
# Second round on same multi
|
|
156
234
|
c2 = Curl::Easy.new(url)
|
|
157
235
|
c2.on_complete { results << c2.code }
|
|
158
236
|
m.add(c2)
|
|
159
237
|
m.perform
|
|
238
|
+
ensure
|
|
239
|
+
m.close if m
|
|
160
240
|
end
|
|
241
|
+
|
|
242
|
+
assert_equal [200, 200], results
|
|
243
|
+
end
|
|
161
244
|
|
|
245
|
+
def test_easy_perform_reuses_scheduler_multi_when_autoclose_is_enabled
|
|
246
|
+
if skip_no_async
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
251
|
+
results = []
|
|
252
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
253
|
+
Curl::Multi.autoclose = true
|
|
254
|
+
|
|
255
|
+
async_run do
|
|
256
|
+
2.times do
|
|
257
|
+
easy = Curl::Easy.new(url)
|
|
258
|
+
easy.perform
|
|
259
|
+
results << easy.code
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
162
263
|
assert_equal [200, 200], results
|
|
264
|
+
ensure
|
|
265
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
266
|
+
cleanup_scheduler_state
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def test_easy_perform_reraises_on_body_exception_under_scheduler
|
|
270
|
+
if skip_no_async
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
275
|
+
|
|
276
|
+
async_run do |top|
|
|
277
|
+
task = top.async do
|
|
278
|
+
c = Curl::Easy.new(url)
|
|
279
|
+
c.on_body { raise "body blew up" }
|
|
280
|
+
c.on_complete { sleep 0 }
|
|
281
|
+
c.perform
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
error = assert_raise(RuntimeError) do
|
|
285
|
+
task.wait
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
assert_equal "body blew up", error.message
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_easy_perform_reraises_on_header_exception_under_scheduler
|
|
293
|
+
if skip_no_async
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
298
|
+
|
|
299
|
+
async_run do |top|
|
|
300
|
+
task = top.async do
|
|
301
|
+
c = Curl::Easy.new(url)
|
|
302
|
+
c.on_header { raise "header blew up" }
|
|
303
|
+
c.on_complete { sleep 0 }
|
|
304
|
+
c.perform
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
error = assert_raise(RuntimeError) do
|
|
308
|
+
task.wait
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
assert_equal "header blew up", error.message
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def test_easy_perform_does_not_reraise_sibling_on_complete_exception_under_scheduler
|
|
316
|
+
if skip_no_async
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
with_ephemeral_http_server do |port, hits|
|
|
321
|
+
results = {}
|
|
322
|
+
|
|
323
|
+
async_run do |top|
|
|
324
|
+
successful = top.async do
|
|
325
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
326
|
+
easy.perform
|
|
327
|
+
results[:successful] = easy.response_code
|
|
328
|
+
rescue => e
|
|
329
|
+
results[:successful] = e
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
failing = top.async do
|
|
333
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
334
|
+
easy.on_complete { raise "boom" }
|
|
335
|
+
easy.perform
|
|
336
|
+
results[:failing] = :returned
|
|
337
|
+
rescue => e
|
|
338
|
+
results[:failing] = e
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
successful.wait
|
|
342
|
+
failing.wait
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
assert_equal 200, results[:successful], "successful scheduler peer should return normally"
|
|
346
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
|
|
347
|
+
assert_equal "boom", results[:failing].message
|
|
348
|
+
assert_equal 1, hits[:fast]
|
|
349
|
+
assert_equal 1, hits[:slow]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def test_easy_perform_does_not_reraise_fast_on_complete_exception_into_slow_successful_peer_under_scheduler
|
|
354
|
+
if skip_no_async
|
|
355
|
+
return
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
with_ephemeral_http_server do |port, hits|
|
|
359
|
+
results = {}
|
|
360
|
+
|
|
361
|
+
async_run do |top|
|
|
362
|
+
failing = top.async do
|
|
363
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
364
|
+
easy.on_complete { raise "boom" }
|
|
365
|
+
easy.perform
|
|
366
|
+
results[:failing] = :returned
|
|
367
|
+
rescue => e
|
|
368
|
+
results[:failing] = e
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
successful = top.async do
|
|
372
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
373
|
+
easy.perform
|
|
374
|
+
results[:successful] = easy.response_code
|
|
375
|
+
rescue => e
|
|
376
|
+
results[:successful] = e
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
failing.wait
|
|
380
|
+
successful.wait
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
assert_equal 200, results[:successful], "slow successful scheduler peer should not inherit a fast sibling callback error"
|
|
384
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, results[:failing]
|
|
385
|
+
assert_equal "boom", results[:failing].message
|
|
386
|
+
assert_equal 1, hits[:fast]
|
|
387
|
+
assert_equal 1, hits[:slow]
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_easy_perform_keeps_scheduler_timers_running_while_draining_deferred_on_complete_exception
|
|
392
|
+
if skip_no_async
|
|
393
|
+
return
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
with_ephemeral_http_server do |port, hits|
|
|
397
|
+
marks = {}
|
|
398
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
399
|
+
|
|
400
|
+
async_run do |top|
|
|
401
|
+
timer = top.async do
|
|
402
|
+
sleep 0.05
|
|
403
|
+
marks[:timer] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
failing = top.async do
|
|
407
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
|
|
408
|
+
easy.on_complete { raise "boom" }
|
|
409
|
+
easy.perform
|
|
410
|
+
marks[:failing] = :returned
|
|
411
|
+
rescue => e
|
|
412
|
+
marks[:failing] = e
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
successful = top.async do
|
|
416
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
417
|
+
easy.perform
|
|
418
|
+
marks[:slow_done] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
timer.wait
|
|
422
|
+
failing.wait
|
|
423
|
+
successful.wait
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
assert_kind_of Curl::Err::AbortedByCallbackError, marks[:failing]
|
|
427
|
+
assert_operator marks[:timer], :<, marks[:slow_done] - 0.02,
|
|
428
|
+
"scheduler timer should fire before the slow sibling finishes draining"
|
|
429
|
+
assert_equal 1, hits[:fast]
|
|
430
|
+
assert_equal 1, hits[:slow]
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
|
|
435
|
+
state = Curl.scheduler_state
|
|
436
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
437
|
+
waiter = {completed: false, done: false, error: nil}
|
|
438
|
+
|
|
439
|
+
state[:waiters][easy.object_id] = waiter
|
|
440
|
+
state[:pending] << easy
|
|
441
|
+
state[:multi].instance_variable_set(:@__curb_deferred_exception, RuntimeError.new("boom"))
|
|
442
|
+
|
|
443
|
+
Curl.drain_scheduler_pending(state)
|
|
444
|
+
|
|
445
|
+
assert(state[:pending].include?(easy) || waiter[:error],
|
|
446
|
+
"scheduler enqueue rejected during deferred abort should remain pending or fail its waiter instead of disappearing")
|
|
447
|
+
assert_equal 0, state[:multi].requests.length
|
|
448
|
+
ensure
|
|
449
|
+
cleanup_scheduler_state
|
|
163
450
|
end
|
|
164
451
|
|
|
165
452
|
private
|
|
@@ -168,6 +455,10 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
168
455
|
warn 'Skipping fiber scheduler tests on Windows'
|
|
169
456
|
return true
|
|
170
457
|
end
|
|
458
|
+
unless fiber_scheduler_supported?
|
|
459
|
+
warn 'Skipping fiber scheduler tests (Fiber scheduler API unavailable)'
|
|
460
|
+
return true
|
|
461
|
+
end
|
|
171
462
|
unless HAS_ASYNC
|
|
172
463
|
warn 'Skipping fiber scheduler test (Async gem not available)'
|
|
173
464
|
return true
|
|
@@ -183,8 +474,52 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
183
474
|
Async(&block)
|
|
184
475
|
end
|
|
185
476
|
end
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
477
|
+
|
|
478
|
+
def fiber_scheduler_supported?
|
|
479
|
+
Fiber.respond_to?(:set_scheduler) && Fiber.respond_to?(:schedule)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def with_scheduler(scheduler)
|
|
483
|
+
previous_scheduler = Fiber.scheduler if Fiber.respond_to?(:scheduler)
|
|
484
|
+
Fiber.set_scheduler(scheduler)
|
|
485
|
+
fiber = Fiber.schedule { yield }
|
|
486
|
+
fiber.resume while fiber.alive?
|
|
487
|
+
ensure
|
|
488
|
+
Fiber.set_scheduler(previous_scheduler) if Fiber.respond_to?(:set_scheduler)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def cleanup_scheduler_state
|
|
492
|
+
state = Thread.current.thread_variable_get(:curb_scheduler_state)
|
|
493
|
+
state[:multi].close if state && state[:multi]
|
|
494
|
+
Thread.current.thread_variable_set(:curb_scheduler_state, nil)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def with_ephemeral_http_server
|
|
498
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
499
|
+
port = port_socket.addr[1]
|
|
500
|
+
port_socket.close
|
|
501
|
+
|
|
502
|
+
hits = Hash.new(0)
|
|
503
|
+
server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
504
|
+
server.mount_proc('/fast') do |_req, res|
|
|
505
|
+
hits[:fast] += 1
|
|
506
|
+
res['Content-Type'] = 'text/plain'
|
|
507
|
+
res.body = 'fast'
|
|
508
|
+
end
|
|
509
|
+
server.mount_proc('/slow') do |_req, res|
|
|
510
|
+
hits[:slow] += 1
|
|
511
|
+
res['Content-Type'] = 'text/plain'
|
|
512
|
+
sleep 0.2
|
|
513
|
+
res.body = 'slow'
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
server_thread = Thread.new(server) { |srv| srv.start }
|
|
517
|
+
wait_for_server_ready(port, thread: server_thread)
|
|
518
|
+
|
|
519
|
+
yield port, hits
|
|
520
|
+
ensure
|
|
521
|
+
server.shutdown if defined?(server) && server
|
|
522
|
+
server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
|
|
523
|
+
server_thread.kill if defined?(server_thread) && server_thread&.alive?
|
|
524
|
+
end
|
|
190
525
|
end
|
data/tests/tc_gc_compact.rb
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require File.expand_path('helper', __dir__)
|
|
4
4
|
|
|
5
5
|
class TestGcCompact < Test::Unit::TestCase
|
|
6
|
-
ITERATIONS = (ENV['CURB_GC_COMPACT_ITERATIONS'] ||
|
|
6
|
+
ITERATIONS = (ENV['CURB_GC_COMPACT_ITERATIONS'] || 50).to_i
|
|
7
7
|
EASY_PER_MULTI = 3
|
|
8
|
+
GC_CRASH_REGRESSION_ITERATIONS = (ENV['CURB_GC_CRASH_REGRESSION_ITERATIONS'] || [ITERATIONS, 10].min).to_i
|
|
8
9
|
|
|
9
10
|
def setup
|
|
10
11
|
omit('GC.compact unavailable on this Ruby') unless defined?(GC.compact)
|
|
@@ -12,38 +13,153 @@ class TestGcCompact < Test::Unit::TestCase
|
|
|
12
13
|
|
|
13
14
|
def test_multi_perform_with_gc_compact
|
|
14
15
|
ITERATIONS.times do
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
compact
|
|
19
|
-
assert_nothing_raised { multi.perform }
|
|
20
|
-
compact
|
|
16
|
+
run_multi_perform_compact_iteration
|
|
17
|
+
collect_after_iteration
|
|
21
18
|
end
|
|
22
19
|
end
|
|
23
20
|
|
|
24
21
|
def test_gc_compact_during_multi_cleanup
|
|
25
|
-
|
|
26
|
-
multi = Curl::Multi.new
|
|
27
|
-
add_easy_handles(multi)
|
|
22
|
+
omit('GC cleanup isolation requires fork') if NO_FORK || WINDOWS
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
pid = fork do
|
|
25
|
+
begin
|
|
26
|
+
ITERATIONS.times do
|
|
27
|
+
run_multi_cleanup_compact_iteration
|
|
28
|
+
collect_after_iteration
|
|
29
|
+
end
|
|
30
|
+
exit! 0
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
warn("#{e.class}: #{e.message}")
|
|
33
|
+
warn(e.backtrace.join("\n")) if e.backtrace
|
|
34
|
+
exit! 1
|
|
35
|
+
end
|
|
32
36
|
end
|
|
37
|
+
|
|
38
|
+
_child_pid, status = Process.wait2(pid)
|
|
39
|
+
assert_predicate status, :success?
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def test_gc_compact_after_detach
|
|
43
|
+
run_multi_detach_compact_iteration
|
|
44
|
+
collect_after_iteration
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_gc_compact_after_failed_implicit_multi_cleanup
|
|
48
|
+
GC_CRASH_REGRESSION_ITERATIONS.times do
|
|
49
|
+
run_failed_implicit_multi_cleanup_iteration(compact: true, cleanup_with_objectspace: false)
|
|
50
|
+
collect_after_iteration
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_gc_cleanup_after_failed_implicit_multi_callback
|
|
55
|
+
omit('GC cleanup isolation requires fork') if NO_FORK || WINDOWS
|
|
56
|
+
|
|
57
|
+
assert_child_process_success do
|
|
58
|
+
GC_CRASH_REGRESSION_ITERATIONS.times do
|
|
59
|
+
run_failed_implicit_multi_cleanup_iteration(compact: true, cleanup_with_objectspace: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_gc_cleanup_after_invalid_multi_assignment_rejection
|
|
65
|
+
omit('GC cleanup isolation requires fork') if NO_FORK || WINDOWS
|
|
66
|
+
|
|
67
|
+
assert_child_process_success do
|
|
68
|
+
GC_CRASH_REGRESSION_ITERATIONS.times do
|
|
69
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
70
|
+
|
|
71
|
+
begin
|
|
72
|
+
easy.multi = Object.new
|
|
73
|
+
raise 'expected TypeError from Curl::Easy#multi='
|
|
74
|
+
rescue TypeError => error
|
|
75
|
+
raise "unexpected error message: #{error.message}" unless /Curl::Multi/.match?(error.message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
easy = nil
|
|
79
|
+
full_gc(compact: true)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_gc_compact_easy
|
|
85
|
+
iteration = 0
|
|
86
|
+
responses = []
|
|
87
|
+
while iteration < ITERATIONS
|
|
88
|
+
res = Curl.get($TEST_URL) do |easy|
|
|
89
|
+
easy.timeout = 5
|
|
90
|
+
easy.on_complete { |_e| }
|
|
91
|
+
easy.on_failure { |_e, _code| }
|
|
92
|
+
end
|
|
93
|
+
iteration += 1
|
|
94
|
+
responses << res.body
|
|
95
|
+
compact
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def run_multi_perform_compact_iteration
|
|
102
|
+
multi = Curl::Multi.new
|
|
103
|
+
handles = add_easy_handles(multi)
|
|
104
|
+
|
|
105
|
+
compact
|
|
106
|
+
multi.perform { compact }
|
|
107
|
+
compact
|
|
108
|
+
ensure
|
|
109
|
+
handles&.each do |easy|
|
|
110
|
+
begin
|
|
111
|
+
easy.close
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
multi&.close
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def run_multi_cleanup_compact_iteration
|
|
120
|
+
multi = Curl::Multi.new
|
|
121
|
+
add_easy_handles(multi)
|
|
122
|
+
|
|
123
|
+
compact
|
|
124
|
+
multi = nil
|
|
125
|
+
compact
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run_multi_detach_compact_iteration
|
|
36
129
|
multi = Curl::Multi.new
|
|
37
130
|
handles = add_easy_handles(multi)
|
|
38
131
|
|
|
39
132
|
compact
|
|
40
|
-
|
|
133
|
+
multi.perform { compact }
|
|
41
134
|
|
|
42
|
-
handles.each { |easy| multi.remove(easy) }
|
|
135
|
+
handles.each { |easy| multi.remove(easy); compact }
|
|
43
136
|
compact
|
|
137
|
+
ensure
|
|
138
|
+
multi&.close
|
|
44
139
|
end
|
|
45
140
|
|
|
46
|
-
|
|
141
|
+
def run_failed_implicit_multi_cleanup_iteration(compact:, cleanup_with_objectspace:)
|
|
142
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
143
|
+
easy.on_complete { raise 'complete blew up' }
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
easy.perform
|
|
147
|
+
raise 'expected callback abort'
|
|
148
|
+
rescue Curl::Err::AbortedByCallbackError => error
|
|
149
|
+
raise "unexpected callback message: #{error.message}" unless error.message == 'complete blew up'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
raise 'expected implicit cleanup to clear easy.multi' unless easy.multi.nil?
|
|
153
|
+
ensure
|
|
154
|
+
easy = nil
|
|
155
|
+
if cleanup_with_objectspace
|
|
156
|
+
run_teardown_multi_cleanup
|
|
157
|
+
flush_deferred_multi_closes_all
|
|
158
|
+
else
|
|
159
|
+
flush_deferred_multi_closes_all
|
|
160
|
+
end
|
|
161
|
+
full_gc(compact: compact)
|
|
162
|
+
end
|
|
47
163
|
|
|
48
164
|
def add_easy_handles(multi)
|
|
49
165
|
Array.new(EASY_PER_MULTI) do
|
|
@@ -58,4 +174,50 @@ class TestGcCompact < Test::Unit::TestCase
|
|
|
58
174
|
def compact
|
|
59
175
|
GC.compact
|
|
60
176
|
end
|
|
177
|
+
|
|
178
|
+
def full_gc(compact: false)
|
|
179
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
180
|
+
GC.compact if compact && GC.respond_to?(:compact)
|
|
181
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def flush_deferred_multi_closes_all
|
|
185
|
+
return unless Curl::Easy.respond_to?(:flush_deferred_multi_closes)
|
|
186
|
+
|
|
187
|
+
Curl::Easy.flush_deferred_multi_closes(all_threads: true)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def run_teardown_multi_cleanup
|
|
191
|
+
ObjectSpace.each_object(Curl::Multi) do |multi|
|
|
192
|
+
begin
|
|
193
|
+
next if multi.instance_variable_defined?(:@deferred_close) && multi.instance_variable_get(:@deferred_close)
|
|
194
|
+
|
|
195
|
+
multi.instance_variable_set(:@requests, {})
|
|
196
|
+
multi._close
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def assert_child_process_success
|
|
204
|
+
pid = fork do
|
|
205
|
+
begin
|
|
206
|
+
yield
|
|
207
|
+
exit! 0
|
|
208
|
+
rescue Exception => e
|
|
209
|
+
warn("#{e.class}: #{e.message}")
|
|
210
|
+
warn(e.backtrace.join("\n")) if e.backtrace
|
|
211
|
+
exit! 1
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
_child_pid, status = Process.wait2(pid)
|
|
216
|
+
assert_predicate status, :success?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def collect_after_iteration
|
|
220
|
+
ObjectSpace.garbage_collect
|
|
221
|
+
ObjectSpace.garbage_collect
|
|
222
|
+
end
|
|
61
223
|
end
|