lazy_init 0.1.2 → 0.2.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/CHANGELOG.md +18 -3
- data/README.md +26 -8
- data/benchmarks/benchmark.rb +616 -145
- data/benchmarks/benchmark_performance.rb +3 -66
- data/benchmarks/benchmark_threads.rb +83 -89
- data/lazy_init.gemspec +1 -0
- data/lib/lazy_init/class_methods.rb +565 -242
- data/lib/lazy_init/instance_methods.rb +90 -66
- data/lib/lazy_init/ruby_capabilities.rb +39 -0
- data/lib/lazy_init/version.rb +1 -1
- data/lib/lazy_init.rb +1 -0
- metadata +5 -3
@@ -1,17 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module LazyInit
|
4
|
-
#
|
4
|
+
# Class-level methods for defining lazy attributes with Ruby version-specific optimizations.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# The module generates three methods for each lazy attribute:
|
12
|
-
# - `attribute_name` - the main accessor method
|
13
|
-
# - `attribute_name_computed?` - predicate to check computation state
|
14
|
-
# - `reset_attribute_name!` - method to reset and allow recomputation
|
6
|
+
# Automatically selects the most efficient implementation:
|
7
|
+
# - Ruby 3+: eval-based methods for maximum performance
|
8
|
+
# - Ruby 2.6+: define_method with full compatibility
|
9
|
+
# - Simple cases: inline variables, dependency cases: lightweight resolution
|
10
|
+
# - Complex cases: full LazyValue with timeout and dependency support
|
15
11
|
#
|
16
12
|
# @example Basic lazy attribute
|
17
13
|
# class ApiClient
|
@@ -22,52 +18,30 @@ module LazyInit
|
|
22
18
|
# end
|
23
19
|
# end
|
24
20
|
#
|
25
|
-
# @example
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# lazy_attr_reader :config do
|
30
|
-
# load_configuration
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# lazy_attr_reader :connection, depends_on: [:config] do
|
34
|
-
# Database.connect(config.database_url)
|
35
|
-
# end
|
21
|
+
# @example With dependencies
|
22
|
+
# lazy_attr_reader :database, depends_on: [:config] do
|
23
|
+
# Database.connect(config.database_url)
|
36
24
|
# end
|
37
25
|
#
|
38
26
|
# @since 0.1.0
|
39
27
|
module ClassMethods
|
40
28
|
# Set up necessary infrastructure when LazyInit is extended by a class.
|
41
29
|
#
|
42
|
-
# This is an internal Ruby hook method that's automatically called when a class
|
43
|
-
# extends LazyInit. Users should never call this method directly - it's part of
|
44
|
-
# Ruby's module extension mechanism.
|
45
|
-
#
|
46
|
-
# Initializes thread-safe mutex and dependency resolver for the target class.
|
47
|
-
# This ensures each class has its own isolated dependency management.
|
48
|
-
#
|
49
30
|
# @param base [Class] the class being extended with LazyInit
|
50
31
|
# @return [void]
|
51
32
|
# @api private
|
52
33
|
def self.extended(base)
|
53
34
|
base.instance_variable_set(:@lazy_init_class_mutex, Mutex.new)
|
54
|
-
base.instance_variable_set(:@dependency_resolver, DependencyResolver.new(base))
|
55
35
|
end
|
56
36
|
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# Used internally for introspection and debugging. Each entry contains
|
60
|
-
# the configuration (block, timeout, dependencies) for a lazy attribute.
|
37
|
+
# Registry of all lazy initializers defined on this class.
|
61
38
|
#
|
62
39
|
# @return [Hash<Symbol, Hash>] mapping of attribute names to their configuration
|
63
40
|
def lazy_initializers
|
64
41
|
@lazy_initializers ||= {}
|
65
42
|
end
|
66
43
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
# Handles dependency graph management and resolution order computation.
|
70
|
-
# Creates a new resolver if one doesn't exist.
|
44
|
+
# Lazy dependency resolver - created only when needed for performance.
|
71
45
|
#
|
72
46
|
# @return [DependencyResolver] the resolver instance for this class
|
73
47
|
def dependency_resolver
|
@@ -76,10 +50,11 @@ module LazyInit
|
|
76
50
|
|
77
51
|
# Define a thread-safe lazy-initialized instance attribute.
|
78
52
|
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
53
|
+
# Automatically optimizes based on Ruby version and complexity:
|
54
|
+
# - Ruby 3+: uses eval for maximum performance
|
55
|
+
# - Simple cases: direct instance variables
|
56
|
+
# - Dependencies: lightweight resolution for single deps, full resolver for complex
|
57
|
+
# - Timeouts: full LazyValue wrapper
|
83
58
|
#
|
84
59
|
# @param name [Symbol, String] the attribute name
|
85
60
|
# @param timeout [Numeric, nil] timeout in seconds for the computation
|
@@ -107,7 +82,7 @@ module LazyInit
|
|
107
82
|
validate_attribute_name!(name)
|
108
83
|
raise ArgumentError, 'Block is required' unless block
|
109
84
|
|
110
|
-
# store configuration for introspection
|
85
|
+
# store configuration for introspection
|
111
86
|
config = {
|
112
87
|
block: block,
|
113
88
|
timeout: timeout || LazyInit.configuration.default_timeout,
|
@@ -118,27 +93,36 @@ module LazyInit
|
|
118
93
|
# register dependencies with resolver if present
|
119
94
|
dependency_resolver.add_dependency(name, depends_on) if depends_on
|
120
95
|
|
121
|
-
# select optimal implementation strategy
|
122
|
-
if
|
96
|
+
# select optimal implementation strategy
|
97
|
+
if depends_on && Array(depends_on).size == 1 && !timeout
|
98
|
+
generate_simple_dependency_with_inline_check(name, Array(depends_on).first, block)
|
99
|
+
generate_predicate_method(name)
|
100
|
+
generate_reset_method(name)
|
101
|
+
elsif depends_on && Array(depends_on).size > 1 && !timeout
|
102
|
+
generate_fast_dependency_method(name, depends_on, block, config)
|
103
|
+
generate_predicate_method(name)
|
104
|
+
generate_reset_method_with_deps_flag(name)
|
105
|
+
elsif simple_case_eligible?(timeout, depends_on)
|
106
|
+
generate_optimized_simple_method(name, block)
|
107
|
+
elsif enhanced_simple_case?(timeout, depends_on)
|
123
108
|
if simple_dependency_case?(depends_on)
|
124
|
-
|
109
|
+
generate_lazy_compiling_method(name, block, :dependency, depends_on)
|
125
110
|
else
|
126
|
-
|
111
|
+
generate_lazy_compiling_method(name, block, :simple, nil)
|
127
112
|
end
|
113
|
+
generate_predicate_method(name)
|
114
|
+
generate_reset_method(name)
|
128
115
|
else
|
129
116
|
generate_complex_lazyvalue_method(name, config)
|
117
|
+
generate_predicate_method(name)
|
118
|
+
generate_reset_method(name)
|
130
119
|
end
|
131
|
-
|
132
|
-
# generate helper methods for all implementation types
|
133
|
-
generate_predicate_method(name)
|
134
|
-
generate_reset_method(name)
|
135
120
|
end
|
136
121
|
|
137
122
|
# Define a thread-safe lazy-initialized class variable shared across all instances.
|
138
123
|
#
|
139
|
-
#
|
140
|
-
# All instances share the same computed value.
|
141
|
-
# implemented using LazyValue for full thread safety and feature support.
|
124
|
+
# Uses full LazyValue wrapper for thread safety and feature completeness.
|
125
|
+
# All instances share the same computed value.
|
142
126
|
#
|
143
127
|
# @param name [Symbol, String] the class variable name
|
144
128
|
# @param timeout [Numeric, nil] timeout in seconds for the computation
|
@@ -158,7 +142,7 @@ module LazyInit
|
|
158
142
|
|
159
143
|
class_variable_name = "@@#{name}_lazy_value"
|
160
144
|
|
161
|
-
# register dependencies for class-level attributes
|
145
|
+
# register dependencies for class-level attributes
|
162
146
|
dependency_resolver.add_dependency(name, depends_on) if depends_on
|
163
147
|
|
164
148
|
# cache configuration for use in generated methods
|
@@ -215,20 +199,43 @@ module LazyInit
|
|
215
199
|
|
216
200
|
private
|
217
201
|
|
218
|
-
#
|
202
|
+
# Generate optimized methods based on dependency type and Ruby version.
|
219
203
|
#
|
220
|
-
#
|
221
|
-
#
|
222
|
-
#
|
204
|
+
# @param name [Symbol] the attribute name
|
205
|
+
# @param block [Proc] the computation block
|
206
|
+
# @param dependency_type [Symbol] :simple or :dependency
|
207
|
+
# @param depends_on [Array<Symbol>, Symbol, nil] dependencies for :dependency type
|
208
|
+
# @return [void]
|
209
|
+
# @api private
|
210
|
+
def generate_lazy_compiling_method(name, block, dependency_type = :simple, depends_on = nil)
|
211
|
+
case dependency_type
|
212
|
+
when :dependency
|
213
|
+
if depends_on && Array(depends_on).size == 1
|
214
|
+
# single dependency: fast path with lightweight resolution
|
215
|
+
generate_simple_dependency_with_resolution(name, depends_on, block)
|
216
|
+
else
|
217
|
+
# complex dependency: full LazyValue with complex resolution
|
218
|
+
config = { block: block, timeout: nil, depends_on: depends_on }
|
219
|
+
generate_complex_lazyvalue_method(name, config)
|
220
|
+
end
|
221
|
+
when :simple
|
222
|
+
# no dependencies: fastest path
|
223
|
+
if LazyInit::RubyCapabilities::IMPROVED_EVAL_PERFORMANCE
|
224
|
+
generate_simple_inline_method_with_eval(name, block)
|
225
|
+
else
|
226
|
+
generate_simple_inline_method_with_define_method(name, block)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Check if attribute qualifies for simple optimization (no timeout, simple dependencies).
|
223
232
|
#
|
224
233
|
# @param timeout [Object] timeout configuration
|
225
234
|
# @param depends_on [Object] dependency configuration
|
226
235
|
# @return [Boolean] true if simple implementation should be used
|
227
236
|
def enhanced_simple_case?(timeout, depends_on)
|
228
|
-
# timeout requires LazyValue for proper handling
|
229
237
|
return false unless timeout.nil?
|
230
238
|
|
231
|
-
# categorize dependency complexity
|
232
239
|
case depends_on
|
233
240
|
when nil, []
|
234
241
|
true # no dependencies are always simple
|
@@ -243,150 +250,196 @@ module LazyInit
|
|
243
250
|
|
244
251
|
# Check if dependencies qualify for simple dependency optimization.
|
245
252
|
#
|
246
|
-
# Single dependencies can use an optimized resolution strategy that
|
247
|
-
# avoids the full dependency resolver overhead.
|
248
|
-
#
|
249
253
|
# @param depends_on [Object] dependency configuration
|
250
254
|
# @return [Boolean] true if simple dependency method should be used
|
251
255
|
def simple_dependency_case?(depends_on)
|
252
256
|
return false if depends_on.nil? || depends_on.empty?
|
253
257
|
|
254
|
-
|
255
|
-
deps.size == 1 # any single dependency qualifies for optimization
|
258
|
+
Array(depends_on).size == 1
|
256
259
|
end
|
257
260
|
|
258
|
-
# Generate
|
259
|
-
#
|
260
|
-
# This creates a method that uses inline variables for storage and
|
261
|
-
# optimized dependency resolution that avoids LazyValue overhead.
|
262
|
-
# Includes circular dependency detection and thread-safe error caching.
|
261
|
+
# Generate full LazyValue method for complex scenarios (timeout, multiple dependencies).
|
263
262
|
#
|
264
263
|
# @param name [Symbol] the attribute name
|
265
|
-
# @param
|
266
|
-
# @param block [Proc] the computation block
|
264
|
+
# @param config [Hash] the attribute configuration
|
267
265
|
# @return [void]
|
268
|
-
def
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
exception_var = "@#{name}_exception"
|
273
|
-
|
274
|
-
# cache references to avoid repeated lookups in generated method
|
275
|
-
cached_block = block
|
276
|
-
cached_dep_name = dep_name
|
266
|
+
def generate_complex_lazyvalue_method(name, config)
|
267
|
+
cached_timeout = config[:timeout]
|
268
|
+
cached_depends_on = config[:depends_on]
|
269
|
+
cached_block = config[:block]
|
277
270
|
|
278
271
|
define_method(name) do
|
279
|
-
#
|
280
|
-
if
|
281
|
-
stored_exception = instance_variable_get(exception_var)
|
282
|
-
raise stored_exception if stored_exception
|
272
|
+
# resolve dependencies using full dependency resolver
|
273
|
+
self.class.dependency_resolver.resolve_dependencies(name, self) if cached_depends_on
|
283
274
|
|
284
|
-
|
275
|
+
# lazy creation of LazyValue wrapper
|
276
|
+
ivar_name = "@#{name}_lazy_value"
|
277
|
+
lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
|
278
|
+
|
279
|
+
unless lazy_value
|
280
|
+
lazy_value = LazyValue.new(timeout: cached_timeout) do
|
281
|
+
instance_eval(&cached_block)
|
282
|
+
end
|
283
|
+
instance_variable_set(ivar_name, lazy_value)
|
285
284
|
end
|
286
285
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
circular_error = LazyInit::DependencyError.new(
|
291
|
-
"Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
|
292
|
-
)
|
286
|
+
lazy_value.value
|
287
|
+
end
|
288
|
+
end
|
293
289
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
290
|
+
# Generate predicate method to check computation state.
|
291
|
+
# Handles both simple (inline variables) and complex (LazyValue) cases.
|
292
|
+
#
|
293
|
+
# @param name [Symbol] the attribute name
|
294
|
+
# @return [void]
|
295
|
+
def generate_predicate_method(name)
|
296
|
+
define_method("#{name}_computed?") do
|
297
|
+
# check simple implementation first
|
298
|
+
computed_var = "@#{name}_computed"
|
299
|
+
exception_var = "@#{name}_exception"
|
299
300
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
301
|
+
if instance_variable_defined?(computed_var)
|
302
|
+
# simple implementation: computed but not if there's a cached exception
|
303
|
+
return instance_variable_get(computed_var) && !instance_variable_get(exception_var)
|
304
|
+
end
|
304
305
|
|
305
|
-
|
306
|
+
# check complex implementation (LazyValue wrapper)
|
307
|
+
lazy_var = "@#{name}_lazy_value"
|
308
|
+
if instance_variable_defined?(lazy_var)
|
309
|
+
lazy_value = instance_variable_get(lazy_var)
|
310
|
+
return lazy_value&.computed? || false
|
306
311
|
end
|
307
312
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
+
false
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Generate reset method to clear computed state and allow recomputation.
|
318
|
+
# Handles both simple and complex implementations.
|
319
|
+
#
|
320
|
+
# @param name [Symbol] the attribute name
|
321
|
+
# @return [void]
|
322
|
+
def generate_reset_method(name)
|
323
|
+
define_method("reset_#{name}!") do
|
324
|
+
# handle simple implementation reset
|
325
|
+
computed_var = "@#{name}_computed"
|
326
|
+
value_var = "@#{name}_value"
|
327
|
+
exception_var = "@#{name}_exception"
|
328
|
+
|
329
|
+
if instance_variable_defined?(computed_var)
|
330
|
+
# use mutex if available for thread safety
|
331
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
332
|
+
if mutex
|
333
|
+
mutex.synchronize do
|
334
|
+
instance_variable_set(computed_var, false)
|
335
|
+
remove_instance_variable(value_var) if instance_variable_defined?(value_var)
|
336
|
+
remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
|
337
|
+
end
|
338
|
+
else
|
339
|
+
instance_variable_set(computed_var, false)
|
340
|
+
remove_instance_variable(value_var) if instance_variable_defined?(value_var)
|
341
|
+
remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
|
342
|
+
end
|
343
|
+
return
|
313
344
|
end
|
314
345
|
|
315
|
-
#
|
316
|
-
|
346
|
+
# handle complex implementation reset (LazyValue)
|
347
|
+
lazy_var = "@#{name}_lazy_value"
|
348
|
+
if instance_variable_defined?(lazy_var)
|
349
|
+
lazy_value = instance_variable_get(lazy_var)
|
350
|
+
lazy_value&.reset!
|
351
|
+
remove_instance_variable(lazy_var)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Validate attribute name follows Ruby conventions.
|
357
|
+
#
|
358
|
+
# @param name [Object] the proposed attribute name
|
359
|
+
# @return [void]
|
360
|
+
# @raise [InvalidAttributeNameError] if the name is invalid
|
361
|
+
def validate_attribute_name!(name)
|
362
|
+
raise InvalidAttributeNameError, 'Attribute name cannot be nil' if name.nil?
|
363
|
+
raise InvalidAttributeNameError, 'Attribute name cannot be empty' if name.to_s.strip.empty?
|
364
|
+
|
365
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
366
|
+
raise InvalidAttributeNameError, 'Attribute name must be a symbol or string'
|
367
|
+
end
|
368
|
+
|
369
|
+
name_str = name.to_s
|
370
|
+
return if name_str.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
|
371
|
+
|
372
|
+
raise InvalidAttributeNameError, "Invalid attribute name: #{name_str}"
|
373
|
+
end
|
374
|
+
|
375
|
+
# Ruby 3+ eval-based method generation for simple methods (no dependencies).
|
376
|
+
# Optimized for maximum performance with minimal overhead.
|
377
|
+
#
|
378
|
+
# @param name [Symbol] attribute name
|
379
|
+
# @param block [Proc] computation block
|
380
|
+
# @return [void]
|
381
|
+
def generate_simple_inline_method_with_eval(name, block)
|
382
|
+
block_var = "@@lazy_#{name}_block_#{object_id}"
|
383
|
+
class_variable_set(block_var, block)
|
384
|
+
|
385
|
+
method_code = <<~RUBY
|
386
|
+
def #{name}
|
387
|
+
# fast path: return cached value immediately if available
|
388
|
+
return @#{name}_value if @#{name}_computed
|
389
|
+
|
390
|
+
# shared mutex for thread safety - avoid per-method mutex overhead
|
391
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
392
|
+
unless mutex
|
393
|
+
mutex = Mutex.new
|
394
|
+
self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
|
395
|
+
end
|
317
396
|
|
318
|
-
begin
|
319
397
|
mutex.synchronize do
|
320
|
-
# double-check pattern
|
321
|
-
if
|
322
|
-
stored_exception =
|
398
|
+
# double-check pattern: another thread might have computed while we waited
|
399
|
+
if @#{name}_computed
|
400
|
+
stored_exception = @#{name}_exception
|
323
401
|
raise stored_exception if stored_exception
|
324
|
-
|
325
|
-
return instance_variable_get(value_var)
|
402
|
+
return @#{name}_value
|
326
403
|
end
|
327
404
|
|
328
405
|
begin
|
329
|
-
#
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
send(cached_dep_name) # uses same shared resolution_stack for circular detection
|
335
|
-
ensure
|
336
|
-
mutex.lock
|
337
|
-
end
|
338
|
-
|
339
|
-
# check if we got computed while lock was released
|
340
|
-
if instance_variable_get(computed_var)
|
341
|
-
stored_exception = instance_variable_get(exception_var)
|
342
|
-
raise stored_exception if stored_exception
|
343
|
-
|
344
|
-
return instance_variable_get(value_var)
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
|
-
# perform the actual computation with dependency available
|
349
|
-
result = instance_eval(&cached_block)
|
350
|
-
instance_variable_set(value_var, result)
|
351
|
-
instance_variable_set(computed_var, true)
|
406
|
+
# perform computation and cache result
|
407
|
+
block = self.class.class_variable_get(:#{block_var})
|
408
|
+
result = instance_eval(&block)
|
409
|
+
@#{name}_value = result
|
410
|
+
@#{name}_computed = true
|
352
411
|
result
|
353
412
|
rescue StandardError => e
|
354
|
-
# cache exceptions
|
355
|
-
|
356
|
-
|
413
|
+
# cache exceptions to ensure consistent error behavior
|
414
|
+
@#{name}_exception = e
|
415
|
+
@#{name}_computed = true
|
357
416
|
raise
|
358
417
|
end
|
359
418
|
end
|
360
|
-
ensure
|
361
|
-
# always clean up resolution stack to prevent leaks
|
362
|
-
resolution_stack.pop
|
363
|
-
Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
|
364
419
|
end
|
365
|
-
|
420
|
+
RUBY
|
421
|
+
|
422
|
+
class_eval(method_code)
|
366
423
|
end
|
367
424
|
|
368
|
-
#
|
425
|
+
# Ruby 2.6+ fallback using define_method for simple methods.
|
426
|
+
# Compatible version of the eval-based simple method.
|
369
427
|
#
|
370
|
-
#
|
371
|
-
#
|
372
|
-
# implementation strategy available.
|
373
|
-
#
|
374
|
-
# @param name [Symbol] the attribute name
|
375
|
-
# @param block [Proc] the computation block
|
428
|
+
# @param name [Symbol] attribute name
|
429
|
+
# @param block [Proc] computation block
|
376
430
|
# @return [void]
|
377
|
-
def
|
431
|
+
def generate_simple_inline_method_with_define_method(name, block)
|
378
432
|
computed_var = "@#{name}_computed"
|
379
433
|
value_var = "@#{name}_value"
|
380
434
|
exception_var = "@#{name}_exception"
|
381
435
|
|
382
|
-
# cache block reference to avoid lookup in generated method
|
383
436
|
cached_block = block
|
384
437
|
|
385
438
|
define_method(name) do
|
386
439
|
# fast path: return cached value immediately if available
|
387
440
|
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
388
441
|
|
389
|
-
#
|
442
|
+
# shared mutex for thread safety
|
390
443
|
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
391
444
|
unless mutex
|
392
445
|
mutex = Mutex.new
|
@@ -418,132 +471,402 @@ module LazyInit
|
|
418
471
|
end
|
419
472
|
end
|
420
473
|
|
421
|
-
# Generate
|
474
|
+
# Generate single dependency method with lightweight resolution.
|
475
|
+
# Uses eval or define_method based on Ruby version capabilities.
|
422
476
|
#
|
423
|
-
#
|
424
|
-
#
|
425
|
-
#
|
477
|
+
# @param name [Symbol] attribute name
|
478
|
+
# @param depends_on [Array<Symbol>, Symbol] dependency specification
|
479
|
+
# @param block [Proc] computation block
|
480
|
+
# @return [void]
|
481
|
+
def generate_simple_dependency_with_resolution(name, depends_on, block)
|
482
|
+
dep_name = Array(depends_on).first
|
483
|
+
dependency_resolver.add_dependency(name, depends_on)
|
484
|
+
|
485
|
+
if LazyInit::RubyCapabilities::IMPROVED_EVAL_PERFORMANCE
|
486
|
+
generate_fast_dependency_method_with_eval(name, dep_name, block)
|
487
|
+
else
|
488
|
+
generate_fast_dependency_method_with_define_method(name, dep_name, block)
|
489
|
+
generate_predicate_method(name)
|
490
|
+
generate_reset_method(name)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Ruby 3+ eval-based fast dependency method with circular detection.
|
495
|
+
# Includes predicate and reset methods in single eval call for performance.
|
426
496
|
#
|
427
|
-
# @param name [Symbol]
|
428
|
-
# @param
|
497
|
+
# @param name [Symbol] attribute name
|
498
|
+
# @param dep_name [Symbol] dependency attribute name
|
499
|
+
# @param block [Proc] computation block
|
429
500
|
# @return [void]
|
430
|
-
def
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
501
|
+
def generate_fast_dependency_method_with_eval(name, dep_name, block)
|
502
|
+
block_var = "@@lazy_#{name}_block_#{object_id}"
|
503
|
+
class_variable_set(block_var, block)
|
504
|
+
|
505
|
+
method_code = <<~RUBY
|
506
|
+
def #{name}
|
507
|
+
if @#{name}_computed
|
508
|
+
stored_exception = @#{name}_exception
|
509
|
+
raise stored_exception if stored_exception
|
510
|
+
return @#{name}_value
|
511
|
+
end
|
512
|
+
|
513
|
+
# circular dependency detection
|
514
|
+
resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
|
515
|
+
if resolution_stack.include?(:#{name})
|
516
|
+
circular_error = LazyInit::DependencyError.new(
|
517
|
+
"Circular dependency detected: \#{resolution_stack.join(' -> ')} -> #{name}"
|
518
|
+
)
|
519
|
+
@#{name}_exception = circular_error
|
520
|
+
@#{name}_computed = true
|
521
|
+
raise circular_error
|
522
|
+
end
|
523
|
+
|
524
|
+
# lightweight dependency resolution with circular protection
|
525
|
+
resolution_stack.push(:#{name})
|
526
|
+
begin
|
527
|
+
#{dep_name} unless #{dep_name}_computed?
|
528
|
+
ensure
|
529
|
+
resolution_stack.pop
|
530
|
+
Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
|
531
|
+
end
|
532
|
+
|
533
|
+
@#{name}_mutex ||= Mutex.new
|
534
|
+
@#{name}_mutex.synchronize do
|
535
|
+
if @#{name}_computed
|
536
|
+
stored_exception = @#{name}_exception#{' '}
|
537
|
+
raise stored_exception if stored_exception
|
538
|
+
return @#{name}_value
|
539
|
+
end
|
540
|
+
|
541
|
+
begin
|
542
|
+
block = self.class.class_variable_get(:#{block_var})
|
543
|
+
result = instance_eval(&block)
|
544
|
+
@#{name}_value = result
|
545
|
+
@#{name}_computed = true
|
546
|
+
result
|
547
|
+
rescue StandardError => e
|
548
|
+
@#{name}_exception = e
|
549
|
+
@#{name}_computed = true
|
550
|
+
raise
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# generate compatible predicate method
|
556
|
+
def #{name}_computed?
|
557
|
+
@#{name}_computed && !@#{name}_exception
|
558
|
+
end
|
559
|
+
|
560
|
+
# generate compatible reset method
|
561
|
+
def reset_#{name}!
|
562
|
+
@#{name}_mutex&.synchronize do
|
563
|
+
@#{name}_computed = false
|
564
|
+
@#{name}_value = nil
|
565
|
+
@#{name}_exception = nil
|
566
|
+
end
|
567
|
+
end
|
568
|
+
RUBY
|
569
|
+
|
570
|
+
class_eval(method_code)
|
571
|
+
end
|
572
|
+
|
573
|
+
# Ruby 2.6+ define_method version of fast dependency method.
|
574
|
+
# Provides same functionality as eval version with full compatibility.
|
575
|
+
#
|
576
|
+
# @param name [Symbol] attribute name
|
577
|
+
# @param dep_name [Symbol] dependency attribute name
|
578
|
+
# @param block [Proc] computation block
|
579
|
+
# @return [void]
|
580
|
+
def generate_fast_dependency_method_with_define_method(name, dep_name, block)
|
581
|
+
computed_var = "@#{name}_computed"
|
582
|
+
value_var = "@#{name}_value"
|
583
|
+
exception_var = "@#{name}_exception"
|
584
|
+
|
585
|
+
cached_block = block
|
586
|
+
cached_dep_name = dep_name
|
435
587
|
|
436
588
|
define_method(name) do
|
437
|
-
#
|
438
|
-
|
589
|
+
# fast path with exception check
|
590
|
+
if instance_variable_get(computed_var)
|
591
|
+
stored_exception = instance_variable_get(exception_var)
|
592
|
+
raise stored_exception if stored_exception
|
439
593
|
|
440
|
-
|
441
|
-
|
442
|
-
lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
|
594
|
+
return instance_variable_get(value_var)
|
595
|
+
end
|
443
596
|
|
444
|
-
|
445
|
-
|
446
|
-
|
597
|
+
# circular dependency detection
|
598
|
+
resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
|
599
|
+
if resolution_stack.include?(name)
|
600
|
+
circular_error = LazyInit::DependencyError.new(
|
601
|
+
"Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
|
602
|
+
)
|
603
|
+
# cache the error
|
604
|
+
instance_variable_set(exception_var, circular_error)
|
605
|
+
instance_variable_set(computed_var, true)
|
606
|
+
raise circular_error
|
607
|
+
end
|
608
|
+
|
609
|
+
# lightweight dependency resolution with circular protection
|
610
|
+
resolution_stack.push(name)
|
611
|
+
begin
|
612
|
+
send(cached_dep_name) unless send("#{cached_dep_name}_computed?")
|
613
|
+
ensure
|
614
|
+
resolution_stack.pop
|
615
|
+
Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
|
616
|
+
end
|
617
|
+
|
618
|
+
# thread-safe computation
|
619
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
620
|
+
unless mutex
|
621
|
+
mutex = Mutex.new
|
622
|
+
self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
|
623
|
+
end
|
624
|
+
|
625
|
+
mutex.synchronize do
|
626
|
+
if instance_variable_get(computed_var)
|
627
|
+
stored_exception = instance_variable_get(exception_var)
|
628
|
+
raise stored_exception if stored_exception
|
629
|
+
|
630
|
+
return instance_variable_get(value_var)
|
631
|
+
end
|
632
|
+
|
633
|
+
begin
|
634
|
+
result = instance_eval(&cached_block)
|
635
|
+
instance_variable_set(value_var, result)
|
636
|
+
instance_variable_set(computed_var, true)
|
637
|
+
result
|
638
|
+
rescue StandardError => e
|
639
|
+
instance_variable_set(exception_var, e)
|
640
|
+
instance_variable_set(computed_var, true)
|
641
|
+
raise
|
447
642
|
end
|
448
|
-
instance_variable_set(ivar_name, lazy_value)
|
449
643
|
end
|
644
|
+
end
|
645
|
+
end
|
450
646
|
|
451
|
-
|
647
|
+
def simple_case_eligible?(timeout, depends_on)
|
648
|
+
timeout.nil? &&
|
649
|
+
(depends_on.nil? || depends_on.empty?) && LazyInit::RubyCapabilities::RUBY_3_PLUS
|
650
|
+
end
|
651
|
+
|
652
|
+
def generate_optimized_simple_method(name, block)
|
653
|
+
if LazyInit::RubyCapabilities::RUBY_3_PLUS
|
654
|
+
generate_ruby3_ultra_simple_method(name, block)
|
655
|
+
else
|
656
|
+
# Fallback to existing implementation
|
657
|
+
generate_simple_inline_method_with_define_method(name, block)
|
452
658
|
end
|
659
|
+
|
660
|
+
generate_simple_helpers(name)
|
453
661
|
end
|
454
662
|
|
455
|
-
# Generate
|
663
|
+
# Generate ultra-optimized method for Ruby 3+ simple cases.
|
456
664
|
#
|
457
|
-
#
|
458
|
-
#
|
665
|
+
# Uses eval-based method generation with shared mutex and direct
|
666
|
+
# instance variable access for maximum performance. Stores computation
|
667
|
+
# block in class variable for fast access.
|
459
668
|
#
|
460
|
-
# @param name [Symbol]
|
669
|
+
# @param name [Symbol] attribute name
|
670
|
+
# @param block [Proc] computation block
|
461
671
|
# @return [void]
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
672
|
+
# @api private
|
673
|
+
def generate_ruby3_ultra_simple_method(name, block)
|
674
|
+
ensure_shared_mutex
|
675
|
+
|
676
|
+
block_var = "@@simple_#{name}_#{object_id}"
|
677
|
+
class_variable_set(block_var, block)
|
467
678
|
|
468
|
-
|
469
|
-
|
470
|
-
return
|
471
|
-
end
|
679
|
+
method_code = <<~RUBY
|
680
|
+
def #{name}
|
681
|
+
return @#{name}_value if defined?(@#{name}_value)
|
472
682
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
683
|
+
shared_mutex = self.class.instance_variable_get(:@shared_mutex)
|
684
|
+
shared_mutex.synchronize do
|
685
|
+
return @#{name}_value if defined?(@#{name}_value)
|
686
|
+
raise @#{name}_exception if defined?(@#{name}_exception)
|
687
|
+
|
688
|
+
begin
|
689
|
+
@#{name}_value = instance_eval(&self.class.class_variable_get(:#{block_var}))
|
690
|
+
rescue StandardError => e
|
691
|
+
@#{name}_exception = e
|
692
|
+
raise
|
693
|
+
end
|
694
|
+
end
|
478
695
|
end
|
696
|
+
RUBY
|
479
697
|
|
480
|
-
|
481
|
-
false
|
482
|
-
end
|
698
|
+
class_eval(method_code)
|
483
699
|
end
|
484
700
|
|
485
|
-
# Generate
|
701
|
+
# Generate predicate and reset helper methods for simple cases.
|
486
702
|
#
|
487
|
-
#
|
488
|
-
#
|
703
|
+
# Creates computed? and reset! methods that work with the direct
|
704
|
+
# instance variable approach used by simple case optimization.
|
489
705
|
#
|
490
|
-
# @param name [Symbol]
|
706
|
+
# @param name [Symbol] attribute name
|
491
707
|
# @return [void]
|
492
|
-
|
708
|
+
# @api private
|
709
|
+
def generate_simple_helpers(name)
|
710
|
+
define_method("#{name}_computed?") do
|
711
|
+
instance_variable_defined?("@#{name}_value") && !instance_variable_defined?("@#{name}_exception")
|
712
|
+
end
|
713
|
+
|
493
714
|
define_method("reset_#{name}!") do
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
715
|
+
shared_mutex = self.class.instance_variable_get(:@shared_mutex)
|
716
|
+
shared_mutex.synchronize do
|
717
|
+
remove_instance_variable("@#{name}_value") if instance_variable_defined?("@#{name}_value")
|
718
|
+
remove_instance_variable("@#{name}_exception") if instance_variable_defined?("@#{name}_exception")
|
719
|
+
end
|
720
|
+
end
|
721
|
+
end
|
498
722
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
723
|
+
# Ensure shared mutex exists for simple case optimization.
|
724
|
+
#
|
725
|
+
# Creates a class-level mutex shared by all simple attributes to
|
726
|
+
# reduce memory overhead compared to per-attribute mutexes.
|
727
|
+
#
|
728
|
+
# @return [void]
|
729
|
+
# @api private
|
730
|
+
def ensure_shared_mutex
|
731
|
+
return if instance_variable_defined?(:@shared_mutex)
|
732
|
+
@shared_mutex = Mutex.new
|
733
|
+
end
|
734
|
+
|
735
|
+
# Generate optimized method for single dependency attributes.
|
736
|
+
#
|
737
|
+
# Bypasses dependency resolver overhead by directly checking and
|
738
|
+
# resolving single dependencies inline. Includes circular dependency
|
739
|
+
# detection and thread-safe computation.
|
740
|
+
#
|
741
|
+
# @param name [Symbol] attribute name
|
742
|
+
# @param dep_name [Symbol] dependency attribute name
|
743
|
+
# @param block [Proc] computation block
|
744
|
+
# @return [void]
|
745
|
+
# @api private
|
746
|
+
def generate_simple_dependency_with_inline_check(name, dep_name, block)
|
747
|
+
cached_block = block
|
748
|
+
cached_dep_name = dep_name
|
749
|
+
computed_var = "@#{name}_computed"
|
750
|
+
value_var = "@#{name}_value"
|
751
|
+
exception_var = "@#{name}_exception"
|
752
|
+
|
753
|
+
define_method(name) do
|
754
|
+
# Fast path: return cached result
|
755
|
+
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
756
|
+
|
757
|
+
# Circular dependency detection BEFORE mutex
|
758
|
+
resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
|
759
|
+
if resolution_stack.include?(name)
|
760
|
+
circular_error = LazyInit::DependencyError.new(
|
761
|
+
"Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
|
762
|
+
)
|
763
|
+
raise circular_error
|
764
|
+
end
|
765
|
+
|
766
|
+
resolution_stack.push(name)
|
767
|
+
begin
|
768
|
+
# Inline dependency check
|
769
|
+
unless send("#{cached_dep_name}_computed?")
|
770
|
+
send(cached_dep_name)
|
771
|
+
end
|
772
|
+
|
773
|
+
# Thread-safe computation
|
774
|
+
mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
|
775
|
+
mutex.synchronize do
|
776
|
+
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
777
|
+
|
778
|
+
begin
|
779
|
+
result = instance_eval(&cached_block)
|
780
|
+
instance_variable_set(value_var, result)
|
781
|
+
instance_variable_set(computed_var, true)
|
782
|
+
result
|
783
|
+
rescue StandardError => e
|
784
|
+
instance_variable_set(exception_var, e)
|
785
|
+
instance_variable_set(computed_var, true)
|
786
|
+
raise
|
507
787
|
end
|
508
|
-
else
|
509
|
-
# no mutex means no concurrent access, safe to reset directly
|
510
|
-
instance_variable_set(computed_var, false)
|
511
|
-
remove_instance_variable(value_var) if instance_variable_defined?(value_var)
|
512
|
-
remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
|
513
788
|
end
|
514
|
-
|
789
|
+
ensure
|
790
|
+
resolution_stack.pop
|
791
|
+
Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
|
515
792
|
end
|
793
|
+
end
|
794
|
+
end
|
516
795
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
796
|
+
# Generate reset method that clears dependency resolution flag.
|
797
|
+
#
|
798
|
+
# Used by attributes with dependency caching to ensure dependencies
|
799
|
+
# are re-resolved after reset. Thread-safe operation that clears
|
800
|
+
# both computed state and dependency resolution state.
|
801
|
+
#
|
802
|
+
# @param name [Symbol] attribute name
|
803
|
+
# @return [void]
|
804
|
+
# @api private
|
805
|
+
def generate_reset_method_with_deps_flag(name)
|
806
|
+
computed_var = "@#{name}_computed"
|
807
|
+
value_var = "@#{name}_value"
|
808
|
+
exception_var = "@#{name}_exception"
|
809
|
+
deps_resolved_var = "@#{name}_deps_resolved"
|
810
|
+
|
811
|
+
define_method("reset_#{name}!") do
|
812
|
+
mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
|
813
|
+
mutex.synchronize do
|
814
|
+
remove_instance_variable(value_var) if instance_variable_defined?(value_var)
|
815
|
+
remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
|
816
|
+
instance_variable_set(computed_var, false)
|
817
|
+
instance_variable_set(deps_resolved_var, false) # Reset dependency flag
|
523
818
|
end
|
524
819
|
end
|
525
820
|
end
|
526
821
|
|
527
|
-
#
|
822
|
+
# Generate method with cached dependency resolution for multiple dependencies.
|
528
823
|
#
|
529
|
-
#
|
530
|
-
#
|
824
|
+
# Optimizes attributes with multiple dependencies by caching the dependency
|
825
|
+
# resolution step. Once dependencies are resolved for an instance, subsequent
|
826
|
+
# calls skip the dependency resolver entirely.
|
531
827
|
#
|
532
|
-
# @param name [
|
828
|
+
# @param name [Symbol] attribute name
|
829
|
+
# @param depends_on [Array<Symbol>] dependency attributes
|
830
|
+
# @param block [Proc] computation block
|
831
|
+
# @param config [Hash] attribute configuration
|
533
832
|
# @return [void]
|
534
|
-
# @
|
535
|
-
def
|
536
|
-
|
537
|
-
|
833
|
+
# @api private
|
834
|
+
def generate_fast_dependency_method(name, depends_on, block, config)
|
835
|
+
cached_block = block
|
836
|
+
cached_depends_on = depends_on
|
837
|
+
computed_var = "@#{name}_computed"
|
838
|
+
value_var = "@#{name}_value"
|
839
|
+
exception_var = "@#{name}_exception"
|
840
|
+
deps_resolved_var = "@#{name}_deps_resolved" # flag for resolved deps
|
538
841
|
|
539
|
-
|
540
|
-
|
541
|
-
|
842
|
+
define_method(name) do
|
843
|
+
# Fast path: return cached result
|
844
|
+
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
542
845
|
|
543
|
-
|
544
|
-
|
846
|
+
# Fast dependency check: skip resolution if already resolved
|
847
|
+
unless instance_variable_get(deps_resolved_var)
|
848
|
+
# Only resolve dependencies once
|
849
|
+
self.class.dependency_resolver.resolve_dependencies(name, self)
|
850
|
+
instance_variable_set(deps_resolved_var, true)
|
851
|
+
end
|
545
852
|
|
546
|
-
|
853
|
+
# Thread-safe computation
|
854
|
+
mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
|
855
|
+
mutex.synchronize do
|
856
|
+
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
857
|
+
|
858
|
+
begin
|
859
|
+
result = instance_eval(&cached_block)
|
860
|
+
instance_variable_set(value_var, result)
|
861
|
+
instance_variable_set(computed_var, true)
|
862
|
+
result
|
863
|
+
rescue StandardError => e
|
864
|
+
instance_variable_set(exception_var, e)
|
865
|
+
instance_variable_set(computed_var, true)
|
866
|
+
raise
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|
547
870
|
end
|
548
871
|
end
|
549
|
-
end
|
872
|
+
end
|