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
data/README.md
ADDED
@@ -0,0 +1,765 @@
|
|
1
|
+
# LazyInit
|
2
|
+
|
3
|
+
Thread-safe lazy initialization patterns for Ruby with automatic dependency resolution, memory management, and performance optimization.
|
4
|
+
|
5
|
+
[](https://www.ruby-lang.org/)
|
6
|
+
[](https://badge.fury.io/rb/lazy_init)
|
7
|
+
[](https://github.com/N3BCKN/lazy_init/actions)
|
8
|
+
|
9
|
+
## Table of Contents
|
10
|
+
|
11
|
+
- [Problem Statement](#problem-statement)
|
12
|
+
- [Installation](#installation)
|
13
|
+
- [Quick Start](#quick-start)
|
14
|
+
- [Core Features](#core-features)
|
15
|
+
- [API Reference](#api-reference)
|
16
|
+
- [Instance Attributes](#instance-attributes)
|
17
|
+
- [Class Variables](#class-variables)
|
18
|
+
- [Instance Methods](#instance-methods)
|
19
|
+
- [Configuration](#configuration)
|
20
|
+
- [Advanced Usage](#advanced-usage)
|
21
|
+
- [Dependency Resolution](#dependency-resolution)
|
22
|
+
- [Timeout Protection](#timeout-protection)
|
23
|
+
- [Memory Management](#memory-management)
|
24
|
+
- [Common Use Cases](#common-use-cases)
|
25
|
+
- [Error Handling](#error-handling)
|
26
|
+
- [Performance](#performance)
|
27
|
+
- [Thread Safety](#thread-safety)
|
28
|
+
- [Thread Safety Deep Dive](#thread-safety-deep-dive)
|
29
|
+
- [Compatibility](#compatibility)
|
30
|
+
- [Testing](#testing)
|
31
|
+
- [Migration Guide](#migration-guide)
|
32
|
+
- [When NOT to Use LazyInit](#when-not-to-use-lazyinit)
|
33
|
+
- [FAQ](#faq)
|
34
|
+
- [Contributing](#contributing)
|
35
|
+
- [License](#license)
|
36
|
+
|
37
|
+
## Problem Statement
|
38
|
+
|
39
|
+
Ruby's common lazy initialization pattern using `||=` is **not thread-safe** and can cause race conditions in multi-threaded environments:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# ❌ Thread-unsafe (common pattern)
|
43
|
+
def expensive_calculation
|
44
|
+
@result ||= perform_heavy_computation # Race condition possible!
|
45
|
+
end
|
46
|
+
|
47
|
+
# ✅ Thread-safe (LazyInit solution)
|
48
|
+
lazy_attr_reader :expensive_calculation do
|
49
|
+
perform_heavy_computation
|
50
|
+
end
|
51
|
+
```
|
52
|
+
LazyInit provides thread-safe lazy initialization with zero race conditions, automatic dependency resolution, and intelligent performance optimization.
|
53
|
+
|
54
|
+
## Installation
|
55
|
+
Add this line to your application's Gemfile:
|
56
|
+
```ruby
|
57
|
+
gem 'lazy_init'
|
58
|
+
```
|
59
|
+
And then execute:
|
60
|
+
```ruby
|
61
|
+
bundle install
|
62
|
+
```
|
63
|
+
Or install it yourself as:
|
64
|
+
```ruby
|
65
|
+
gem install lazy_init
|
66
|
+
```
|
67
|
+
## Requirements:
|
68
|
+
|
69
|
+
- Ruby 2.6 or higher
|
70
|
+
- No external dependencies
|
71
|
+
|
72
|
+
## Quick Start
|
73
|
+
### Basic Usage
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class ApiClient
|
77
|
+
extend LazyInit
|
78
|
+
|
79
|
+
lazy_attr_reader :connection do
|
80
|
+
puts "Establishing connection..."
|
81
|
+
HTTPClient.new(api_url)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
client = ApiClient.new
|
86
|
+
# No connection established yet
|
87
|
+
|
88
|
+
client.connection # "Establishing connection..." - computed once
|
89
|
+
client.connection # Returns cached result (thread-safe)
|
90
|
+
```
|
91
|
+
|
92
|
+
### With Dependencies
|
93
|
+
```ruby
|
94
|
+
class WebService
|
95
|
+
extend LazyInit
|
96
|
+
|
97
|
+
lazy_attr_reader :config do
|
98
|
+
YAML.load_file('config.yml')
|
99
|
+
end
|
100
|
+
|
101
|
+
lazy_attr_reader :database, depends_on: [:config] do
|
102
|
+
Database.connect(config['database_url'])
|
103
|
+
end
|
104
|
+
|
105
|
+
lazy_attr_reader :api_client, depends_on: [:config, :database] do
|
106
|
+
ApiClient.new(
|
107
|
+
url: config['api_url'],
|
108
|
+
database: database
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
service = WebService.new
|
114
|
+
service.api_client # Automatically resolves: config → database → api_client
|
115
|
+
```
|
116
|
+
|
117
|
+
## Core Features
|
118
|
+
### Thread Safety
|
119
|
+
|
120
|
+
- Eliminates all race conditions with optimized double-checked locking
|
121
|
+
- Circular dependency detection prevents infinite loops
|
122
|
+
- Thread-safe reset for testing and error recovery
|
123
|
+
|
124
|
+
### Automatic Dependency Resolution
|
125
|
+
|
126
|
+
- Declarative dependencies with depends_on option
|
127
|
+
- Automatic resolution order computation
|
128
|
+
- Intelligent caching to avoid redundant work
|
129
|
+
|
130
|
+
### Performance Optimization
|
131
|
+
|
132
|
+
- Three-tier implementation strategy based on complexity
|
133
|
+
- 5-6x overhead vs manual ||= (significantly faster than alternatives)
|
134
|
+
- Memory-efficient with automatic cleanup
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# Simple inline (fastest)
|
138
|
+
lazy_attr_reader :simple_value do
|
139
|
+
"simple"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Optimized dependency (medium)
|
143
|
+
lazy_attr_reader :dependent_value, depends_on: [:simple_value] do
|
144
|
+
"depends on #{simple_value}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Full LazyValue (complete features)
|
148
|
+
lazy_attr_reader :complex_value, timeout: 5, depends_on: [:multiple, :deps] do
|
149
|
+
"complex computation"
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Memory Management
|
154
|
+
|
155
|
+
- Automatic cache cleanup prevents memory leaks
|
156
|
+
- LRU eviction for method-local caching
|
157
|
+
- TTL support for time-based expiration
|
158
|
+
|
159
|
+
## API Reference
|
160
|
+
### Instance Attributes
|
161
|
+
```ruby
|
162
|
+
lazy_attr_reader(name, **options, &block)
|
163
|
+
```
|
164
|
+
Defines a thread-safe lazy-initialized attribute.
|
165
|
+
#### Parameters:
|
166
|
+
|
167
|
+
- name (Symbol/String): Attribute name
|
168
|
+
- timeout (Numeric, optional): Timeout in seconds for computation
|
169
|
+
- depends_on (Array<Symbol>/Symbol, optional): Dependencies to resolve first
|
170
|
+
- block (Proc): Computation block
|
171
|
+
|
172
|
+
#### Generated Methods:
|
173
|
+
|
174
|
+
- #{name}: Returns the computed value
|
175
|
+
- #{name}_computed?: Returns true if value has been computed
|
176
|
+
- reset_#{name}!: Resets to uncomputed state
|
177
|
+
|
178
|
+
#### Examples:
|
179
|
+
```ruby
|
180
|
+
rubyclass ServiceManager
|
181
|
+
extend LazyInit
|
182
|
+
|
183
|
+
# Simple lazy attribute
|
184
|
+
lazy_attr_reader :expensive_service do
|
185
|
+
ExpensiveService.new
|
186
|
+
end
|
187
|
+
|
188
|
+
# With timeout protection
|
189
|
+
lazy_attr_reader :external_api, timeout: 10 do
|
190
|
+
ExternalAPI.connect
|
191
|
+
end
|
192
|
+
|
193
|
+
# With dependencies
|
194
|
+
lazy_attr_reader :configured_service, depends_on: [:config] do
|
195
|
+
ConfiguredService.new(config)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
manager = ServiceManager.new
|
200
|
+
manager.expensive_service_computed? # => false
|
201
|
+
manager.expensive_service # Creates service
|
202
|
+
manager.expensive_service_computed? # => true
|
203
|
+
manager.reset_expensive_service! # Resets for re-computation
|
204
|
+
```
|
205
|
+
|
206
|
+
### Class Variables
|
207
|
+
```ruby
|
208
|
+
lazy_class_variable(name, **options, &block)
|
209
|
+
```
|
210
|
+
Defines a thread-safe lazy-initialized class variable shared across all instances.
|
211
|
+
#### Parameters:
|
212
|
+
|
213
|
+
- Same as lazy_attr_reader
|
214
|
+
|
215
|
+
#### Generated Methods:
|
216
|
+
|
217
|
+
- Class-level: ClassName.#{name}, ClassName.#{name}\_computed?, ClassName.reset\_#{name}!
|
218
|
+
- Instance-level: #{name}, #{name}\_computed?, reset\_#{name}! (delegates to class)
|
219
|
+
|
220
|
+
#### Example:
|
221
|
+
```ruby
|
222
|
+
class DatabaseManager
|
223
|
+
extend LazyInit
|
224
|
+
|
225
|
+
lazy_class_variable :connection_pool do
|
226
|
+
ConnectionPool.new(size: 20, timeout: 30)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Shared across all instances
|
231
|
+
db1 = DatabaseManager.new
|
232
|
+
db2 = DatabaseManager.new
|
233
|
+
db1.connection_pool.equal?(db2.connection_pool) # => true
|
234
|
+
|
235
|
+
# Class-level access
|
236
|
+
DatabaseManager.connection_pool # Direct access
|
237
|
+
DatabaseManager.reset_connection_pool! # Reset for all instances
|
238
|
+
```
|
239
|
+
|
240
|
+
### Instance Methods
|
241
|
+
#### Include LazyInit (instead of extending) to get instance-level utilities:
|
242
|
+
```ruby
|
243
|
+
class DataProcessor
|
244
|
+
include LazyInit # Note: include, not extend
|
245
|
+
end
|
246
|
+
```
|
247
|
+
**lazy(&block)**
|
248
|
+
|
249
|
+
Creates a standalone lazy value container.
|
250
|
+
```ruby
|
251
|
+
def expensive_calculation
|
252
|
+
result = lazy { perform_heavy_computation }
|
253
|
+
result.value
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
|
258
|
+
__lazy_once(**options, &block)__
|
259
|
+
|
260
|
+
Method-scoped lazy initialization with automatic cache key generation.
|
261
|
+
#### Parameters:
|
262
|
+
|
263
|
+
- max_entries (Integer): Maximum cache entries before LRU eviction
|
264
|
+
- ttl (Numeric): Time-to-live in seconds for cache entries
|
265
|
+
|
266
|
+
#### Example:
|
267
|
+
``` ruby
|
268
|
+
class DataAnalyzer
|
269
|
+
include LazyInit
|
270
|
+
|
271
|
+
def analyze_data(dataset_id)
|
272
|
+
lazy_once(ttl: 5.minutes, max_entries: 100) do
|
273
|
+
expensive_analysis(dataset_id)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
**clear_lazy_once_values!**
|
280
|
+
|
281
|
+
Clears all cached lazy_once values for the instance.
|
282
|
+
|
283
|
+
**lazy_once_statistics**
|
284
|
+
|
285
|
+
Returns cache statistics for debugging and monitoring.
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
stats = processor.lazy_once_statistics
|
289
|
+
# => {
|
290
|
+
# total_entries: 25,
|
291
|
+
# computed_entries: 25,
|
292
|
+
# total_accesses: 150,
|
293
|
+
# average_accesses: 6.0,
|
294
|
+
# oldest_entry: 2025-07-01 10:00:00,
|
295
|
+
# newest_entry: 2024-07-01 10:30:00
|
296
|
+
# }
|
297
|
+
```
|
298
|
+
|
299
|
+
### Configuration
|
300
|
+
Configure global behavior:
|
301
|
+
```ruby
|
302
|
+
LazyInit.configure do |config|
|
303
|
+
config.default_timeout = 30
|
304
|
+
config.max_lazy_once_entries = 5000
|
305
|
+
config.lazy_once_ttl = 1.hour
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
#### Configuration Options:
|
310
|
+
|
311
|
+
- **default_timeout**: Default timeout for all lazy attributes (default: nil)
|
312
|
+
- **max_lazy_once_entries**: Maximum entries in lazy_once cache (default: 1000)
|
313
|
+
- **lazy_once_ttl**: Default TTL for lazy_once entries (default: nil)
|
314
|
+
|
315
|
+
## Advanced Usage
|
316
|
+
### Dependency Resolution
|
317
|
+
#### LazyInit automatically resolves dependencies in the correct order:
|
318
|
+
```ruby
|
319
|
+
rubyclass ComplexService
|
320
|
+
extend LazyInit
|
321
|
+
|
322
|
+
lazy_attr_reader :config do
|
323
|
+
load_configuration
|
324
|
+
end
|
325
|
+
|
326
|
+
lazy_attr_reader :database, depends_on: [:config] do
|
327
|
+
Database.connect(config.database_url)
|
328
|
+
end
|
329
|
+
|
330
|
+
lazy_attr_reader :cache, depends_on: [:config] do
|
331
|
+
Cache.new(config.cache_settings)
|
332
|
+
end
|
333
|
+
|
334
|
+
lazy_attr_reader :processor, depends_on: [:database, :cache] do
|
335
|
+
DataProcessor.new(database: database, cache: cache)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
service = ComplexService.new
|
340
|
+
service.processor # Resolves: config → database & cache → processor
|
341
|
+
```
|
342
|
+
|
343
|
+
#### Circular Dependency Detection:
|
344
|
+
```ruby
|
345
|
+
lazy_attr_reader :service_a, depends_on: [:service_b] do
|
346
|
+
"A"
|
347
|
+
end
|
348
|
+
|
349
|
+
lazy_attr_reader :service_b, depends_on: [:service_a] do
|
350
|
+
"B"
|
351
|
+
end
|
352
|
+
|
353
|
+
service.service_a # Raises: LazyInit::DependencyError
|
354
|
+
```
|
355
|
+
|
356
|
+
### Timeout Protection
|
357
|
+
#### Protect against hanging computations:
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
class ExternalService
|
361
|
+
extend LazyInit
|
362
|
+
|
363
|
+
lazy_attr_reader :slow_api, timeout: 5 do
|
364
|
+
HTTPClient.get('http://very-slow-api.com/data')
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
service = ExternalService.new
|
369
|
+
begin
|
370
|
+
service.slow_api
|
371
|
+
rescue LazyInit::TimeoutError => e
|
372
|
+
puts "API call timed out: #{e.message}"
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
### Memory Management
|
377
|
+
#### LazyInit includes sophisticated memory management:
|
378
|
+
```ruby
|
379
|
+
class MemoryAwareService
|
380
|
+
include LazyInit
|
381
|
+
|
382
|
+
def process_data(data_id)
|
383
|
+
# Automatic cleanup when cache grows too large
|
384
|
+
lazy_once(max_entries: 50, ttl: 10.minutes) do
|
385
|
+
expensive_data_processing(data_id)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def cleanup!
|
390
|
+
clear_lazy_once_values! # Manual cleanup
|
391
|
+
end
|
392
|
+
end
|
393
|
+
```
|
394
|
+
|
395
|
+
## Common Use Cases
|
396
|
+
#### Rails Application Services
|
397
|
+
```ruby
|
398
|
+
class UserService
|
399
|
+
extend LazyInit
|
400
|
+
|
401
|
+
lazy_attr_reader :redis_client do
|
402
|
+
Redis.new(url: Rails.application.credentials.redis_url)
|
403
|
+
end
|
404
|
+
|
405
|
+
lazy_class_variable :connection_pool do
|
406
|
+
ConnectionPool.new(size: Rails.env.production? ? 20 : 5)
|
407
|
+
end
|
408
|
+
|
409
|
+
lazy_attr_reader :email_service, depends_on: [:redis_client] do
|
410
|
+
EmailService.new(cache: redis_client)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
#### Background Jobs
|
416
|
+
```ruby
|
417
|
+
class ImageProcessorJob
|
418
|
+
extend LazyInit
|
419
|
+
|
420
|
+
lazy_attr_reader :image_processor do
|
421
|
+
ImageProcessor.new(memory_limit: '512MB')
|
422
|
+
end
|
423
|
+
|
424
|
+
lazy_attr_reader :cloud_storage, timeout: 10 do
|
425
|
+
CloudStorage.new(credentials: ENV['CLOUD_CREDENTIALS'])
|
426
|
+
end
|
427
|
+
|
428
|
+
def perform(image_id)
|
429
|
+
processed = image_processor.process(image_id)
|
430
|
+
cloud_storage.upload(processed)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
```
|
434
|
+
#### Microservices
|
435
|
+
```ruby
|
436
|
+
class PaymentService
|
437
|
+
extend LazyInit
|
438
|
+
|
439
|
+
lazy_attr_reader :config do
|
440
|
+
ServiceConfig.load('payment_service')
|
441
|
+
end
|
442
|
+
|
443
|
+
lazy_attr_reader :database, depends_on: [:config] do
|
444
|
+
Database.connect(config.database_url)
|
445
|
+
end
|
446
|
+
|
447
|
+
lazy_attr_reader :payment_gateway, depends_on: [:config], timeout: 15 do
|
448
|
+
PaymentGateway.new(
|
449
|
+
api_key: config.payment_api_key,
|
450
|
+
environment: config.environment
|
451
|
+
)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
### Rails Concerns
|
456
|
+
```ruby
|
457
|
+
module Cacheable
|
458
|
+
extend ActiveSupport::Concern
|
459
|
+
|
460
|
+
included do
|
461
|
+
extend LazyInit
|
462
|
+
|
463
|
+
lazy_attr_reader :cache_client do
|
464
|
+
Rails.cache
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
def cached_method(key)
|
469
|
+
lazy_once(ttl: 1.hour) do
|
470
|
+
expensive_computation(key)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
```
|
475
|
+
## Error Handling
|
476
|
+
LazyInit provides predictable error behavior:
|
477
|
+
```ruby
|
478
|
+
class ServiceWithErrors
|
479
|
+
extend LazyInit
|
480
|
+
|
481
|
+
lazy_attr_reader :failing_service do
|
482
|
+
raise StandardError, "Service unavailable"
|
483
|
+
end
|
484
|
+
|
485
|
+
lazy_attr_reader :timeout_service, timeout: 1 do
|
486
|
+
sleep(5) # Will timeout
|
487
|
+
"Success"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
service = ServiceWithErrors.new
|
492
|
+
|
493
|
+
# Exceptions are cached and re-raised consistently
|
494
|
+
begin
|
495
|
+
service.failing_service
|
496
|
+
rescue StandardError => e
|
497
|
+
puts "First call: #{e.message}"
|
498
|
+
end
|
499
|
+
|
500
|
+
begin
|
501
|
+
service.failing_service # Same exception re-raised (cached)
|
502
|
+
rescue StandardError => e
|
503
|
+
puts "Second call: #{e.message}" # Same exception instance
|
504
|
+
end
|
505
|
+
|
506
|
+
# Check error state
|
507
|
+
service.failing_service_computed? # => false (failed computation)
|
508
|
+
|
509
|
+
# Reset allows retry
|
510
|
+
service.reset_failing_service!
|
511
|
+
service.failing_service # => Attempts computation again
|
512
|
+
|
513
|
+
# Timeout errors
|
514
|
+
begin
|
515
|
+
service.timeout_service
|
516
|
+
rescue LazyInit::TimeoutError => e
|
517
|
+
puts "Timeout: #{e.message}"
|
518
|
+
# Subsequent calls raise the same timeout error
|
519
|
+
end
|
520
|
+
```
|
521
|
+
#### Exception Types
|
522
|
+
```ruby
|
523
|
+
LazyInit::Error # Base error class
|
524
|
+
LazyInit::InvalidAttributeNameError # Invalid attribute names
|
525
|
+
LazyInit::TimeoutError # Timeout exceeded
|
526
|
+
LazyInit::DependencyError # Circular dependencies
|
527
|
+
Performance
|
528
|
+
LazyInit is optimized for production use:
|
529
|
+
```
|
530
|
+
## Performance
|
531
|
+
|
532
|
+
Realistic benchmark results (x86_64-darwin19, Ruby 3.0.2):
|
533
|
+
|
534
|
+
- Initial computation: ~identical (LazyInit setup overhead negligible)
|
535
|
+
- Cached access: 3.5x slower than manual ||=
|
536
|
+
-100,000 calls: Manual 13ms, LazyInit 45ms
|
537
|
+
- In practice: For expensive operations (5-50ms), the 0.0004ms per call overhead is negligible.
|
538
|
+
- Trade-off: 3.5x cached access cost for 100% thread safety
|
539
|
+
|
540
|
+
[Full details can be found here](https://github.com/N3BCKN/lazy_init/blob/main/benchmarks/benchmark_performance.rb)
|
541
|
+
|
542
|
+
### Optimization Strategies
|
543
|
+
LazyInit automatically selects the best implementation:
|
544
|
+
|
545
|
+
- Simple inline (no dependencies, no timeout): Maximum performance
|
546
|
+
- Optimized dependency (single dependency): Balanced performance
|
547
|
+
- Full LazyValue (complex scenarios): Full feature set
|
548
|
+
|
549
|
+
|
550
|
+
## Thread Safety
|
551
|
+
LazyInit provides comprehensive thread safety guarantees:
|
552
|
+
### Thread Safety Features
|
553
|
+
|
554
|
+
- Double-checked locking for optimal performance
|
555
|
+
- Per-attribute mutexes to avoid global locks
|
556
|
+
- Atomic state transitions to prevent race conditions
|
557
|
+
- Exception safety with proper cleanup
|
558
|
+
|
559
|
+
#### Example: Concurrent Access
|
560
|
+
```ruby
|
561
|
+
class ThreadSafeService
|
562
|
+
extend LazyInit
|
563
|
+
|
564
|
+
lazy_attr_reader :shared_resource do
|
565
|
+
puts "Creating resource in thread #{Thread.current.object_id}"
|
566
|
+
ExpensiveResource.new
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
service = ThreadSafeService.new
|
571
|
+
|
572
|
+
# Multiple threads accessing the same attribute
|
573
|
+
threads = 10.times.map do |i|
|
574
|
+
Thread.new do
|
575
|
+
puts "Thread #{i}: #{service.shared_resource.object_id}"
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
threads.each(&:join)
|
580
|
+
# Output: All threads get the same object_id (single computation)
|
581
|
+
```
|
582
|
+
### Thread Safety Deep Dive
|
583
|
+
LazyInit uses several techniques to ensure thread safety:
|
584
|
+
|
585
|
+
- Double-checked locking: Fast path avoids synchronization after computation
|
586
|
+
- Per-attribute mutexes: No global locks that could cause bottlenecks
|
587
|
+
- Atomic state transitions: Prevents race conditions during computation
|
588
|
+
- Exception safety: Proper cleanup even when computations fail
|
589
|
+
|
590
|
+
[Full report with benchmark here](https://github.com/N3BCKN/lazy_init/blob/main/benchmarks/benchmark_threads.rb)
|
591
|
+
|
592
|
+
#### Thread Safety benchmark
|
593
|
+
- 200 concurrent requests: 0 race conditions
|
594
|
+
- 6,000+ operations/second sustained throughput
|
595
|
+
- Complex dependency chains: 100% reliable
|
596
|
+
- Zero-downtime resets: 100% success rate
|
597
|
+
- Tested on Ruby 3.0.2, macOS (Intel)
|
598
|
+
|
599
|
+
## Compatibility
|
600
|
+
|
601
|
+
- Ruby: 2.6, 2.7, 3.0, 3.1, 3.2, 3.3+
|
602
|
+
- Rails: 5.2+ (optional, no Rails dependency required)
|
603
|
+
- Thread-safe: Yes, across all Ruby implementations (MRI, JRuby, TruffleRuby)
|
604
|
+
- Ractor-safe: Planned for future versions
|
605
|
+
- Versioning: LazyInit follows semantic versioning
|
606
|
+
|
607
|
+
## Testing
|
608
|
+
|
609
|
+
#### RSpec Integration
|
610
|
+
```ruby
|
611
|
+
RSpec.describe UserService do
|
612
|
+
let(:service) { UserService.new }
|
613
|
+
|
614
|
+
describe '#expensive_calculation' do
|
615
|
+
it 'computes value lazily' do
|
616
|
+
expect(service.expensive_calculation_computed?).to be false
|
617
|
+
|
618
|
+
result = service.expensive_calculation
|
619
|
+
expect(result).to be_a(String)
|
620
|
+
expect(service.expensive_calculation_computed?).to be true
|
621
|
+
end
|
622
|
+
|
623
|
+
it 'returns same value on multiple calls' do
|
624
|
+
first_call = service.expensive_calculation
|
625
|
+
second_call = service.expensive_calculation
|
626
|
+
|
627
|
+
expect(first_call).to be(second_call) # Same object
|
628
|
+
end
|
629
|
+
|
630
|
+
it 'can be reset for fresh computation' do
|
631
|
+
old_value = service.expensive_calculation
|
632
|
+
service.reset_expensive_calculation!
|
633
|
+
new_value = service.expensive_calculation
|
634
|
+
|
635
|
+
expect(new_value).not_to be(old_value)
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
```
|
640
|
+
#### Test Helpers
|
641
|
+
```ruby
|
642
|
+
# Custom helpers for testing
|
643
|
+
module LazyInitTestHelpers
|
644
|
+
def reset_all_lazy_attributes(object)
|
645
|
+
object.class.lazy_initializers.each_key do |attr_name|
|
646
|
+
object.send("reset_#{attr_name}!")
|
647
|
+
end
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
RSpec.configure do |config|
|
652
|
+
config.include LazyInitTestHelpers
|
653
|
+
end
|
654
|
+
```
|
655
|
+
|
656
|
+
#### Rails Testing Considerations
|
657
|
+
```ruby
|
658
|
+
# In Rails, be careful with class variables during code reloading
|
659
|
+
RSpec.configure do |config|
|
660
|
+
config.before(:each) do
|
661
|
+
# Reset class variables in development/test
|
662
|
+
MyService.reset_connection_pool! if defined?(MyService)
|
663
|
+
end
|
664
|
+
end
|
665
|
+
```
|
666
|
+
## Migration Guide
|
667
|
+
#### From Manual ||= Pattern
|
668
|
+
Before:
|
669
|
+
```ruby
|
670
|
+
class LegacyService
|
671
|
+
def config
|
672
|
+
@config ||= YAML.load_file('config.yml')
|
673
|
+
end
|
674
|
+
|
675
|
+
def database
|
676
|
+
@database ||= Database.connect(config['url'])
|
677
|
+
end
|
678
|
+
end
|
679
|
+
```
|
680
|
+
After:
|
681
|
+
```ruby
|
682
|
+
class ModernService
|
683
|
+
extend LazyInit
|
684
|
+
|
685
|
+
lazy_attr_reader :config do
|
686
|
+
YAML.load_file('config.yml')
|
687
|
+
end
|
688
|
+
|
689
|
+
lazy_attr_reader :database, depends_on: [:config] do
|
690
|
+
Database.connect(config['url'])
|
691
|
+
end
|
692
|
+
end
|
693
|
+
```
|
694
|
+
|
695
|
+
### Migration Benefits
|
696
|
+
|
697
|
+
- Thread safety: Automatic protection against race conditions
|
698
|
+
- Dependency management: Explicit dependency declaration
|
699
|
+
- Error handling: Built-in timeout and exception management
|
700
|
+
- Testing: Easier state management in tests
|
701
|
+
|
702
|
+
### Gradual Migration Strategy
|
703
|
+
|
704
|
+
- Start with new lazy attributes using LazyInit
|
705
|
+
- Identify critical thread-unsafe ||= patterns
|
706
|
+
- Convert high-risk areas first
|
707
|
+
- Add dependency declarations where beneficial
|
708
|
+
- Remove manual patterns over time
|
709
|
+
|
710
|
+
## When NOT to Use LazyInit
|
711
|
+
Consider alternatives in these scenarios:
|
712
|
+
|
713
|
+
- Simple value caching where manual ||= suffices and thread safety isn't needed
|
714
|
+
- Performance-critical hot paths in tight loops where every microsecond counts
|
715
|
+
- Single-threaded applications with basic caching needs
|
716
|
+
- Primitive value caching (strings, numbers, booleans) where overhead outweighs benefits
|
717
|
+
- Very simple Rails applications without complex service layers
|
718
|
+
|
719
|
+
## FAQ
|
720
|
+
Q: How does performance compare to other approaches?
|
721
|
+
|
722
|
+
A: Compared to manual mutex-based solutions, LazyInit provides better developer experience with competitive performance. See benchmarks for detailed comparison with manual ||= patterns.
|
723
|
+
|
724
|
+
Q: Can I use this in Rails initializers?
|
725
|
+
|
726
|
+
A: Yes, but be careful with class variables in development mode due to code reloading.
|
727
|
+
|
728
|
+
Q: What happens during Rails code reloading?
|
729
|
+
|
730
|
+
A: Instance attributes are automatically reset. Class variables may need manual reset in development.
|
731
|
+
|
732
|
+
Q: Is there any memory overhead?
|
733
|
+
|
734
|
+
A: Minimal - about 1 mutex + 3 instance variables per lazy attribute.
|
735
|
+
|
736
|
+
Q: Can I use lazy_attr_reader with private methods?
|
737
|
+
|
738
|
+
A: Yes, the generated methods respect the same visibility as where they're defined.
|
739
|
+
|
740
|
+
Q: How do I debug dependency resolution issues?
|
741
|
+
|
742
|
+
A: Use YourClass.lazy_initializers to inspect dependency configuration and check for circular dependencies.
|
743
|
+
|
744
|
+
Q: Does this work with inheritance?
|
745
|
+
|
746
|
+
A: Yes, lazy attributes are inherited and can be overridden in subclasses.
|
747
|
+
## Contributing
|
748
|
+
|
749
|
+
1. Fork the repository
|
750
|
+
2. Create your feature branch (git checkout -b my-new-feature)
|
751
|
+
3. Write tests for your changes
|
752
|
+
4. Ensure all tests pass (bundle exec rspec)
|
753
|
+
5. Commit your changes (git commit -am 'Add some feature')
|
754
|
+
6. Push to the branch (git push origin my-new-feature)
|
755
|
+
7. Create a Pull Request
|
756
|
+
|
757
|
+
## Development Setup
|
758
|
+
```bash
|
759
|
+
git clone https://github.com/N3BCKN/lazy_init.git
|
760
|
+
cd lazy_init
|
761
|
+
bundle install
|
762
|
+
bundle exec rspec # Run tests
|
763
|
+
```
|
764
|
+
## License
|
765
|
+
The gem is available as open source under the terms of the MIT License.
|