curb 1.2.2 → 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.
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'objspace'
6
+ require 'optparse'
7
+ require 'rbconfig'
8
+ require 'tmpdir'
9
+
10
+ TOPDIR = File.expand_path('..', __dir__)
11
+ LIBDIR = File.join(TOPDIR, 'lib')
12
+ EXTDIR = File.join(TOPDIR, 'ext')
13
+ $LOAD_PATH.unshift(LIBDIR)
14
+ $LOAD_PATH.unshift(EXTDIR)
15
+
16
+ require 'curb'
17
+
18
+ module LeakTrace
19
+ Record = Struct.new(:identifier, :created_location, :closed, keyword_init: true)
20
+
21
+ module_function
22
+
23
+ def install_multi_trace!
24
+ return if @multi_trace_installed
25
+
26
+ @multi_trace_installed = true
27
+ @multi_records = {}
28
+
29
+ multi_singleton = class << Curl::Multi
30
+ self
31
+ end
32
+
33
+ multi_singleton.alias_method(:__leak_trace_new, :new)
34
+ multi_singleton.define_method(:new) do |*args, **kwargs, &block|
35
+ multi = if kwargs.empty?
36
+ __leak_trace_new(*args, &block)
37
+ else
38
+ __leak_trace_new(*args, **kwargs, &block)
39
+ end
40
+
41
+ LeakTrace.multi_records[multi.object_id] ||= Record.new(
42
+ identifier: multi.object_id,
43
+ created_location: caller_locations(1, 6).map(&:to_s),
44
+ closed: false
45
+ )
46
+ multi
47
+ end
48
+
49
+ Curl::Multi.class_eval do
50
+ alias_method :__leak_trace__close, :_close
51
+
52
+ def _close(*args, **kwargs, &block)
53
+ record = LeakTrace.multi_records[object_id]
54
+ record.closed = true if record
55
+
56
+ if kwargs.empty?
57
+ __leak_trace__close(*args, &block)
58
+ else
59
+ __leak_trace__close(*args, **kwargs, &block)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def multi_records
66
+ @multi_records ||= {}
67
+ end
68
+
69
+ def live_multis
70
+ ObjectSpace.each_object(Curl::Multi).to_a
71
+ end
72
+
73
+ def fixture_path
74
+ File.expand_path('helper.rb', __dir__)
75
+ end
76
+
77
+ def fixture_url
78
+ path = fixture_path.tr('\\', '/')
79
+ "file://#{path.start_with?('/') ? '' : '/'}#{path}"
80
+ end
81
+
82
+ def full_gc(compact: false)
83
+ GC.start(full_mark: true, immediate_sweep: true)
84
+ GC.compact if compact && GC.respond_to?(:compact)
85
+ GC.start(full_mark: true, immediate_sweep: true)
86
+ end
87
+
88
+ def close_multi(multi)
89
+ return unless multi
90
+
91
+ multi.instance_variable_set(:@requests, {}) if multi.respond_to?(:instance_variable_set)
92
+ multi._close
93
+ rescue StandardError
94
+ multi.close rescue nil
95
+ end
96
+
97
+ def multi_perform(iterations:, handles:, close:, compact:)
98
+ iterations.times do
99
+ multi = Curl::Multi.new
100
+ handles.times { multi.add(Curl::Easy.new(fixture_url)) }
101
+ multi.perform
102
+ ensure
103
+ close_multi(multi) if close
104
+ multi = nil
105
+ full_gc(compact: compact)
106
+ end
107
+ end
108
+
109
+ def multi_gc_cleanup(iterations:, handles:, compact:)
110
+ iterations.times do
111
+ multi = Curl::Multi.new
112
+ handles.times { multi.add(Curl::Easy.new(fixture_url)) }
113
+ multi.perform
114
+ multi = nil
115
+ full_gc(compact: compact)
116
+ end
117
+ end
118
+
119
+ def easy_perform(iterations:, compact:)
120
+ iterations.times do
121
+ easy = Curl::Easy.new(fixture_url)
122
+ easy.perform
123
+ easy.close
124
+ easy = nil
125
+ full_gc(compact: compact)
126
+ end
127
+ end
128
+
129
+ def download(iterations:, compact:)
130
+ iterations.times do |i|
131
+ Dir.mktmpdir("curb-leak-trace-#{i}") do |dir|
132
+ Curl::Easy.download(fixture_url, File.join(dir, 'helper-copy.rb'))
133
+ end
134
+ full_gc(compact: compact)
135
+ end
136
+ end
137
+
138
+ def report(io:, verbose:)
139
+ live = live_multis
140
+ tracked_live = live.filter_map { |multi| multi_records[multi.object_id] }
141
+
142
+ io.puts "scanned live Curl::Multi objects: #{live.size}"
143
+ io.puts "tracked Curl::Multi allocations: #{multi_records.size}"
144
+ io.puts "tracked Curl::Multi closes: #{multi_records.count { |_id, record| record.closed }}"
145
+
146
+ grouped = tracked_live.group_by { |record| record.created_location.first || '<unknown>' }
147
+ grouped.sort_by { |location, records| [-records.size, location] }.each do |location, records|
148
+ io.puts "live #{records.size}: #{location}"
149
+ next unless verbose
150
+
151
+ records.each do |record|
152
+ io.puts " object_id=#{record.identifier}"
153
+ record.created_location.drop(1).each do |line|
154
+ io.puts " #{line}"
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ options = {
162
+ scenario: 'multi_perform',
163
+ iterations: 25,
164
+ handles: 3,
165
+ close: true,
166
+ compact: false,
167
+ verbose: false,
168
+ fail_on_live: false
169
+ }
170
+
171
+ OptionParser.new do |opts|
172
+ opts.banner = 'Usage: ruby tests/leak_trace.rb [options]'
173
+
174
+ opts.on('--scenario NAME', 'multi_perform, multi_gc_cleanup, easy_perform, download') do |value|
175
+ options[:scenario] = value
176
+ end
177
+
178
+ opts.on('--iterations N', Integer, 'number of iterations to run') do |value|
179
+ options[:iterations] = value
180
+ end
181
+
182
+ opts.on('--handles N', Integer, 'number of easy handles per multi iteration') do |value|
183
+ options[:handles] = value
184
+ end
185
+
186
+ opts.on('--[no-]close', 'explicitly close multi handles when the scenario finishes') do |value|
187
+ options[:close] = value
188
+ end
189
+
190
+ opts.on('--compact', 'run GC.compact between iterations when available') do
191
+ options[:compact] = true
192
+ end
193
+
194
+ opts.on('--verbose', 'print full creation stacks for live multi handles') do
195
+ options[:verbose] = true
196
+ end
197
+
198
+ opts.on('--fail-on-live', 'exit non-zero when any live Curl::Multi objects remain') do
199
+ options[:fail_on_live] = true
200
+ end
201
+ end.parse!
202
+
203
+ LeakTrace.install_multi_trace!
204
+
205
+ case options[:scenario]
206
+ when 'multi_perform'
207
+ LeakTrace.multi_perform(
208
+ iterations: options[:iterations],
209
+ handles: options[:handles],
210
+ close: options[:close],
211
+ compact: options[:compact]
212
+ )
213
+ when 'multi_gc_cleanup'
214
+ LeakTrace.multi_gc_cleanup(
215
+ iterations: options[:iterations],
216
+ handles: options[:handles],
217
+ compact: options[:compact]
218
+ )
219
+ when 'easy_perform'
220
+ LeakTrace.easy_perform(
221
+ iterations: options[:iterations],
222
+ compact: options[:compact]
223
+ )
224
+ when 'download'
225
+ LeakTrace.download(
226
+ iterations: options[:iterations],
227
+ compact: options[:compact]
228
+ )
229
+ else
230
+ warn "unknown scenario: #{options[:scenario]}"
231
+ exit 64
232
+ end
233
+
234
+ LeakTrace.full_gc(compact: options[:compact])
235
+ LeakTrace.report(io: $stdout, verbose: options[:verbose])
236
+
237
+ exit 1 if options[:fail_on_live] && LeakTrace.live_multis.any?
@@ -38,10 +38,13 @@ class TestCurbCurlDownload < Test::Unit::TestCase
38
38
  reader, writer = IO.pipe
39
39
 
40
40
  # Write to local file
41
- fork do
41
+ child_pid = fork do
42
42
  begin
43
43
  writer.close
44
44
  File.open(dl_path, 'wb') { |file| file << reader.read }
45
+ exit! 0
46
+ rescue StandardError
47
+ exit! 1
45
48
  ensure
46
49
  reader.close rescue IOError # if the stream has already been closed
47
50
  end
@@ -51,7 +54,8 @@ class TestCurbCurlDownload < Test::Unit::TestCase
51
54
  begin
52
55
  reader.close
53
56
  Curl::Easy.download(dl_url, writer)
54
- Process.wait
57
+ _pid, status = Process.wait2(child_pid)
58
+ assert_predicate status, :success?
55
59
  ensure
56
60
  writer.close rescue IOError # if the stream has already been closed, which occurs in Easy::download
57
61
  end