lazy_init 0.1.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 +7 -0
- data/.gitignore +20 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +0 -0
- data/GEMFILE +5 -0
- data/LICENSE +21 -0
- data/RAKEFILE +43 -0
- data/README.md +765 -0
- data/benchmarks/benchmark.rb +796 -0
- data/benchmarks/benchmark_performance.rb +250 -0
- data/benchmarks/benchmark_threads.rb +433 -0
- data/benchmarks/bottleneck_searcher.rb +381 -0
- data/benchmarks/thread_safety_verification.rb +376 -0
- data/lazy_init.gemspec +40 -0
- data/lib/lazy_init/class_methods.rb +549 -0
- data/lib/lazy_init/configuration.rb +57 -0
- data/lib/lazy_init/dependency_resolver.rb +226 -0
- data/lib/lazy_init/errors.rb +23 -0
- data/lib/lazy_init/instance_methods.rb +291 -0
- data/lib/lazy_init/lazy_value.rb +167 -0
- data/lib/lazy_init/version.rb +5 -0
- data/lib/lazy_init.rb +47 -0
- metadata +140 -0
@@ -0,0 +1,376 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../lib/lazy_init'
|
5
|
+
|
6
|
+
puts '=== LazyInit Fixed Thread Safety Verification ==='
|
7
|
+
puts "Ruby version: #{RUBY_VERSION}"
|
8
|
+
puts "Platform: #{RUBY_PLATFORM}"
|
9
|
+
|
10
|
+
# Test configuration
|
11
|
+
THREAD_COUNTS = [2, 4, 8, 16, 32, 50, 100]
|
12
|
+
|
13
|
+
class ThreadSafetyTester
|
14
|
+
def initialize(description)
|
15
|
+
@description = description
|
16
|
+
@results = []
|
17
|
+
@failures = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def test(thread_count)
|
21
|
+
puts "\n--- Testing #{@description} with #{thread_count} threads ---"
|
22
|
+
|
23
|
+
results = []
|
24
|
+
exceptions = []
|
25
|
+
start_time = Time.now
|
26
|
+
|
27
|
+
# Improved synchronization
|
28
|
+
barrier = Mutex.new
|
29
|
+
ready_count = 0
|
30
|
+
ready_condition = ConditionVariable.new
|
31
|
+
|
32
|
+
threads = thread_count.times.map do |i|
|
33
|
+
Thread.new do
|
34
|
+
barrier.synchronize do
|
35
|
+
ready_count += 1
|
36
|
+
if ready_count == thread_count
|
37
|
+
ready_condition.broadcast
|
38
|
+
else
|
39
|
+
ready_condition.wait(barrier)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
result = yield(i)
|
45
|
+
results << result
|
46
|
+
rescue StandardError => e
|
47
|
+
exceptions << { thread: i, error: e }
|
48
|
+
results << :error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
threads.each(&:join)
|
54
|
+
end_time = Time.now
|
55
|
+
|
56
|
+
analyze_results(thread_count, results, exceptions, end_time - start_time)
|
57
|
+
end
|
58
|
+
|
59
|
+
def summary
|
60
|
+
puts "\n=== #{@description} SUMMARY ==="
|
61
|
+
puts "Total tests: #{@results.size}"
|
62
|
+
successes = @results.count { |r| r[:success] }
|
63
|
+
puts "Successful: #{successes}/#{@results.size}"
|
64
|
+
|
65
|
+
if @failures.any?
|
66
|
+
puts "\nFAILURES DETECTED:"
|
67
|
+
@failures.each do |failure|
|
68
|
+
puts " #{failure[:thread_count]} threads: #{failure[:unique_results]} unique results"
|
69
|
+
puts " Expected: 1, Got: #{failure[:unique_results]}"
|
70
|
+
puts " Sample results: #{failure[:results_sample]}"
|
71
|
+
end
|
72
|
+
false
|
73
|
+
else
|
74
|
+
puts '✓ ALL TESTS PASSED - THREAD SAFE'
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def analyze_results(thread_count, results, exceptions, duration)
|
82
|
+
valid_results = results.reject { |r| r == :error }
|
83
|
+
unique_results = valid_results.uniq
|
84
|
+
|
85
|
+
puts " Threads: #{thread_count}"
|
86
|
+
puts " Duration: #{'%.3f' % duration}s"
|
87
|
+
puts " Results: #{results.size} total, #{valid_results.size} valid"
|
88
|
+
puts " Unique results: #{unique_results.size}"
|
89
|
+
puts " Exceptions: #{exceptions.size}"
|
90
|
+
|
91
|
+
if exceptions.any?
|
92
|
+
puts ' EXCEPTIONS:'
|
93
|
+
exceptions.first(3).each { |ex| puts " Thread #{ex[:thread]}: #{ex[:error].message}" }
|
94
|
+
end
|
95
|
+
|
96
|
+
success = unique_results.size == 1 && exceptions.empty?
|
97
|
+
puts " STATUS: #{success ? '✓ THREAD-SAFE' : '✗ RACE CONDITION DETECTED'}"
|
98
|
+
|
99
|
+
unless success
|
100
|
+
@failures << {
|
101
|
+
thread_count: thread_count,
|
102
|
+
unique_results: unique_results.size,
|
103
|
+
exceptions: exceptions.size,
|
104
|
+
results_sample: unique_results.first(3)
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
@results << {
|
109
|
+
thread_count: thread_count,
|
110
|
+
success: success,
|
111
|
+
unique_results: unique_results.size,
|
112
|
+
duration: duration,
|
113
|
+
exceptions: exceptions.size
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# === 1. Fixed Basic lazy_attr_reader Test ===
|
119
|
+
puts "\n" + '=' * 60
|
120
|
+
puts '1. FIXED BASIC LAZY_ATTR_READER THREAD SAFETY'
|
121
|
+
puts '=' * 60
|
122
|
+
|
123
|
+
basic_tester = ThreadSafetyTester.new('Fixed basic lazy_attr_reader')
|
124
|
+
|
125
|
+
THREAD_COUNTS.each do |thread_count|
|
126
|
+
test_class = Class.new do
|
127
|
+
extend LazyInit
|
128
|
+
|
129
|
+
lazy_attr_reader :shared_computation do
|
130
|
+
sleep(0.001) # Simulate potential race condition
|
131
|
+
computation_id = Time.now.to_f * 1_000_000
|
132
|
+
"result_#{computation_id.to_i}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
instance = test_class.new
|
137
|
+
|
138
|
+
basic_tester.test(thread_count) do |_thread_id|
|
139
|
+
instance.shared_computation
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
basic_success = basic_tester.summary
|
144
|
+
|
145
|
+
# === 2. Class Variable Thread Safety ===
|
146
|
+
puts "\n" + '=' * 60
|
147
|
+
puts '2. CLASS VARIABLE THREAD SAFETY'
|
148
|
+
puts '=' * 60
|
149
|
+
|
150
|
+
class_var_tester = ThreadSafetyTester.new('Class variable lazy_class_variable')
|
151
|
+
|
152
|
+
THREAD_COUNTS.each do |thread_count|
|
153
|
+
test_class = Class.new do
|
154
|
+
extend LazyInit
|
155
|
+
|
156
|
+
lazy_class_variable :shared_resource do
|
157
|
+
sleep(rand * 0.01) # Race condition opportunity
|
158
|
+
resource_id = Time.now.to_f * 1_000_000
|
159
|
+
"shared_resource_#{resource_id.to_i}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class_var_tester.test(thread_count) do |thread_id|
|
164
|
+
# Mix of class and instance access
|
165
|
+
if thread_id.even?
|
166
|
+
test_class.shared_resource
|
167
|
+
else
|
168
|
+
test_class.new.shared_resource
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class_var_success = class_var_tester.summary
|
174
|
+
|
175
|
+
# === 3. Dependency Injection Thread Safety ===
|
176
|
+
puts "\n" + '=' * 60
|
177
|
+
puts '3. DEPENDENCY INJECTION THREAD SAFETY'
|
178
|
+
puts '=' * 60
|
179
|
+
|
180
|
+
dependency_tester = ThreadSafetyTester.new('Dependency injection')
|
181
|
+
|
182
|
+
THREAD_COUNTS.each do |thread_count|
|
183
|
+
test_class = Class.new do
|
184
|
+
extend LazyInit
|
185
|
+
|
186
|
+
lazy_attr_reader :base_config do
|
187
|
+
sleep(rand * 0.005)
|
188
|
+
config_id = Time.now.to_f * 1_000_000
|
189
|
+
{ id: config_id.to_i, url: "http://api-#{config_id.to_i}.com" }
|
190
|
+
end
|
191
|
+
|
192
|
+
lazy_attr_reader :database, depends_on: [:base_config] do
|
193
|
+
sleep(rand * 0.005)
|
194
|
+
"db_connection_#{base_config[:id]}"
|
195
|
+
end
|
196
|
+
|
197
|
+
lazy_attr_reader :api_client, depends_on: %i[base_config database] do
|
198
|
+
sleep(rand * 0.005)
|
199
|
+
"api_client_#{base_config[:id]}_#{database.split('_').last}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
instance = test_class.new
|
204
|
+
|
205
|
+
dependency_tester.test(thread_count) do |_thread_id|
|
206
|
+
# All threads try to access the final dependent value
|
207
|
+
instance.api_client
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
dependency_success = dependency_tester.summary
|
212
|
+
|
213
|
+
# === 4. Exception Handling Thread Safety ===
|
214
|
+
puts "\n" + '=' * 60
|
215
|
+
puts '4. EXCEPTION HANDLING THREAD SAFETY'
|
216
|
+
puts '=' * 60
|
217
|
+
|
218
|
+
exception_tester = ThreadSafetyTester.new('Exception handling')
|
219
|
+
|
220
|
+
THREAD_COUNTS.each do |thread_count|
|
221
|
+
test_class = Class.new do
|
222
|
+
extend LazyInit
|
223
|
+
|
224
|
+
lazy_attr_reader :failing_operation do
|
225
|
+
sleep(rand * 0.01)
|
226
|
+
raise StandardError, "Intentional failure #{Time.now.to_f}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
instance = test_class.new
|
231
|
+
|
232
|
+
exception_tester.test(thread_count) do |_thread_id|
|
233
|
+
instance.failing_operation
|
234
|
+
'unexpected_success'
|
235
|
+
rescue StandardError => e
|
236
|
+
e.message # Should be the same message for all threads
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
exception_success = exception_tester.summary
|
241
|
+
|
242
|
+
# === 5. Stress Test - Mixed Operations ===
|
243
|
+
puts "\n" + '=' * 60
|
244
|
+
puts '5. STRESS TEST - MIXED OPERATIONS'
|
245
|
+
puts '=' * 60
|
246
|
+
|
247
|
+
stress_tester = ThreadSafetyTester.new('Mixed operations stress test')
|
248
|
+
|
249
|
+
# Single stress test with maximum thread count
|
250
|
+
test_class = Class.new do
|
251
|
+
extend LazyInit
|
252
|
+
|
253
|
+
lazy_attr_reader :config do
|
254
|
+
sleep(rand * 0.02)
|
255
|
+
base_id = Time.now.to_f * 1_000_000
|
256
|
+
{
|
257
|
+
id: base_id.to_i,
|
258
|
+
timestamp: Time.now.to_f,
|
259
|
+
random: rand(1_000_000)
|
260
|
+
}
|
261
|
+
end
|
262
|
+
|
263
|
+
lazy_attr_reader :service_a, depends_on: [:config] do
|
264
|
+
sleep(rand * 0.01)
|
265
|
+
"service_a_#{config[:id]}"
|
266
|
+
end
|
267
|
+
|
268
|
+
lazy_attr_reader :service_b, depends_on: [:config] do
|
269
|
+
sleep(rand * 0.01)
|
270
|
+
"service_b_#{config[:id]}"
|
271
|
+
end
|
272
|
+
|
273
|
+
lazy_attr_reader :combined, depends_on: %i[service_a service_b] do
|
274
|
+
sleep(rand * 0.01)
|
275
|
+
"combined_#{service_a}_#{service_b}_#{config[:random]}"
|
276
|
+
end
|
277
|
+
|
278
|
+
lazy_class_variable :global_state do
|
279
|
+
sleep(rand * 0.02)
|
280
|
+
state_id = Time.now.to_f * 1_000_000
|
281
|
+
"global_#{state_id.to_i}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Test with multiple instances and high thread count
|
286
|
+
# instances = Array.new(10) { test_class.new }
|
287
|
+
|
288
|
+
shared_instance = test_class.new # Single shared instance
|
289
|
+
|
290
|
+
stress_tester.test(100) do |thread_id|
|
291
|
+
operation = thread_id % 4
|
292
|
+
|
293
|
+
case operation
|
294
|
+
when 0
|
295
|
+
shared_instance.config # All threads same instance
|
296
|
+
when 1
|
297
|
+
shared_instance.combined # All threads same instance
|
298
|
+
when 2
|
299
|
+
test_class.global_state # Class level - already correct
|
300
|
+
when 3
|
301
|
+
shared_instance.service_a # All threads same instance
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
stress_success = stress_tester.summary
|
306
|
+
|
307
|
+
puts "\n" + '=' * 60
|
308
|
+
puts '6. LAZY_ONCE THREAD SAFETY'
|
309
|
+
puts '=' * 60
|
310
|
+
|
311
|
+
lazy_once_tester = ThreadSafetyTester.new('lazy_once')
|
312
|
+
|
313
|
+
test_class = Class.new do
|
314
|
+
include LazyInit
|
315
|
+
|
316
|
+
def shared_computation
|
317
|
+
lazy_once do
|
318
|
+
sleep(rand * 0.01)
|
319
|
+
computation_id = Time.now.to_f * 1_000_000
|
320
|
+
"lazy_once_#{computation_id.to_i}"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
THREAD_COUNTS.each do |thread_count|
|
326
|
+
instance = test_class.new
|
327
|
+
|
328
|
+
lazy_once_tester.test(thread_count) do |_thread_id|
|
329
|
+
instance.shared_computation
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
lazy_once_success = lazy_once_tester.summary
|
334
|
+
|
335
|
+
# === FINAL VERIFICATION REPORT ===
|
336
|
+
puts "\n" + '=' * 80
|
337
|
+
puts 'FINAL THREAD SAFETY VERIFICATION REPORT'
|
338
|
+
puts '=' * 80
|
339
|
+
|
340
|
+
all_tests = [
|
341
|
+
['Basic lazy_attr_reader', basic_success],
|
342
|
+
['Class variables', class_var_success],
|
343
|
+
['Dependency injection', dependency_success],
|
344
|
+
['Exception handling', exception_success],
|
345
|
+
['Stress test', stress_success],
|
346
|
+
['lazy_once', lazy_once_success]
|
347
|
+
]
|
348
|
+
|
349
|
+
passed_tests = all_tests.count { |_, success| success }
|
350
|
+
total_tests = all_tests.size
|
351
|
+
|
352
|
+
puts 'SUMMARY:'
|
353
|
+
puts " Passed: #{passed_tests}/#{total_tests} test categories"
|
354
|
+
puts " Thread counts tested: #{THREAD_COUNTS.join(', ')}"
|
355
|
+
puts " Maximum threads: #{THREAD_COUNTS.max}"
|
356
|
+
puts " Ruby version: #{RUBY_VERSION}"
|
357
|
+
|
358
|
+
puts "\nDETAILS:"
|
359
|
+
all_tests.each do |name, success|
|
360
|
+
status = success ? '✓ PASS' : '✗ FAIL'
|
361
|
+
puts " #{status} #{name}"
|
362
|
+
end
|
363
|
+
|
364
|
+
overall_success = passed_tests == total_tests
|
365
|
+
|
366
|
+
puts "\n" + '=' * 40
|
367
|
+
if overall_success
|
368
|
+
puts '🎉 ALL THREAD SAFETY TESTS PASSED!'
|
369
|
+
puts 'LazyInit gem is THREAD-SAFE across all features'
|
370
|
+
else
|
371
|
+
puts '⚠️ THREAD SAFETY ISSUES DETECTED!'
|
372
|
+
puts 'Some features may have race conditions'
|
373
|
+
end
|
374
|
+
puts '=' * 40
|
375
|
+
|
376
|
+
exit(overall_success ? 0 : 1)
|
data/lazy_init.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/lazy_init/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'lazy_init'
|
7
|
+
spec.version = LazyInit::VERSION
|
8
|
+
spec.authors = ['Konstanty Koszewski']
|
9
|
+
spec.email = ['ka.koszewski@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Thread-safe lazy initialization patterns for Ruby'
|
12
|
+
spec.description = 'Provides thread-safe lazy initialization with clean, Ruby-idiomatic API. ' \
|
13
|
+
'Eliminates race conditions in lazy attribute initialization while maintaining performance.'
|
14
|
+
spec.homepage = 'https://github.com/N3BCKN/lazy_init'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/N3BCKN/lazy_init'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/N3BCKN/lazy_init/blob/main/CHANGELOG.md'
|
20
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/N3BCKN/lazy_init/issues'
|
21
|
+
spec.metadata['documentation_uri'] = 'https://rubydoc.info/gems/lazy_init'
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
26
|
+
end
|
27
|
+
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.required_ruby_version = '>= 2.6'
|
33
|
+
|
34
|
+
# Development dependencies
|
35
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
36
|
+
spec.add_development_dependency 'benchmark-ips', '~> 2.10'
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 1.50.2'
|
38
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
39
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
40
|
+
end
|