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.
- checksums.yaml +4 -4
- data/README.md +38 -1
- data/Rakefile +12 -1
- data/ext/curb.c +17 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +71 -31
- data/ext/curb_easy.h +1 -0
- data/ext/curb_multi.c +428 -95
- data/ext/curb_multi.h +4 -0
- data/ext/curb_postfield.c +31 -31
- data/ext/extconf.rb +6 -0
- data/lib/curl/easy.rb +18 -9
- data/lib/curl.rb +49 -12
- data/tests/helper.rb +122 -3
- data/tests/tc_curl_easy.rb +59 -4
- data/tests/tc_curl_easy_cookielist.rb +8 -4
- 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_postfield.rb +39 -0
- data/tests/tc_fiber_scheduler.rb +101 -2
- data/tests/tc_ractor.rb +94 -0
- metadata +8 -2
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -118,7 +118,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
118
118
|
|
|
119
119
|
duration = Time.now - started
|
|
120
120
|
|
|
121
|
-
|
|
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
|
-
|
|
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
|
data/tests/tc_ractor.rb
ADDED
|
@@ -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.
|
|
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-
|
|
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
|