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,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module LazyInit
|
6
|
+
# Thread-safe container for lazy-initialized values with performance-optimized access.
|
7
|
+
#
|
8
|
+
# LazyValue provides a thread-safe wrapper around expensive computations that should
|
9
|
+
# only be executed once. It uses a double-checked locking pattern with an ultra-fast
|
10
|
+
# hot path that avoids synchronization overhead after initial computation.
|
11
|
+
#
|
12
|
+
# The implementation separates the fast path (simple instance variable access) from
|
13
|
+
# the slow path (computation with full synchronization) for optimal performance in
|
14
|
+
# the common case where values are accessed repeatedly after computation.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# lazy_value = LazyValue.new do
|
18
|
+
# expensive_database_query
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# result = lazy_value.value # computes once
|
22
|
+
# result = lazy_value.value # returns cached value
|
23
|
+
#
|
24
|
+
# @example With timeout protection
|
25
|
+
# lazy_value = LazyValue.new(timeout: 5) do
|
26
|
+
# slow_external_api_call
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# begin
|
30
|
+
# result = lazy_value.value
|
31
|
+
# rescue LazyInit::TimeoutError
|
32
|
+
# puts "API call timed out"
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @since 0.1.0
|
36
|
+
class LazyValue
|
37
|
+
# Create a new lazy value container.
|
38
|
+
#
|
39
|
+
# The computation block will be executed at most once when value() is first called.
|
40
|
+
# Subsequent calls to value() return the cached result without re-executing the block.
|
41
|
+
#
|
42
|
+
# @param timeout [Numeric, nil] optional timeout in seconds for the computation
|
43
|
+
# @param block [Proc] the computation to execute lazily
|
44
|
+
# @raise [ArgumentError] if no block is provided
|
45
|
+
def initialize(timeout: nil, &block)
|
46
|
+
raise ArgumentError, 'Block is required' unless block
|
47
|
+
|
48
|
+
@block = block
|
49
|
+
@timeout = timeout
|
50
|
+
@mutex = Mutex.new
|
51
|
+
@computed = false
|
52
|
+
@value = nil
|
53
|
+
@exception = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get the computed value, executing the block if necessary.
|
57
|
+
#
|
58
|
+
# Uses an optimized double-checked locking pattern: the hot path (after computation)
|
59
|
+
# requires only a single instance variable check and direct return. The cold path
|
60
|
+
# (during computation) handles full synchronization and error management.
|
61
|
+
#
|
62
|
+
# If the computation raises an exception, that exception is cached and re-raised
|
63
|
+
# on subsequent calls to maintain consistent behavior.
|
64
|
+
#
|
65
|
+
# @return [Object] the computed value
|
66
|
+
# @raise [TimeoutError] if computation exceeds the configured timeout
|
67
|
+
# @raise [StandardError] any exception raised by the computation block
|
68
|
+
def value
|
69
|
+
# hot path: optimized for repeated access after computation
|
70
|
+
# this should be nearly as fast as direct instance variable access
|
71
|
+
return @value if @computed
|
72
|
+
|
73
|
+
# cold path: handle computation and synchronization
|
74
|
+
compute_or_raise_exception
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check if the value has been successfully computed.
|
78
|
+
#
|
79
|
+
# Returns false if computation hasn't started, failed with an exception,
|
80
|
+
# or was reset. Only returns true for successful computations.
|
81
|
+
#
|
82
|
+
# @return [Boolean] true if value has been computed without errors
|
83
|
+
def computed?
|
84
|
+
@computed && @exception.nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reset the lazy value to its initial uncomputed state.
|
88
|
+
#
|
89
|
+
# Clears the cached value and any cached exceptions, allowing the computation
|
90
|
+
# to be re-executed on the next call to value(). This method is thread-safe.
|
91
|
+
#
|
92
|
+
# @return [void]
|
93
|
+
def reset!
|
94
|
+
@mutex.synchronize do
|
95
|
+
@computed = false
|
96
|
+
@value = nil
|
97
|
+
@exception = nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if the computation resulted in a cached exception.
|
102
|
+
#
|
103
|
+
# This method is thread-safe and can be used to determine if value()
|
104
|
+
# will raise an exception without actually triggering it.
|
105
|
+
#
|
106
|
+
# @return [Boolean] true if an exception is cached
|
107
|
+
def exception?
|
108
|
+
@mutex.synchronize { !@exception.nil? }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Access the cached exception if one exists.
|
112
|
+
#
|
113
|
+
# Returns the actual exception object that was raised during computation,
|
114
|
+
# or nil if no exception occurred or computation hasn't been attempted.
|
115
|
+
#
|
116
|
+
# @return [Exception, nil] the cached exception or nil
|
117
|
+
def exception
|
118
|
+
@mutex.synchronize { @exception }
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Handle computation and exception management in the slow path.
|
124
|
+
#
|
125
|
+
# This method is separated from the main value() method to keep the hot path
|
126
|
+
# as minimal and fast as possible. It handles all the complex logic around
|
127
|
+
# synchronization, timeout management, and exception caching.
|
128
|
+
#
|
129
|
+
# @return [Object] the computed value
|
130
|
+
# @raise [Exception] any exception from computation (after caching)
|
131
|
+
def compute_or_raise_exception
|
132
|
+
@mutex.synchronize do
|
133
|
+
# double-check pattern: another thread might have computed while we waited for the lock
|
134
|
+
return @value if @computed
|
135
|
+
|
136
|
+
# if a previous computation failed, re-raise the cached exception
|
137
|
+
raise @exception if @exception
|
138
|
+
|
139
|
+
begin
|
140
|
+
# execute computation with optional timeout protection
|
141
|
+
computed_value = if @timeout
|
142
|
+
Timeout.timeout(@timeout) do
|
143
|
+
@block.call
|
144
|
+
end
|
145
|
+
else
|
146
|
+
@block.call
|
147
|
+
end
|
148
|
+
|
149
|
+
# atomic state update: set value first, then mark as computed
|
150
|
+
# this ensures @computed is only true when @value contains valid data
|
151
|
+
@value = computed_value
|
152
|
+
@computed = true
|
153
|
+
|
154
|
+
computed_value
|
155
|
+
rescue Timeout::Error => e
|
156
|
+
# wrap timeout errors in our custom exception type for consistency
|
157
|
+
@exception = LazyInit::TimeoutError.new("Lazy initialization timed out after #{@timeout}s")
|
158
|
+
raise @exception
|
159
|
+
rescue StandardError => e
|
160
|
+
# cache any other exceptions for consistent re-raising behavior
|
161
|
+
@exception = e
|
162
|
+
raise
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/lazy_init.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lazy_init/version'
|
4
|
+
require_relative 'lazy_init/lazy_value'
|
5
|
+
require_relative 'lazy_init/class_methods'
|
6
|
+
require_relative 'lazy_init/instance_methods'
|
7
|
+
require_relative 'lazy_init/errors'
|
8
|
+
require_relative 'lazy_init/configuration'
|
9
|
+
require_relative 'lazy_init/dependency_resolver'
|
10
|
+
|
11
|
+
# Thread-safe lazy initialization patterns for Ruby
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# class ApiClient
|
15
|
+
# extend LazyInit
|
16
|
+
#
|
17
|
+
# lazy_attr_reader :connection do
|
18
|
+
# HTTPClient.new(api_url)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example Class-level shared resources
|
23
|
+
# class DatabaseManager
|
24
|
+
# extend LazyInit
|
25
|
+
#
|
26
|
+
# lazy_class_variable :connection_pool do
|
27
|
+
# ConnectionPool.new(size: 20)
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
module LazyInit
|
31
|
+
# Called when LazyInit is included in a class
|
32
|
+
# Adds both class and instance methods
|
33
|
+
#
|
34
|
+
# @param base [Class] the class including this module
|
35
|
+
def self.included(base)
|
36
|
+
base.extend(ClassMethods)
|
37
|
+
base.include(InstanceMethods)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Called when LazyInit is extended by a class
|
41
|
+
# Adds only class methods
|
42
|
+
#
|
43
|
+
# @param base [Class] the class extending this module
|
44
|
+
def self.extended(base)
|
45
|
+
base.extend(ClassMethods)
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lazy_init
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Konstanty Koszewski
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: benchmark-ips
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.50.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.50.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '13.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13.0'
|
83
|
+
description: Provides thread-safe lazy initialization with clean, Ruby-idiomatic API.
|
84
|
+
Eliminates race conditions in lazy attribute initialization while maintaining performance.
|
85
|
+
email:
|
86
|
+
- ka.koszewski@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- CHANGELOG.md
|
94
|
+
- GEMFILE
|
95
|
+
- LICENSE
|
96
|
+
- RAKEFILE
|
97
|
+
- README.md
|
98
|
+
- benchmarks/benchmark.rb
|
99
|
+
- benchmarks/benchmark_performance.rb
|
100
|
+
- benchmarks/benchmark_threads.rb
|
101
|
+
- benchmarks/bottleneck_searcher.rb
|
102
|
+
- benchmarks/thread_safety_verification.rb
|
103
|
+
- lazy_init.gemspec
|
104
|
+
- lib/lazy_init.rb
|
105
|
+
- lib/lazy_init/class_methods.rb
|
106
|
+
- lib/lazy_init/configuration.rb
|
107
|
+
- lib/lazy_init/dependency_resolver.rb
|
108
|
+
- lib/lazy_init/errors.rb
|
109
|
+
- lib/lazy_init/instance_methods.rb
|
110
|
+
- lib/lazy_init/lazy_value.rb
|
111
|
+
- lib/lazy_init/version.rb
|
112
|
+
homepage: https://github.com/N3BCKN/lazy_init
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata:
|
116
|
+
homepage_uri: https://github.com/N3BCKN/lazy_init
|
117
|
+
source_code_uri: https://github.com/N3BCKN/lazy_init
|
118
|
+
changelog_uri: https://github.com/N3BCKN/lazy_init/blob/main/CHANGELOG.md
|
119
|
+
bug_tracker_uri: https://github.com/N3BCKN/lazy_init/issues
|
120
|
+
documentation_uri: https://rubydoc.info/gems/lazy_init
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '2.6'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubygems_version: 3.0.9
|
137
|
+
signing_key:
|
138
|
+
specification_version: 4
|
139
|
+
summary: Thread-safe lazy initialization patterns for Ruby
|
140
|
+
test_files: []
|