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,549 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyInit
|
4
|
+
# Provides class-level methods for defining lazy attributes with various optimization strategies.
|
5
|
+
#
|
6
|
+
# This module is automatically extended when a class includes or extends LazyInit.
|
7
|
+
# It analyzes attribute configuration and selects the most efficient implementation:
|
8
|
+
# simple inline methods for basic cases, optimized dependency methods for single
|
9
|
+
# dependencies, and full LazyValue wrappers for complex scenarios.
|
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
|
15
|
+
#
|
16
|
+
# @example Basic lazy attribute
|
17
|
+
# class ApiClient
|
18
|
+
# extend LazyInit
|
19
|
+
#
|
20
|
+
# lazy_attr_reader :connection do
|
21
|
+
# HTTPClient.new(api_url)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @example Lazy attribute with dependencies
|
26
|
+
# class DatabaseService
|
27
|
+
# extend LazyInit
|
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
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# @since 0.1.0
|
39
|
+
module ClassMethods
|
40
|
+
# Set up necessary infrastructure when LazyInit is extended by a class.
|
41
|
+
#
|
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
|
+
# @param base [Class] the class being extended with LazyInit
|
50
|
+
# @return [void]
|
51
|
+
# @api private
|
52
|
+
def self.extended(base)
|
53
|
+
base.instance_variable_set(:@lazy_init_class_mutex, Mutex.new)
|
54
|
+
base.instance_variable_set(:@dependency_resolver, DependencyResolver.new(base))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Access the registry of all lazy initializers defined on this class.
|
58
|
+
#
|
59
|
+
# Used internally for introspection and debugging. Each entry contains
|
60
|
+
# the configuration (block, timeout, dependencies) for a lazy attribute.
|
61
|
+
#
|
62
|
+
# @return [Hash<Symbol, Hash>] mapping of attribute names to their configuration
|
63
|
+
def lazy_initializers
|
64
|
+
@lazy_initializers ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Access the dependency resolver for this class.
|
68
|
+
#
|
69
|
+
# Handles dependency graph management and resolution order computation.
|
70
|
+
# Creates a new resolver if one doesn't exist.
|
71
|
+
#
|
72
|
+
# @return [DependencyResolver] the resolver instance for this class
|
73
|
+
def dependency_resolver
|
74
|
+
@dependency_resolver ||= DependencyResolver.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Define a thread-safe lazy-initialized instance attribute.
|
78
|
+
#
|
79
|
+
# The attribute will be computed only once per instance when first accessed.
|
80
|
+
# Subsequent calls return the cached value. The implementation is automatically
|
81
|
+
# optimized based on complexity: simple cases use inline variables, single
|
82
|
+
# dependencies use optimized resolution, complex cases use full LazyValue.
|
83
|
+
#
|
84
|
+
# @param name [Symbol, String] the attribute name
|
85
|
+
# @param timeout [Numeric, nil] timeout in seconds for the computation
|
86
|
+
# @param depends_on [Array<Symbol>, Symbol, nil] other attributes this depends on
|
87
|
+
# @param block [Proc] the computation block
|
88
|
+
# @return [void]
|
89
|
+
# @raise [ArgumentError] if no block is provided
|
90
|
+
# @raise [InvalidAttributeNameError] if the attribute name is invalid
|
91
|
+
#
|
92
|
+
# @example Simple lazy attribute
|
93
|
+
# lazy_attr_reader :expensive_data do
|
94
|
+
# fetch_from_external_api
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# @example With dependencies
|
98
|
+
# lazy_attr_reader :database, depends_on: [:config] do
|
99
|
+
# Database.connect(config.database_url)
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# @example With timeout protection
|
103
|
+
# lazy_attr_reader :slow_service, timeout: 10 do
|
104
|
+
# SlowExternalService.connect
|
105
|
+
# end
|
106
|
+
def lazy_attr_reader(name, timeout: nil, depends_on: nil, &block)
|
107
|
+
validate_attribute_name!(name)
|
108
|
+
raise ArgumentError, 'Block is required' unless block
|
109
|
+
|
110
|
+
# store configuration for introspection and debugging
|
111
|
+
config = {
|
112
|
+
block: block,
|
113
|
+
timeout: timeout || LazyInit.configuration.default_timeout,
|
114
|
+
depends_on: depends_on
|
115
|
+
}
|
116
|
+
lazy_initializers[name] = config
|
117
|
+
|
118
|
+
# register dependencies with resolver if present
|
119
|
+
dependency_resolver.add_dependency(name, depends_on) if depends_on
|
120
|
+
|
121
|
+
# select optimal implementation strategy based on complexity
|
122
|
+
if enhanced_simple_case?(timeout, depends_on)
|
123
|
+
if simple_dependency_case?(depends_on)
|
124
|
+
generate_simple_dependency_method(name, depends_on, block)
|
125
|
+
else
|
126
|
+
generate_simple_inline_method(name, block)
|
127
|
+
end
|
128
|
+
else
|
129
|
+
generate_complex_lazyvalue_method(name, config)
|
130
|
+
end
|
131
|
+
|
132
|
+
# generate helper methods for all implementation types
|
133
|
+
generate_predicate_method(name)
|
134
|
+
generate_reset_method(name)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Define a thread-safe lazy-initialized class variable shared across all instances.
|
138
|
+
#
|
139
|
+
# The variable will be computed only once per class when first accessed.
|
140
|
+
# All instances share the same computed value. Class variables are always
|
141
|
+
# implemented using LazyValue for full thread safety and feature support.
|
142
|
+
#
|
143
|
+
# @param name [Symbol, String] the class variable name
|
144
|
+
# @param timeout [Numeric, nil] timeout in seconds for the computation
|
145
|
+
# @param depends_on [Array<Symbol>, Symbol, nil] other attributes this depends on
|
146
|
+
# @param block [Proc] the computation block
|
147
|
+
# @return [void]
|
148
|
+
# @raise [ArgumentError] if no block is provided
|
149
|
+
# @raise [InvalidAttributeNameError] if the attribute name is invalid
|
150
|
+
#
|
151
|
+
# @example Shared connection pool
|
152
|
+
# lazy_class_variable :connection_pool do
|
153
|
+
# ConnectionPool.new(size: 20)
|
154
|
+
# end
|
155
|
+
def lazy_class_variable(name, timeout: nil, depends_on: nil, &block)
|
156
|
+
validate_attribute_name!(name)
|
157
|
+
raise ArgumentError, 'Block is required' unless block
|
158
|
+
|
159
|
+
class_variable_name = "@@#{name}_lazy_value"
|
160
|
+
|
161
|
+
# register dependencies for class-level attributes too
|
162
|
+
dependency_resolver.add_dependency(name, depends_on) if depends_on
|
163
|
+
|
164
|
+
# cache configuration for use in generated methods
|
165
|
+
cached_timeout = timeout
|
166
|
+
cached_depends_on = depends_on
|
167
|
+
cached_block = block
|
168
|
+
|
169
|
+
# generate class-level accessor with full thread safety
|
170
|
+
define_singleton_method(name) do
|
171
|
+
@lazy_init_class_mutex.synchronize do
|
172
|
+
return class_variable_get(class_variable_name).value if class_variable_defined?(class_variable_name)
|
173
|
+
|
174
|
+
# resolve dependencies using temporary instance if needed
|
175
|
+
if cached_depends_on
|
176
|
+
temp_instance = begin
|
177
|
+
new
|
178
|
+
rescue StandardError
|
179
|
+
# fallback for classes that can't be instantiated normally
|
180
|
+
Object.new.tap { |obj| obj.extend(self) }
|
181
|
+
end
|
182
|
+
dependency_resolver.resolve_dependencies(name, temp_instance)
|
183
|
+
end
|
184
|
+
|
185
|
+
# create and store the lazy value wrapper
|
186
|
+
lazy_value = LazyValue.new(timeout: cached_timeout, &cached_block)
|
187
|
+
class_variable_set(class_variable_name, lazy_value)
|
188
|
+
lazy_value.value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# generate class-level predicate method
|
193
|
+
define_singleton_method("#{name}_computed?") do
|
194
|
+
if class_variable_defined?(class_variable_name)
|
195
|
+
class_variable_get(class_variable_name).computed?
|
196
|
+
else
|
197
|
+
false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# generate class-level reset method
|
202
|
+
define_singleton_method("reset_#{name}!") do
|
203
|
+
if class_variable_defined?(class_variable_name)
|
204
|
+
lazy_value = class_variable_get(class_variable_name)
|
205
|
+
lazy_value.reset!
|
206
|
+
remove_class_variable(class_variable_name)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# generate instance-level delegation methods for convenience
|
211
|
+
define_method(name) { self.class.send(name) }
|
212
|
+
define_method("#{name}_computed?") { self.class.send("#{name}_computed?") }
|
213
|
+
define_method("reset_#{name}!") { self.class.send("reset_#{name}!") }
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# Determine if an attribute qualifies for simple optimization.
|
219
|
+
#
|
220
|
+
# Simple cases avoid LazyValue overhead by using direct instance variables.
|
221
|
+
# This includes attributes with no timeout and either no dependencies or
|
222
|
+
# a single simple dependency that can be inlined.
|
223
|
+
#
|
224
|
+
# @param timeout [Object] timeout configuration
|
225
|
+
# @param depends_on [Object] dependency configuration
|
226
|
+
# @return [Boolean] true if simple implementation should be used
|
227
|
+
def enhanced_simple_case?(timeout, depends_on)
|
228
|
+
# timeout requires LazyValue for proper handling
|
229
|
+
return false unless timeout.nil?
|
230
|
+
|
231
|
+
# categorize dependency complexity
|
232
|
+
case depends_on
|
233
|
+
when nil, []
|
234
|
+
true # no dependencies are always simple
|
235
|
+
when Array
|
236
|
+
depends_on.size == 1 # single dependency can be optimized
|
237
|
+
when Symbol, String
|
238
|
+
true # single dependency in simple form
|
239
|
+
else
|
240
|
+
false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Check if dependencies qualify for simple dependency optimization.
|
245
|
+
#
|
246
|
+
# Single dependencies can use an optimized resolution strategy that
|
247
|
+
# avoids the full dependency resolver overhead.
|
248
|
+
#
|
249
|
+
# @param depends_on [Object] dependency configuration
|
250
|
+
# @return [Boolean] true if simple dependency method should be used
|
251
|
+
def simple_dependency_case?(depends_on)
|
252
|
+
return false if depends_on.nil? || depends_on.empty?
|
253
|
+
|
254
|
+
deps = Array(depends_on)
|
255
|
+
deps.size == 1 # any single dependency qualifies for optimization
|
256
|
+
end
|
257
|
+
|
258
|
+
# Generate an optimized method for attributes with single dependencies.
|
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.
|
263
|
+
#
|
264
|
+
# @param name [Symbol] the attribute name
|
265
|
+
# @param depends_on [Array, Symbol] the single dependency
|
266
|
+
# @param block [Proc] the computation block
|
267
|
+
# @return [void]
|
268
|
+
def generate_simple_dependency_method(name, depends_on, block)
|
269
|
+
dep_name = Array(depends_on).first
|
270
|
+
computed_var = "@#{name}_computed"
|
271
|
+
value_var = "@#{name}_value"
|
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
|
277
|
+
|
278
|
+
define_method(name) do
|
279
|
+
# fast path: return cached result including cached errors
|
280
|
+
if instance_variable_get(computed_var)
|
281
|
+
stored_exception = instance_variable_get(exception_var)
|
282
|
+
raise stored_exception if stored_exception
|
283
|
+
|
284
|
+
return instance_variable_get(value_var)
|
285
|
+
end
|
286
|
+
|
287
|
+
# circular dependency protection using shared resolution stack
|
288
|
+
resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
|
289
|
+
if resolution_stack.include?(name)
|
290
|
+
circular_error = LazyInit::DependencyError.new(
|
291
|
+
"Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
|
292
|
+
)
|
293
|
+
|
294
|
+
# thread-safe error caching so all threads see the same error
|
295
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex) || Mutex.new
|
296
|
+
unless self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
297
|
+
self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
|
298
|
+
end
|
299
|
+
|
300
|
+
mutex.synchronize do
|
301
|
+
instance_variable_set(exception_var, circular_error)
|
302
|
+
instance_variable_set(computed_var, true)
|
303
|
+
end
|
304
|
+
|
305
|
+
raise circular_error
|
306
|
+
end
|
307
|
+
|
308
|
+
# ensure we have a mutex for thread-safe computation
|
309
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
310
|
+
unless mutex
|
311
|
+
mutex = Mutex.new
|
312
|
+
self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
|
313
|
+
end
|
314
|
+
|
315
|
+
# track this attribute in resolution stack
|
316
|
+
resolution_stack.push(name)
|
317
|
+
|
318
|
+
begin
|
319
|
+
mutex.synchronize do
|
320
|
+
# double-check pattern after acquiring lock
|
321
|
+
if instance_variable_get(computed_var)
|
322
|
+
stored_exception = instance_variable_get(exception_var)
|
323
|
+
raise stored_exception if stored_exception
|
324
|
+
|
325
|
+
return instance_variable_get(value_var)
|
326
|
+
end
|
327
|
+
|
328
|
+
begin
|
329
|
+
# ensure dependency is computed first using optimized approach
|
330
|
+
unless send("#{cached_dep_name}_computed?")
|
331
|
+
# temporarily release lock to avoid deadlocks during dependency resolution
|
332
|
+
mutex.unlock
|
333
|
+
begin
|
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)
|
352
|
+
result
|
353
|
+
rescue StandardError => e
|
354
|
+
# cache exceptions for consistent behavior across threads
|
355
|
+
instance_variable_set(exception_var, e)
|
356
|
+
instance_variable_set(computed_var, true)
|
357
|
+
raise
|
358
|
+
end
|
359
|
+
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
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Generate a simple inline method for attributes with no dependencies.
|
369
|
+
#
|
370
|
+
# Uses direct instance variables for maximum performance while maintaining
|
371
|
+
# thread safety through mutex synchronization. This is the fastest
|
372
|
+
# implementation strategy available.
|
373
|
+
#
|
374
|
+
# @param name [Symbol] the attribute name
|
375
|
+
# @param block [Proc] the computation block
|
376
|
+
# @return [void]
|
377
|
+
def generate_simple_inline_method(name, block)
|
378
|
+
computed_var = "@#{name}_computed"
|
379
|
+
value_var = "@#{name}_value"
|
380
|
+
exception_var = "@#{name}_exception"
|
381
|
+
|
382
|
+
# cache block reference to avoid lookup in generated method
|
383
|
+
cached_block = block
|
384
|
+
|
385
|
+
define_method(name) do
|
386
|
+
# fast path: return cached value immediately if available
|
387
|
+
return instance_variable_get(value_var) if instance_variable_get(computed_var)
|
388
|
+
|
389
|
+
# ensure we have a shared mutex for thread safety
|
390
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
391
|
+
unless mutex
|
392
|
+
mutex = Mutex.new
|
393
|
+
self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
|
394
|
+
end
|
395
|
+
|
396
|
+
mutex.synchronize do
|
397
|
+
# double-check pattern: another thread might have computed while we waited
|
398
|
+
if instance_variable_get(computed_var)
|
399
|
+
stored_exception = instance_variable_get(exception_var)
|
400
|
+
raise stored_exception if stored_exception
|
401
|
+
|
402
|
+
return instance_variable_get(value_var)
|
403
|
+
end
|
404
|
+
|
405
|
+
begin
|
406
|
+
# perform computation and cache result
|
407
|
+
result = instance_eval(&cached_block)
|
408
|
+
instance_variable_set(value_var, result)
|
409
|
+
instance_variable_set(computed_var, true)
|
410
|
+
result
|
411
|
+
rescue StandardError => e
|
412
|
+
# cache exceptions to ensure consistent error behavior
|
413
|
+
instance_variable_set(exception_var, e)
|
414
|
+
instance_variable_set(computed_var, true)
|
415
|
+
raise
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Generate a method using full LazyValue for complex scenarios.
|
422
|
+
#
|
423
|
+
# This handles timeouts, complex dependencies, and other advanced features
|
424
|
+
# that require the full LazyValue implementation. Used when simple
|
425
|
+
# optimizations aren't applicable.
|
426
|
+
#
|
427
|
+
# @param name [Symbol] the attribute name
|
428
|
+
# @param config [Hash] the attribute configuration
|
429
|
+
# @return [void]
|
430
|
+
def generate_complex_lazyvalue_method(name, config)
|
431
|
+
# cache configuration to avoid hash lookups in generated method
|
432
|
+
cached_timeout = config[:timeout]
|
433
|
+
cached_depends_on = config[:depends_on]
|
434
|
+
cached_block = config[:block]
|
435
|
+
|
436
|
+
define_method(name) do
|
437
|
+
# resolve dependencies using full dependency resolver if needed
|
438
|
+
self.class.dependency_resolver.resolve_dependencies(name, self) if cached_depends_on
|
439
|
+
|
440
|
+
# lazy creation of LazyValue wrapper
|
441
|
+
ivar_name = "@#{name}_lazy_value"
|
442
|
+
lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
|
443
|
+
|
444
|
+
unless lazy_value
|
445
|
+
lazy_value = LazyValue.new(timeout: cached_timeout) do
|
446
|
+
instance_eval(&cached_block)
|
447
|
+
end
|
448
|
+
instance_variable_set(ivar_name, lazy_value)
|
449
|
+
end
|
450
|
+
|
451
|
+
lazy_value.value
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# Generate predicate method to check if attribute has been computed.
|
456
|
+
#
|
457
|
+
# Handles both simple (inline variables) and complex (LazyValue) cases.
|
458
|
+
# Returns false for exceptions to maintain consistent behavior.
|
459
|
+
#
|
460
|
+
# @param name [Symbol] the attribute name
|
461
|
+
# @return [void]
|
462
|
+
def generate_predicate_method(name)
|
463
|
+
define_method("#{name}_computed?") do
|
464
|
+
# check simple implementation first (most common after optimization)
|
465
|
+
computed_var = "@#{name}_computed"
|
466
|
+
exception_var = "@#{name}_exception"
|
467
|
+
|
468
|
+
if instance_variable_defined?(computed_var)
|
469
|
+
# simple implementation: computed but not if there's a cached exception
|
470
|
+
return instance_variable_get(computed_var) && !instance_variable_get(exception_var)
|
471
|
+
end
|
472
|
+
|
473
|
+
# check complex implementation (LazyValue wrapper)
|
474
|
+
lazy_var = "@#{name}_lazy_value"
|
475
|
+
if instance_variable_defined?(lazy_var)
|
476
|
+
lazy_value = instance_variable_get(lazy_var)
|
477
|
+
return lazy_value&.computed? || false
|
478
|
+
end
|
479
|
+
|
480
|
+
# not computed yet
|
481
|
+
false
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Generate reset method to clear computed state and allow recomputation.
|
486
|
+
#
|
487
|
+
# Handles both simple and complex implementations, ensuring proper
|
488
|
+
# cleanup of all associated state including cached exceptions.
|
489
|
+
#
|
490
|
+
# @param name [Symbol] the attribute name
|
491
|
+
# @return [void]
|
492
|
+
def generate_reset_method(name)
|
493
|
+
define_method("reset_#{name}!") do
|
494
|
+
# handle simple implementation reset
|
495
|
+
computed_var = "@#{name}_computed"
|
496
|
+
value_var = "@#{name}_value"
|
497
|
+
exception_var = "@#{name}_exception"
|
498
|
+
|
499
|
+
if instance_variable_defined?(computed_var)
|
500
|
+
# use mutex if available for thread safety during reset
|
501
|
+
mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
|
502
|
+
if mutex
|
503
|
+
mutex.synchronize do
|
504
|
+
instance_variable_set(computed_var, false)
|
505
|
+
remove_instance_variable(value_var) if instance_variable_defined?(value_var)
|
506
|
+
remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
|
507
|
+
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
|
+
end
|
514
|
+
return
|
515
|
+
end
|
516
|
+
|
517
|
+
# handle complex implementation reset (LazyValue)
|
518
|
+
lazy_var = "@#{name}_lazy_value"
|
519
|
+
if instance_variable_defined?(lazy_var)
|
520
|
+
lazy_value = instance_variable_get(lazy_var)
|
521
|
+
lazy_value&.reset!
|
522
|
+
remove_instance_variable(lazy_var)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
# Validate that the attribute name is suitable for method generation.
|
528
|
+
#
|
529
|
+
# Ensures the name follows Ruby method naming conventions and won't
|
530
|
+
# cause issues when used to generate accessor methods.
|
531
|
+
#
|
532
|
+
# @param name [Object] the proposed attribute name
|
533
|
+
# @return [void]
|
534
|
+
# @raise [InvalidAttributeNameError] if the name is invalid
|
535
|
+
def validate_attribute_name!(name)
|
536
|
+
raise InvalidAttributeNameError, 'Attribute name cannot be nil' if name.nil?
|
537
|
+
raise InvalidAttributeNameError, 'Attribute name cannot be empty' if name.to_s.strip.empty?
|
538
|
+
|
539
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
540
|
+
raise InvalidAttributeNameError, 'Attribute name must be a symbol or string'
|
541
|
+
end
|
542
|
+
|
543
|
+
name_str = name.to_s
|
544
|
+
return if name_str.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
|
545
|
+
|
546
|
+
raise InvalidAttributeNameError, "Invalid attribute name: #{name_str}"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyInit
|
4
|
+
# Global configuration for LazyInit gem behavior.
|
5
|
+
#
|
6
|
+
# Provides centralized configuration for timeout defaults and memory management settings.
|
7
|
+
#
|
8
|
+
# @example Basic configuration
|
9
|
+
# LazyInit.configure do |config|
|
10
|
+
# config.default_timeout = 30
|
11
|
+
# config.max_lazy_once_entries = 5000
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @since 0.1.0
|
15
|
+
class Configuration
|
16
|
+
# Default timeout in seconds for all lazy attributes
|
17
|
+
# @return [Numeric, nil] timeout value (default: nil)
|
18
|
+
attr_accessor :default_timeout
|
19
|
+
|
20
|
+
# Maximum entries in lazy_once cache
|
21
|
+
# @return [Integer] maximum cache entries (default: 1000)
|
22
|
+
attr_accessor :max_lazy_once_entries
|
23
|
+
|
24
|
+
# Time-to-live for lazy_once entries in seconds
|
25
|
+
# @return [Numeric, nil] TTL value (default: nil)
|
26
|
+
attr_accessor :lazy_once_ttl
|
27
|
+
|
28
|
+
# Initializes configuration with default values.
|
29
|
+
def initialize
|
30
|
+
@default_timeout = nil
|
31
|
+
@max_lazy_once_entries = 1000
|
32
|
+
@lazy_once_ttl = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the global configuration instance.
|
37
|
+
#
|
38
|
+
# @return [Configuration] the current configuration object
|
39
|
+
def self.configuration
|
40
|
+
@configuration ||= Configuration.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Configures LazyInit global settings.
|
44
|
+
#
|
45
|
+
# @yield [Configuration] the configuration object to modify
|
46
|
+
# @return [Configuration] the updated configuration
|
47
|
+
#
|
48
|
+
# @example Environment-specific configuration
|
49
|
+
# LazyInit.configure do |config|
|
50
|
+
# config.default_timeout = 10
|
51
|
+
# config.max_lazy_once_entries = 5000
|
52
|
+
# config.lazy_once_ttl = 1.hour
|
53
|
+
# end
|
54
|
+
def self.configure
|
55
|
+
yield(configuration)
|
56
|
+
end
|
57
|
+
end
|