curb 1.3.7 → 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.
@@ -118,7 +118,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
118
118
 
119
119
  duration = Time.now - started
120
120
 
121
- 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}"
121
+ assert_ran_concurrently(duration)
122
122
  assert_equal ITERS, results.size
123
123
  assert_equal ITERS.times.map {200}, results
124
124
  end
@@ -149,7 +149,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
149
149
 
150
150
  duration = Time.now - started
151
151
 
152
- 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}"
152
+ assert_ran_concurrently(duration)
153
153
  assert_equal ITERS, results.size
154
154
  assert_equal ITERS.times.map {200}, results
155
155
  end
@@ -734,6 +734,97 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
734
734
  end
735
735
  end
736
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
+
737
828
  def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
738
829
  state = Curl.scheduler_state
739
830
  easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
@@ -849,6 +940,14 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
849
940
  false
850
941
  end
851
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
+
852
951
  def socket_perform_supported?
853
952
  Curl::Multi.private_method_defined?(:_socket_perform)
854
953
  end
@@ -0,0 +1,94 @@
1
+ require File.expand_path('helper', __dir__)
2
+
3
+ class TestCurbRactor < Test::Unit::TestCase
4
+ include TestServerMethods
5
+
6
+ RACTOR_WORKERS = 4
7
+ REQUESTS_PER_WORKER = 3
8
+
9
+ def setup
10
+ super
11
+ server_setup
12
+ end
13
+
14
+ def test_exported_string_constants_are_shareable
15
+ omit('Ractor is unavailable on this Ruby') unless defined?(Ractor)
16
+
17
+ %i[CURB_VERSION VERSION CURL_VERSION LONG_VERSION CURL_LONG_VERSION].each do |name|
18
+ value = Curl.const_get(name)
19
+ assert_predicate value, :frozen?, "Curl::#{name} should be frozen"
20
+ assert Ractor.shareable?(value), "Curl::#{name} should be Ractor-shareable"
21
+ end
22
+ end
23
+
24
+ def test_independent_ractors_can_perform_requests_concurrently
25
+ original_timeout = Curl::Multi.default_timeout
26
+ original_autoclose = Curl::Multi.autoclose
27
+ omit_unless_curb_ractor_safe
28
+
29
+ url = TestServlet.url
30
+
31
+ workers = RACTOR_WORKERS.times.map do |worker_id|
32
+ Ractor.new(url, worker_id, REQUESTS_PER_WORKER) do |target, id, request_count|
33
+ Curl.safe! { |config| config.protocols = [:http] }
34
+ Curl::Multi.default_timeout = 25 + id
35
+ Curl::Multi.autoclose = true
36
+
37
+ responses = request_count.times.map do
38
+ chunks = 0
39
+ easy = Curl::Easy.new(target)
40
+ easy.on_body do |data|
41
+ chunks += 1
42
+ data.bytesize
43
+ end
44
+ easy.perform
45
+ [easy.response_code, chunks]
46
+ end
47
+
48
+ {
49
+ responses: responses,
50
+ timeout: Curl::Multi.default_timeout,
51
+ autoclose: Curl::Multi.autoclose,
52
+ deferred_closes: Curl::Easy.deferred_multi_closes.length,
53
+ version: Curl::VERSION,
54
+ }
55
+ end
56
+ end
57
+
58
+ results = workers.map { |worker| ractor_value(worker) }
59
+
60
+ results.each_with_index do |result, worker_id|
61
+ assert_equal REQUESTS_PER_WORKER, result[:responses].length
62
+ result[:responses].each do |status, chunks|
63
+ assert_equal 200, status
64
+ assert_operator chunks, :>=, 1
65
+ end
66
+ assert_equal 25 + worker_id, result[:timeout]
67
+ assert_equal true, result[:autoclose]
68
+ assert_equal 0, result[:deferred_closes]
69
+ assert_equal Curl::VERSION, result[:version]
70
+ end
71
+
72
+ assert_equal original_timeout, Curl::Multi.default_timeout
73
+ assert_equal original_autoclose, Curl::Multi.autoclose
74
+ ensure
75
+ Curl::Multi.default_timeout = original_timeout if defined?(original_timeout)
76
+ Curl::Multi.autoclose = original_autoclose if defined?(original_autoclose)
77
+ end
78
+
79
+ private
80
+
81
+ def omit_unless_curb_ractor_safe
82
+ omit('Ractor is unavailable on this Ruby') unless defined?(Ractor)
83
+ omit('This build does not advertise Ractor-safe native support') unless
84
+ Curl.const_defined?(:RACTOR_SAFE) && Curl::RACTOR_SAFE
85
+ end
86
+
87
+ def ractor_value(ractor)
88
+ if ractor.respond_to?(:value)
89
+ ractor.value
90
+ else
91
+ ractor.take
92
+ end
93
+ end
94
+ end
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.7
4
+ version: 1.4.0
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-07-10 00:00:00.000000000 Z
11
+ date: 2026-09-27 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
@@ -78,6 +78,8 @@ files:
78
78
  - tests/tc_curl_easy_setopt.rb
79
79
  - tests/tc_curl_maxfilesize.rb
80
80
  - tests/tc_curl_multi.rb
81
+ - tests/tc_curl_multi_poll.rb
82
+ - tests/tc_curl_multi_socket_poller.rb
81
83
  - tests/tc_curl_native_coverage.rb
82
84
  - tests/tc_curl_network_policy.rb
83
85
  - tests/tc_curl_postfield.rb
@@ -85,6 +87,7 @@ files:
85
87
  - tests/tc_fiber_scheduler.rb
86
88
  - tests/tc_ftp_options.rb
87
89
  - tests/tc_gc_compact.rb
90
+ - tests/tc_ractor.rb
88
91
  - tests/tc_test_server_methods.rb
89
92
  - tests/timeout.rb
90
93
  - tests/timeout_server.rb
@@ -148,6 +151,8 @@ test_files:
148
151
  - tests/tc_curl_easy_setopt.rb
149
152
  - tests/tc_curl_maxfilesize.rb
150
153
  - tests/tc_curl_multi.rb
154
+ - tests/tc_curl_multi_poll.rb
155
+ - tests/tc_curl_multi_socket_poller.rb
151
156
  - tests/tc_curl_native_coverage.rb
152
157
  - tests/tc_curl_network_policy.rb
153
158
  - tests/tc_curl_postfield.rb
@@ -155,6 +160,7 @@ test_files:
155
160
  - tests/tc_fiber_scheduler.rb
156
161
  - tests/tc_ftp_options.rb
157
162
  - tests/tc_gc_compact.rb
163
+ - tests/tc_ractor.rb
158
164
  - tests/tc_test_server_methods.rb
159
165
  - tests/timeout.rb
160
166
  - tests/timeout_server.rb