curb 1.2.1 → 1.3.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.
data/lib/curl.rb CHANGED
@@ -6,6 +6,199 @@ require 'uri'
6
6
 
7
7
  # expose shortcut methods
8
8
  module Curl
9
+ def self.scheduler_active?
10
+ Fiber.respond_to?(:scheduler) && !Fiber.scheduler.nil?
11
+ end
12
+
13
+ def self.deferred_exception_source_id(state)
14
+ return unless state[:multi].instance_variable_defined?(:@__curb_deferred_exception_source_id)
15
+
16
+ state[:multi].instance_variable_get(:@__curb_deferred_exception_source_id)
17
+ end
18
+
19
+ def self.scheduler_waiter_blocking_supported?
20
+ scheduler = Fiber.scheduler
21
+ scheduler && scheduler.respond_to?(:block) && scheduler.respond_to?(:unblock)
22
+ end
23
+
24
+ def self.wake_scheduler_waiter(waiter)
25
+ fiber = waiter[:fiber]
26
+ scheduler = waiter[:scheduler]
27
+ return unless fiber&.alive? && scheduler&.respond_to?(:unblock)
28
+
29
+ scheduler.unblock(waiter, fiber)
30
+ end
31
+
32
+ def self.complete_scheduler_waiter(waiter)
33
+ return if waiter[:done]
34
+
35
+ waiter[:done] = true
36
+ wake_scheduler_waiter(waiter)
37
+ end
38
+
39
+ def self.fail_scheduler_waiter(waiter, error)
40
+ return if waiter[:error]
41
+
42
+ waiter[:error] = error
43
+ wake_scheduler_waiter(waiter)
44
+ end
45
+
46
+ def self.release_scheduler_error(state, error)
47
+ source_waiter = state[:waiters][deferred_exception_source_id(state)]
48
+
49
+ if source_waiter
50
+ fail_scheduler_waiter(source_waiter, error)
51
+ else
52
+ state[:error] = error
53
+ state[:waiters].each_value { |waiter| wake_scheduler_waiter(waiter) }
54
+ end
55
+ end
56
+
57
+ def self.block_scheduler_waiter(waiter)
58
+ unless scheduler_waiter_blocking_supported?
59
+ sleep 0
60
+ return
61
+ end
62
+
63
+ waiter[:fiber] = Fiber.current
64
+ waiter[:scheduler] ||= Fiber.scheduler
65
+ return if waiter[:done] || waiter[:error]
66
+
67
+ waiter[:scheduler].block(waiter, nil)
68
+ ensure
69
+ waiter[:fiber] = nil if waiter[:fiber].equal?(Fiber.current)
70
+ end
71
+
72
+ def self.scheduler_yield
73
+ scheduler = Fiber.scheduler
74
+
75
+ if scheduler&.respond_to?(:kernel_sleep)
76
+ scheduler.kernel_sleep(0)
77
+ else
78
+ sleep 0
79
+ end
80
+ end
81
+
82
+ def self.release_scheduler_waiters(state)
83
+ source_id = deferred_exception_source_id(state)
84
+
85
+ state[:waiters].each do |easy_id, waiter|
86
+ next if source_id == easy_id
87
+
88
+ complete_scheduler_waiter(waiter) if waiter[:completed]
89
+ end
90
+ end
91
+
92
+ def self.perform_with_scheduler(easy)
93
+ state = scheduler_state
94
+ waiter = {completed: false, done: false, error: nil, fiber: nil, scheduler: Fiber.scheduler}
95
+ state[:waiters][easy.object_id] = waiter
96
+ previous_complete = easy.on_complete do |completed_easy|
97
+ previous_complete.call(completed_easy) if previous_complete
98
+ waiter[:completed] = true
99
+ end
100
+
101
+ state[:pending] << easy
102
+ ensure_scheduler_driver(state)
103
+
104
+ until waiter[:done]
105
+ raise waiter[:error] if waiter[:error]
106
+ raise state[:error] if state[:error]
107
+ block_scheduler_waiter(waiter)
108
+ end
109
+
110
+ while state[:driver_running] && state[:pending].empty? &&
111
+ state[:waiters].length == 1 && state[:waiters].key?(easy.object_id)
112
+ scheduler_yield
113
+ end
114
+
115
+ true
116
+ ensure
117
+ state[:waiters].delete(easy.object_id) if defined?(state) && state[:waiters]
118
+ if defined?(previous_complete)
119
+ if previous_complete
120
+ easy.on_complete(&previous_complete)
121
+ else
122
+ easy.on_complete
123
+ end
124
+ end
125
+ end
126
+
127
+ def self.scheduler_state
128
+ Thread.current.thread_variable_get(:curb_scheduler_state) || begin
129
+ state = {
130
+ multi: Curl::Multi.new,
131
+ pending: [],
132
+ driver_running: false,
133
+ error: nil,
134
+ waiters: {},
135
+ }
136
+ Thread.current.thread_variable_set(:curb_scheduler_state, state)
137
+ state
138
+ end
139
+ end
140
+
141
+ def self.ensure_scheduler_driver(state)
142
+ return if state[:driver_running]
143
+
144
+ state[:driver_running] = true
145
+ state[:error] = nil
146
+
147
+ runner = proc do
148
+ begin
149
+ # Give sibling fibers a chance to enqueue work so the shared multi can
150
+ # batch scheduler-driven Easy#perform calls together.
151
+ pending_count = -1
152
+ until pending_count == state[:pending].size
153
+ pending_count = state[:pending].size
154
+ scheduler_yield
155
+ end
156
+
157
+ loop do
158
+ drain_scheduler_pending(state)
159
+ break if state[:multi].idle?
160
+
161
+ begin
162
+ state[:multi].perform do
163
+ drain_scheduler_pending(state)
164
+ release_scheduler_waiters(state)
165
+ scheduler_yield
166
+ end
167
+ ensure
168
+ # Release any siblings that completed just before a deferred
169
+ # callback exception is re-raised.
170
+ release_scheduler_waiters(state)
171
+ end
172
+ end
173
+ rescue => e
174
+ release_scheduler_waiters(state)
175
+ release_scheduler_error(state, e)
176
+ ensure
177
+ state[:driver_running] = false
178
+ ensure_scheduler_driver(state) if state[:error].nil? && !state[:pending].empty?
179
+ end
180
+ end
181
+
182
+ if Fiber.respond_to?(:schedule)
183
+ Fiber.schedule(&runner)
184
+ else
185
+ Fiber.new(blocking: false, &runner).resume
186
+ end
187
+ end
188
+
189
+ def self.drain_scheduler_pending(state)
190
+ pending = state[:pending]
191
+ until pending.empty?
192
+ easy = pending.first
193
+
194
+ break if state[:multi].instance_variable_defined?(:@__curb_deferred_exception)
195
+
196
+ state[:multi].add(easy)
197
+ break unless state[:multi].requests.key?(easy.object_id)
198
+
199
+ pending.shift
200
+ end
201
+ end
9
202
 
10
203
  def self.http(verb, url, post_body=nil, put_data=nil, &block)
11
204
  if Thread.current[:curb_curl_yielding]
data/tests/helper.rb CHANGED
@@ -4,6 +4,8 @@ $CURB_TESTING = true
4
4
  require 'uri'
5
5
  require 'stringio'
6
6
  require 'digest/md5'
7
+ require 'rbconfig'
8
+ require File.join(RbConfig::CONFIG['rubylibdir'], 'timeout')
7
9
 
8
10
  $TOPDIR = File.expand_path(File.join(File.dirname(__FILE__), '..'))
9
11
  $EXTDIR = File.join($TOPDIR, 'ext')
@@ -57,6 +59,7 @@ $TEST_URL = "file://#{'/' if RUBY_DESCRIPTION =~ /mswin|msys|mingw|cygwin|bccwin
57
59
 
58
60
  require 'thread'
59
61
  require 'webrick'
62
+ require 'socket'
60
63
 
61
64
  # set this to true to avoid testing with multiple threads
62
65
  # or to test with multiple threads set it to false
@@ -64,21 +67,39 @@ require 'webrick'
64
67
  # on the presence of multiple threads
65
68
  TEST_SINGLE_THREADED=false
66
69
 
67
- # keep webrick quiet
68
- ::WEBrick::HTTPServer.send(:remove_method,:access_log) if ::WEBrick::HTTPServer.instance_methods.include?(:access_log)
69
- ::WEBrick::BasicLog.send(:remove_method,:log) if ::WEBrick::BasicLog.instance_methods.include?(:log)
70
+ WEBRICK_TEST_LOG = WEBrick::Log.new(File.open(File::NULL, 'w'), WEBrick::BasicLog::ERROR)
71
+
72
+ module CurbTestResourceCleanup
73
+ def teardown
74
+ super
75
+ ensure
76
+ begin
77
+ if Curl::Easy.respond_to?(:flush_deferred_multi_closes)
78
+ Curl::Easy.flush_deferred_multi_closes(all_threads: true)
79
+ end
80
+ rescue StandardError
81
+ nil
82
+ end
83
+
84
+ begin
85
+ ObjectSpace.each_object(Curl::Multi) do |multi|
86
+ begin
87
+ next if multi.instance_variable_defined?(:@deferred_close) && multi.instance_variable_get(:@deferred_close)
88
+ multi.instance_variable_set(:@requests, {})
89
+ multi._close
90
+ rescue StandardError
91
+ nil
92
+ end
93
+ end
94
+ rescue StandardError
95
+ nil
96
+ end
70
97
 
71
- ::WEBrick::HTTPServer.class_eval do
72
- def access_log(config, req, res)
73
- # nop
74
- end
75
- end
76
- ::WEBrick::BasicLog.class_eval do
77
- def log(level, data)
78
- # nop
79
98
  end
80
99
  end
81
100
 
101
+ Test::Unit::TestCase.prepend(CurbTestResourceCleanup)
102
+
82
103
  #
83
104
  # Simple test server to record number of times a request is sent/recieved of a specific
84
105
  # request type, e.g. GET,POST,PUT,DELETE
@@ -187,7 +208,7 @@ end
187
208
  module BugTestServerSetupTeardown
188
209
  def setup
189
210
  @port ||= 9992
190
- @server = WEBrick::HTTPServer.new( :Port => @port )
211
+ @server = WEBrick::HTTPServer.new(:Port => @port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
191
212
  @server.mount_proc("/test") do|req,res|
192
213
  if @response_proc
193
214
  @response_proc.call(res)
@@ -211,14 +232,170 @@ module BugTestServerSetupTeardown
211
232
  end
212
233
 
213
234
  module TestServerMethods
235
+ def server_responding?(port)
236
+ socket = TCPSocket.new('127.0.0.1', port)
237
+ socket.close
238
+ true
239
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, IOError
240
+ false
241
+ end
242
+
243
+ def server_startup_timeout
244
+ ENV['RUBY_MEMCHECK_RUNNING'] ? 30 : 5
245
+ end
246
+
247
+ def wait_for_server_ready(port, thread: nil)
248
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_startup_timeout
249
+
250
+ loop do
251
+ if thread && !thread.alive?
252
+ return false
253
+ end
254
+
255
+ return true if server_responding?(port)
256
+
257
+ raise "Failed to startup test server on port #{port}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
258
+
259
+ sleep 0.01
260
+ end
261
+ end
262
+
263
+ def server_shutdown_timeout
264
+ [server_startup_timeout, 0.25].max
265
+ end
266
+
267
+ def wait_for_server_stopped(port, thread: nil)
268
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_shutdown_timeout
269
+
270
+ loop do
271
+ return true unless server_responding?(port)
272
+
273
+ if thread && !thread.alive?
274
+ return !server_responding?(port)
275
+ end
276
+
277
+ return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
278
+
279
+ sleep 0.01
280
+ end
281
+ end
282
+
214
283
  def locked_file
215
284
  File.join(File.dirname(__FILE__),"server_lock-#{@__port}")
216
285
  end
217
286
 
287
+ def read_server_lock_pid
288
+ Integer(File.read(locked_file).strip, 10)
289
+ rescue Errno::ENOENT, ArgumentError, TypeError
290
+ nil
291
+ end
292
+
293
+ def write_server_lock(pid = Process.pid)
294
+ File.open(locked_file, 'w') { |f| f << "#{pid}\n" }
295
+ end
296
+
297
+ def process_alive?(pid)
298
+ return false unless pid && pid.positive?
299
+
300
+ Process.kill(0, pid)
301
+ true
302
+ rescue Errno::ESRCH
303
+ false
304
+ rescue Errno::EPERM
305
+ true
306
+ end
307
+
308
+ def server_lock_fresh?
309
+ (Time.now - File.mtime(locked_file)) < server_startup_timeout
310
+ rescue Errno::ENOENT
311
+ false
312
+ end
313
+
314
+ def stale_server_lock?(port)
315
+ return false unless File.exist?(locked_file)
316
+
317
+ pid = read_server_lock_pid
318
+ return false if pid && process_alive?(pid) && server_lock_fresh?
319
+ return false if server_responding?(port)
320
+
321
+ true
322
+ end
323
+
324
+ def clear_stale_server_lock(port)
325
+ return unless stale_server_lock?(port)
326
+
327
+ File.unlink(locked_file)
328
+ rescue Errno::ENOENT
329
+ nil
330
+ end
331
+
332
+ def stop_test_server
333
+ server = instance_variable_defined?(:@server) ? @server : nil
334
+ pid = instance_variable_defined?(:@__pid) ? @__pid : nil
335
+ return unless server || pid
336
+
337
+ if TEST_SINGLE_THREADED
338
+ @__pid = nil
339
+
340
+ if pid
341
+ begin
342
+ Process.kill('INT', pid)
343
+ rescue Errno::ESRCH
344
+ nil
345
+ end
346
+
347
+ begin
348
+ Process.wait(pid)
349
+ rescue Errno::ECHILD, Errno::ESRCH
350
+ nil
351
+ end
352
+ end
353
+ else
354
+ thread = instance_variable_defined?(:@test_thread) ? @test_thread : nil
355
+ port = instance_variable_defined?(:@__port) ? @__port : nil
356
+ @server = nil
357
+ @test_thread = nil
358
+
359
+ server.shutdown if server
360
+ wait_for_server_stopped(port, thread: thread) if port
361
+ thread.join(server_shutdown_timeout) if thread
362
+
363
+ if thread&.alive?
364
+ thread.kill
365
+ thread.join(server_shutdown_timeout)
366
+ wait_for_server_stopped(port) if port
367
+ end
368
+ end
369
+
370
+ File.unlink(locked_file) if File.exist?(locked_file)
371
+ rescue Errno::ENOENT
372
+ nil
373
+ end
374
+
375
+ def teardown
376
+ super
377
+ ensure
378
+ stop_test_server
379
+ end
380
+
218
381
  def server_setup(port=9129,servlet=TestServlet)
219
382
  @__port = port
220
- if (@server ||= nil).nil? and !File.exist?(locked_file)
221
- File.open(locked_file,'w') {|f| f << 'locked' }
383
+ @server = nil unless instance_variable_defined?(:@server)
384
+ @__pid = nil unless instance_variable_defined?(:@__pid)
385
+ @test_thread = nil unless instance_variable_defined?(:@test_thread)
386
+ clear_stale_server_lock(port)
387
+
388
+ if @server.nil? && File.exist?(locked_file)
389
+ begin
390
+ wait_for_server_ready(port)
391
+ return
392
+ rescue RuntimeError
393
+ clear_stale_server_lock(port)
394
+ end
395
+ end
396
+
397
+ if @server.nil? and !File.exist?(locked_file)
398
+ write_server_lock
222
399
  if TEST_SINGLE_THREADED
223
400
  rd, wr = IO.pipe
224
401
  @__pid = fork do
@@ -226,33 +403,50 @@ module TestServerMethods
226
403
  rd = nil
227
404
 
228
405
  # start up a webrick server for testing delete
229
- server = WEBrick::HTTPServer.new :Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__))
406
+ server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
230
407
 
231
408
  server.mount(servlet.path, servlet)
232
409
  server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
233
410
 
234
411
  trap("INT") { server.shutdown }
235
412
  GC.start
236
- wr.flush
237
- wr.close
238
- server.start
413
+ server_thread = Thread.new { server.start }
414
+
415
+ begin
416
+ if wait_for_server_ready(port, thread: server_thread)
417
+ wr.write('1')
418
+ else
419
+ wr.write('0')
420
+ end
421
+ rescue StandardError
422
+ wr.write('0')
423
+ ensure
424
+ wr.flush
425
+ wr.close
426
+ end
427
+
428
+ server_thread.join
239
429
  end
240
430
  wr.close
241
- rd.read
431
+ ready = rd.read
242
432
  rd.close
433
+ if ready != '1'
434
+ STDERR.puts "Failed to startup test server!"
435
+ exit(1)
436
+ end
243
437
  else
244
438
  # start up a webrick server for testing delete
245
- @server = WEBrick::HTTPServer.new :Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__))
439
+ server = WEBrick::HTTPServer.new(:Port => port, :DocumentRoot => File.expand_path(File.dirname(__FILE__)), :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
246
440
 
247
- @server.mount(servlet.path, servlet)
248
- @server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
249
- queue = Queue.new # synchronize the thread startup to the main thread
441
+ server.mount(servlet.path, servlet)
442
+ server.mount("/ext", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__),'..','ext'))
250
443
 
251
- @test_thread = Thread.new { queue << 1; @server.start }
444
+ @server = server
445
+ # Keep a stable reference inside the thread so helper shutdown can clear
446
+ # @server without racing the server startup path.
447
+ @test_thread = Thread.new(server) { |srv| srv.start }
252
448
 
253
- # wait for the queue
254
- value = queue.pop
255
- if !value
449
+ if !wait_for_server_ready(port, thread: @test_thread)
256
450
  STDERR.puts "Failed to startup test server!"
257
451
  exit(1)
258
452
  end
@@ -261,15 +455,7 @@ module TestServerMethods
261
455
 
262
456
  exit_code = lambda do
263
457
  begin
264
- if File.exist?(locked_file)
265
- File.unlink locked_file
266
- if TEST_SINGLE_THREADED
267
- Process.kill 'INT', @__pid
268
- else
269
- @server.shutdown unless @server.nil?
270
- end
271
- end
272
- #@server.shutdown unless @server.nil?
458
+ stop_test_server
273
459
  rescue Object => e
274
460
  puts "Error #{__FILE__}:#{__LINE__}\n#{e.message}"
275
461
  end