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.
@@ -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