lazy_init 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyInit
4
+ VERSION = '0.1.0'
5
+ 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: []