memorb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9724352c954ae84fd4004bc0cdc534fd71c847fdd5ceeb241a1b92855cd3374f
4
+ data.tar.gz: 3d1b910ae44c6680eb4ae5a997800630da89bffed0f851849b32beedfbbeb257
5
+ SHA512:
6
+ metadata.gz: 62d3610c4f5591da319e16bef054ad79acc32329c4ea95e2a9fe793b1124e3059fdf6743a19130e1c6fd5a380bd387c3b0ea8b67b39e3fc13c01c4880b1b23ce
7
+ data.tar.gz: ece13fcec090a805cd010217e85d6ff3b42868156c47856f58781dc79faa1edb30eb88b06da0039869aac93cc59229c86c98c3c2d308376c39cf970538622983
@@ -0,0 +1,52 @@
1
+ _: &steps
2
+ - checkout
3
+ - run:
4
+ name: Bundle
5
+ command: |
6
+ gem install bundler
7
+ bundle install
8
+ - run:
9
+ name: RSpec
10
+ command: bundle exec rspec
11
+
12
+ version: 2
13
+ jobs:
14
+ ruby-2.6:
15
+ docker:
16
+ - image: circleci/ruby:2.6
17
+ steps: *steps
18
+ ruby-2.5:
19
+ docker:
20
+ - image: circleci/ruby:2.5
21
+ steps: *steps
22
+ ruby-2.4:
23
+ docker:
24
+ - image: circleci/ruby:2.4
25
+ steps: *steps
26
+ ruby-2.3:
27
+ docker:
28
+ - image: circleci/ruby:2.3
29
+ steps: *steps
30
+ jruby-9.2:
31
+ docker:
32
+ - image: circleci/jruby:9.2
33
+ steps: *steps
34
+ jruby-9.1:
35
+ docker:
36
+ - image: circleci/jruby:9.1
37
+ steps: *steps
38
+ jruby-9.0:
39
+ docker:
40
+ - image: circleci/jruby:9
41
+ steps: *steps
42
+ workflows:
43
+ version: 2
44
+ rubies:
45
+ jobs:
46
+ - ruby-2.6
47
+ - ruby-2.5
48
+ - ruby-2.4
49
+ - ruby-2.3
50
+ - jruby-9.2
51
+ - jruby-9.1
52
+ - jruby-9.0
@@ -0,0 +1,4 @@
1
+ /pkg
2
+ *.gem
3
+ /.bundle
4
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --order random
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ source :rubygems
4
+ gemspec
@@ -0,0 +1,7 @@
1
+ Copyright 2019 Patrick Rebsch
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,258 @@
1
+ # Memorb
2
+
3
+ Memoize instance methods with ease.
4
+
5
+ [![CircleCI](https://circleci.com/gh/pjrebsch/memorb/tree/master.svg?style=svg)](https://circleci.com/gh/pjrebsch/memorb/tree/master)
6
+
7
+ ## Overview
8
+
9
+ Below is a contrived example class that could benefit from memoization.
10
+
11
+ ```ruby
12
+ class WeekForecast
13
+
14
+ def initialize(date:)
15
+ @date = date
16
+ end
17
+
18
+ def data
19
+ API.get '/weather/week', { date: @date.iso8601 }
20
+ end
21
+
22
+ def week_days
23
+ Date::ABBR_DAYNAMES.rotate(@date.wday)
24
+ end
25
+
26
+ def rain_on?(day)
27
+ percent_chance = data.dig('days', day.to_s, 'rain')
28
+ percent_chance > 75 if percent_chance
29
+ end
30
+
31
+ def will_rain?
32
+ week_days.any? { |wd| rain_on? wd }
33
+ end
34
+
35
+ end
36
+ ```
37
+
38
+ All of its instance methods could be memoized to save them from unnecessary recomputation or I/O.
39
+
40
+ A common way of accomplishing memoization is to save the result in an instance variable:
41
+
42
+ ```ruby
43
+ @will_rain ||= ...
44
+ ```
45
+
46
+ But that approach is problematic for expressions that return a falsey value as the instance variable will be overlooked on subsequent evaluations. Often, the solution for this case is to instead check whether or not the instance variable has been previously defined:
47
+
48
+ ```ruby
49
+ defined?(@will_rain) ? @will_rain : @will_rain = ...
50
+ ```
51
+
52
+ While this does address the issue of falsey values, this is significantly more verbose. And neither of these approaches take into consideration the arguments to the method, which a method like `rain_on?` in the above example class would need to function properly.
53
+
54
+ Memorb exists to make memoization in these cases much easier to implement. Simply register the methods with Memorb on the class and you're done:
55
+
56
+ ```ruby
57
+ class WeekForecast
58
+ extend Memorb
59
+
60
+ memorb! def data
61
+ ...
62
+ end
63
+
64
+ memorb! def week_days
65
+ ...
66
+ end
67
+
68
+ memorb! def rain_on?(day)
69
+ ...
70
+ end
71
+
72
+ memorb! def will_rain?
73
+ ...
74
+ end
75
+ end
76
+ ```
77
+
78
+ These methods' return values will now be memoized for each instance of `WeekForecast`. The `rain_on?` method will memoize its return values based on the arguments supplied to it (in this case one argument since that's all it accepts), and the other methods will each memoize their single, independent return value.
79
+
80
+ ## Usage
81
+
82
+ First, integrate Memorb into a class with `extend Memorb`. Then, use the `memorb!` class method to register instance methods for memoization.
83
+
84
+ ### Integrating Class Methods
85
+
86
+ These methods are available as class methods on the integrating class.
87
+
88
+ #### `memorb!`
89
+
90
+ Use this method to register instance methods for memoization. When a method is both registered and defined, Memorb will override it. Once the method is overridden, it's considered "enabled" for memoization. On initial invocation with a given set of arguments, the method's return value is cached based on the given arguments and returned. Then, subsequent invocations of that method with the same arguments return the cached value.
91
+
92
+ Internally, calls to the overriding method implementation are serialized with a read-write lock to guarantee that the initial method call is not subject to a race condition between threads, while also optimizing the performance of concurrent reads of the cached result.
93
+
94
+ ##### Prefix form
95
+
96
+ Conveniently, methods defined using the `def` keyword return the method name, so the method definition can just be prefixed with a registration directive. This approach helps make apparent the fact that the method is being memoized when reading the method.
97
+
98
+ ```ruby
99
+ memorb! def data
100
+ ...
101
+ end
102
+ ```
103
+
104
+ If you prefer `def` and `end` to align, you can move `memorb!` up to a new line and escape the line break. The Memorb registration methods require arguments, so if you forget to escape the line break, you'll be made aware with an exception when the class is loaded.
105
+
106
+ ```ruby
107
+ memorb! \
108
+ def data
109
+ ...
110
+ end
111
+ ```
112
+
113
+ ##### List form
114
+
115
+ If you wish to enumerate the methods to register all at once, or don't have access to a method's implementation source to use the Prefix form, you can supply a list of method names instead.
116
+
117
+ ```ruby
118
+ memorb! :data, :week_days, :rain_on?, :will_rain?
119
+ ```
120
+
121
+ Typos are a potential problem. Memorb can't know when a registered method's definition is going to occur, so if you mistype the name of a method you intend to define later, Memorb will anticipate that method's definition indefinitely and the method that you intended to register won't end up being memoized. The Prefix form is recommended for this reason.
122
+
123
+ If you do use this form, you can check that all registered methods were enabled by validating that `memorb.disabled_methods` is empty, which might be a valuable addition in a test suite.
124
+
125
+ ##### Block form
126
+
127
+ Instead of listing out method names or decorating their definitions, you can just define them within a block.
128
+
129
+ ```ruby
130
+ memorb! do
131
+ def data
132
+ ...
133
+ end
134
+ def week_days
135
+ ...
136
+ end
137
+ def rain_on?(day)
138
+ ...
139
+ end
140
+ def will_rain?
141
+ ...
142
+ end
143
+ end
144
+ ```
145
+
146
+ Just be careful not to accidentally include any other methods that must always execute!
147
+
148
+ It is also important to note that all instance methods that are defined while the block is executing will be registered, not necessarily just the ones that can be seen using the `def` keyword. This is also not thread-safe, so if you are defining methods concurrently (which you shouldn't be), you may risk registering methods you didn't intend to register.
149
+
150
+ #### `memorb`
151
+
152
+ Returns the `Memorb::Integration` instance for the integrating class.
153
+
154
+ ### Integration Methods
155
+
156
+ These methods are available on the `Memorb::Integration` instance for an integrating class.
157
+
158
+ #### `register`
159
+
160
+ Alias of `memorb!`.
161
+
162
+ #### `registered_methods`
163
+
164
+ Returns the names of methods that have been registered for the integrating class.
165
+
166
+ #### `registered?(method_name)`
167
+
168
+ Returns whether or not the specified method is registered.
169
+
170
+ #### `enable(method_name)` / `disable(method_name)`
171
+
172
+ Enable/Disable a registered method.
173
+
174
+ #### `enabled?(method_name)`
175
+
176
+ Returns whether or not the specified method is enabled.
177
+
178
+ #### `enabled_methods` / `disabled_methods`
179
+
180
+ Returns which methods are registered and enabled/disabled for the integrating class.
181
+
182
+ ### Instance Methods
183
+
184
+ These methods are available to instances of the integrating class.
185
+
186
+ #### `memorb`
187
+
188
+ Returns the `Memorb::Agent` for the object instance.
189
+
190
+ ## Advisories
191
+
192
+ ### Cache Explosion
193
+
194
+ No, sorry, not [the show](https://www.cashexplosionshow.com/).
195
+
196
+ Because memoization trades computation for memory, there is potential for memory explosion with a method that accepts arguments. All distinct sets of arguments to a method will map to a return value, and this mapping will be stored, so the potential for explosion increases exponentially as more arguments are supported. As long as the method is guaranteed to be called with a small, finite set of arguments, this needn't be much of a concern. But if the method is expected to handle arbitrary arguments or a large range of values, you may want to handle caching at a lower level within the method or even abandon the memoization/caching approach altogether.
197
+
198
+ The `rain_on?` method in the example class represents a method that is subject to this. It can also be used as an example of how to handle caching at a lower level. The only valid arguments to it are a representation of the seven days of the week, so there need only ever be up to seven cache entries. The day might not always be passed as a string—it could be anything that responds to `to_s`. The logic of the method doesn't care because it always transforms the argument to a string, but Memorb can't know what values for that argument the method's logic would consider to be the same thing, so it would cache them as distinct values. A solution is to perform "argument normalization" and use the results of that to implement caching within the method:
199
+
200
+ ```ruby
201
+ def rain_on?(day)
202
+ day = day.to_s
203
+ return unless week_days.include?(day)
204
+ memorb.fetch([__method__, day]) do
205
+ ...
206
+ end
207
+ end
208
+ ```
209
+
210
+ Obviously, this method doesn't benefit much from a caching approach in the first place: computation already needs to be done to achieve argument normalization and the actual logic for the method is quite lightweight. Methods that take arguments may not be good candidates for memoization because the explosion problem may represent too big a risk for the benefits that caching would provide, but this is a judgment call to be made per case.
211
+
212
+ ### Blocks are not considered distinguishing arguments
213
+
214
+ Memorb ignores block arguments when determining whether or not a method has been called with the same arguments. It doesn't matter if a block is provided explicitly (using `&block` as a parameter), provided implicitly (using `yield` in the method body), or not provided at all. Therefore, blocks should not be used to distinguish otherwise equivalent method calls for the sake of memoization.
215
+
216
+ However, a `Proc` can be passed as a normal argument and it _will_ be used in distinguishing method calls.
217
+
218
+ ### Redefining an enabled method
219
+
220
+ Redefining a method that Memorb has already overridden can be done. Since Memorb's override of the method is of greater precedence, Memorb will continue to work for the method. But if you are doing this, you'll want to read this section to understand what behavior to expect.
221
+
222
+ Any return values from previous executions of the method will remain in Memorb's cache even after the method has been redefined. If the method was redefined in a way that return values from the old definition no longer make sense for the application, then you can clear the cache after redefining the method.
223
+
224
+ If redefinining the method changes its class visibility, see the next section.
225
+
226
+ ### Changing the visibility of an enabled method
227
+
228
+ If you change the visibility of an enabled method, Memorb won't automatically know that it needs to change the visibility of its corresponding override, so the visibility change will appear to have not worked because Memorb's override takes precedence. Memorb is unable to reliably override the visibility modifier for a class to detect such changes on its own (see [this Ruby not-a-bug report](https://bugs.ruby-lang.org/issues/16100)). You're advised to avoid doing this.
229
+
230
+ ### Aliasing overridden methods
231
+
232
+ Using `alias_method` in Ruby will create a copy of the method implementation found at that time. This means that the aliased method will have different behavior relative to when the method was overridden by Memorb. If the method was aliased before override by Memorb, then its calls will not reference the cache of the original method, but if aliased after the override, then such calls will reference the cache.
233
+
234
+ ### Alias method chaining on overridden methods
235
+
236
+ If you or another library uses alias method chaining on a method that Memorb has overridden, you will experience infinite recursion upon calling that method. See [this article](https://blog.newrelic.com/engineering/ruby-agent-module-prepend-alias-method-chains/) for an explanation of the incompatibility between using `Module#prepend` (which Memorb uses internally) with the alias method chaining technique. Refactoring such alias method chaining in the integrating class to instead use `Module#prepend` will prevent this issue.
237
+
238
+ ### Potential for initial method invocation race
239
+
240
+ If you are relying on Memorb's serialization for method invocation to prevent multiple executions of a method body across threads, then you should read this section.
241
+
242
+ Memorb overrides a registered method only once that method has been defined. To prevent `respond_to?` from returning true for an instance prematurely or allowing the method to be called prematurely, Memorb must wait until after the method is officially defined. There is no way to hook into Ruby's method definition process (in pure Ruby), so Memorb can only know of a method definition event after it has occurred using Ruby's provided notification methods.
243
+
244
+ This means that there is a small window of time between when a registered method is originally defined and when Memorb overrides it with memoization support. For methods that are registered and defined within the initial class definition, this shouldn't be a problem because there should be no instantiations of the class before its initial definition is closed. But methods that are defined dynamically may be able to be called by another thread before Memorb has had a chance to override them.
245
+
246
+ ## Potential Enhancements
247
+
248
+ ### Ability to configure Memorb
249
+
250
+ It could be beneficial to configure Memorb, though the options for configuration are unclear.
251
+
252
+ ### Ability to log cache accesses
253
+
254
+ Caching introduces the possibility of bugs when things are cached too much. It would be helpful for debugging to be able to configure a `Logger` for cache accesses.
255
+
256
+ ### Alternative to instance variables
257
+
258
+ Expanding Memorb to more than just memoization could include providing enhanced features for local instance state, such as capturing parameters during `initialize`.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'memorb/version'
4
+ require_relative 'memorb/ruby_compatibility'
5
+ require_relative 'memorb/errors'
6
+ require_relative 'memorb/method_identifier'
7
+ require_relative 'memorb/key_value_store'
8
+ require_relative 'memorb/integrator_class_methods'
9
+ require_relative 'memorb/integration'
10
+ require_relative 'memorb/agent'
11
+
12
+ module Memorb
13
+ class << self
14
+
15
+ def extended(target)
16
+ Integration.integrate_with!(target)
17
+ end
18
+
19
+ def included(*)
20
+ _raise_invalid_integration_error!
21
+ end
22
+
23
+ def prepended(*)
24
+ _raise_invalid_integration_error!
25
+ end
26
+
27
+ private
28
+
29
+ def _raise_invalid_integration_error!
30
+ raise InvalidIntegrationError, 'Memorb must be integrated using `extend`'
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Memorb
4
+ class Agent
5
+
6
+ def initialize(id)
7
+ @id = id
8
+ @store = KeyValueStore.new
9
+ end
10
+
11
+ attr_reader :id
12
+
13
+ def method_store
14
+ store.fetch(:methods) { KeyValueStore.new }
15
+ end
16
+
17
+ def value_store
18
+ store.fetch(:value) { KeyValueStore.new }
19
+ end
20
+
21
+ def fetch(key, &block)
22
+ value_store.fetch(key.hash, &block)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :store
28
+
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Memorb
4
+
5
+ class InvalidIntegrationError < ::StandardError
6
+ end
7
+
8
+ class MismatchedTargetError < ::StandardError
9
+ end
10
+
11
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module Memorb
6
+ module Integration
7
+ class << self
8
+
9
+ def integrate_with!(target)
10
+ unless target.is_a?(::Class)
11
+ raise InvalidIntegrationError, 'integration target must be a class'
12
+ end
13
+ INTEGRATIONS.fetch(target) do
14
+ new(target).tap do |integration|
15
+ target.singleton_class.prepend(IntegratorClassMethods)
16
+ target.prepend(integration)
17
+ end
18
+ end
19
+ end
20
+
21
+ def integrated?(target)
22
+ INTEGRATIONS.has?(target)
23
+ end
24
+
25
+ def [](integrator)
26
+ INTEGRATIONS.read(integrator)
27
+ end
28
+
29
+ private
30
+
31
+ INTEGRATIONS = KeyValueStore.new
32
+
33
+ def new(integrator)
34
+ mixin = ::Module.new do
35
+ def initialize(*)
36
+ agent = Integration[self.class].create_agent(self)
37
+ define_singleton_method(:memorb) { agent }
38
+ super
39
+ end
40
+
41
+ class << self
42
+
43
+ def register(*names, &block)
44
+ names_present = !names.empty?
45
+ block_present = !block.nil?
46
+
47
+ if names_present && block_present
48
+ raise ::ArgumentError,
49
+ 'register may not be called with both a method name and a block'
50
+ elsif names_present
51
+ names.flatten.each { |n| _register_from_name(_identifier(n)) }
52
+ elsif block_present
53
+ _register_from_block(&block)
54
+ else
55
+ raise ::ArgumentError,
56
+ 'register must be called with either a method name or a block'
57
+ end
58
+
59
+ nil
60
+ end
61
+
62
+ def registered_methods
63
+ _identifiers_to_symbols(_registrations.keys)
64
+ end
65
+
66
+ def registered?(name)
67
+ _registered?(_identifier(name))
68
+ end
69
+
70
+ def enable(name)
71
+ _enable(_identifier(name))
72
+ end
73
+
74
+ def disable(name)
75
+ _disable(_identifier(name))
76
+ end
77
+
78
+ def enabled_methods
79
+ _identifiers_to_symbols(_overrides.keys)
80
+ end
81
+
82
+ def disabled_methods
83
+ registered_methods - enabled_methods
84
+ end
85
+
86
+ def enabled?(name)
87
+ _enabled?(_identifier(name))
88
+ end
89
+
90
+ def purge(name)
91
+ _purge(_identifier(name))
92
+ end
93
+
94
+ def auto_register?
95
+ _auto_registration.value > 0
96
+ end
97
+
98
+ def auto_register!(&block)
99
+ raise ::ArgumentError, 'a block must be provided' if block.nil?
100
+ _auto_registration.update { |v| [0, v].max + 1 }
101
+ begin
102
+ block.call
103
+ ensure
104
+ _auto_registration.update { |v| [0, v - 1].max }
105
+ end
106
+ end
107
+
108
+ def prepended(target)
109
+ _check_target!(target)
110
+ super
111
+ end
112
+
113
+ def included(*)
114
+ raise InvalidIntegrationError,
115
+ 'an integration must be applied with `prepend`, not `include`'
116
+ end
117
+
118
+ def name
119
+ [:name, :inspect, :object_id].each do |m|
120
+ next unless integrator.respond_to?(m)
121
+ base_name = integrator.public_send(m)
122
+ return "Memorb:#{ base_name }" if base_name
123
+ end
124
+ end
125
+
126
+ alias_method :inspect, :name
127
+
128
+ # Never save reference to the integrator instance or it may
129
+ # never be garbage collected!
130
+ def create_agent(integrator_instance)
131
+ Agent.new(integrator_instance.object_id).tap do |agent|
132
+ _agents.write(agent.id, agent)
133
+
134
+ # The proc must not be made here because it would save a
135
+ # reference to `integrator_instance`.
136
+ finalizer = _agent_finalizer(agent.id)
137
+ ::ObjectSpace.define_finalizer(integrator_instance, finalizer)
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def _check_target!(target)
144
+ unless target.equal?(integrator)
145
+ raise MismatchedTargetError
146
+ end
147
+ end
148
+
149
+ def _identifier(name)
150
+ MethodIdentifier.new(name)
151
+ end
152
+
153
+ def _identifiers_to_symbols(method_ids)
154
+ method_ids.map(&:to_sym)
155
+ end
156
+
157
+ def _register_from_name(method_id)
158
+ _registrations.write(method_id, nil)
159
+ _enable(method_id)
160
+ end
161
+
162
+ def _register_from_block(&block)
163
+ auto_register! do
164
+ integrator.class_eval(&block)
165
+ end
166
+ end
167
+
168
+ def _registered?(method_id)
169
+ _registrations.keys.include?(method_id)
170
+ end
171
+
172
+ def _enable(method_id)
173
+ return unless _registered?(method_id)
174
+
175
+ visibility = _integrator_instance_method_visibility(method_id)
176
+ return if visibility.nil?
177
+
178
+ _overrides.fetch(method_id) do
179
+ _define_override(method_id)
180
+ _set_visibility(visibility, method_id.to_sym)
181
+ end
182
+ end
183
+
184
+ def _disable(method_id)
185
+ _overrides.forget(method_id)
186
+ _remove_override(method_id)
187
+ end
188
+
189
+ def _enabled?(method_id)
190
+ _overrides.keys.include?(method_id)
191
+ end
192
+
193
+ def _purge(method_id)
194
+ _agents.keys.each do |id|
195
+ agent = _agents.read(id)
196
+ store = agent&.method_store&.read(method_id)
197
+ store&.reset!
198
+ end
199
+ end
200
+
201
+ def _remove_override(method_id)
202
+ # Ruby will raise an exception if the method doesn't exist.
203
+ # Catching it is the safest thing to do for thread-safety.
204
+ # The alternative would be to check the list if it were
205
+ # present or not, but the read could be outdated by the time
206
+ # that we tried to remove the method and this exception
207
+ # wouldn't be caught.
208
+ remove_method(method_id.to_sym)
209
+ rescue ::NameError => e
210
+ # If this exception was for something else, it should be re-raised.
211
+ unless RubyCompatibility.name_error_matches(e, method_id, self)
212
+ raise e
213
+ end
214
+ end
215
+
216
+ def _define_override(method_id)
217
+ define_method(method_id.to_sym) do |*args, &block|
218
+ memorb.method_store
219
+ .fetch(method_id) { KeyValueStore.new }
220
+ .fetch(args.hash) { super(*args, &block) }
221
+ end
222
+ end
223
+
224
+ def _integrator_instance_method_visibility(method_id)
225
+ [:public, :protected, :private].find do |visibility|
226
+ methods = integrator.send(:"#{ visibility }_instance_methods")
227
+ methods.include?(method_id.to_sym)
228
+ end
229
+ end
230
+
231
+ def _set_visibility(visibility, name)
232
+ send(visibility, name)
233
+ visibility
234
+ end
235
+
236
+ def _agent_finalizer(agent_id)
237
+ # This must not be a lambda proc, otherwise GC hangs!
238
+ ::Proc.new { _agents.forget(agent_id) }
239
+ end
240
+
241
+ def _registrations
242
+ RubyCompatibility.module_constant(self, :registrations)
243
+ end
244
+
245
+ def _overrides
246
+ RubyCompatibility.module_constant(self, :overrides)
247
+ end
248
+
249
+ def _agents
250
+ RubyCompatibility.module_constant(self, :agents)
251
+ end
252
+
253
+ def _auto_registration
254
+ RubyCompatibility.module_constant(self, :auto_registration)
255
+ end
256
+
257
+ end
258
+ end
259
+
260
+ RubyCompatibility.module_constant_set(mixin, :registrations, KeyValueStore.new)
261
+ RubyCompatibility.module_constant_set(mixin, :overrides, KeyValueStore.new)
262
+ RubyCompatibility.module_constant_set(mixin, :agents, KeyValueStore.new)
263
+ RubyCompatibility.module_constant_set(mixin,
264
+ :auto_registration,
265
+ ::Concurrent::AtomicFixnum.new,
266
+ )
267
+
268
+ RubyCompatibility.define_method(mixin.singleton_class, :integrator) do
269
+ integrator
270
+ end
271
+
272
+ mixin
273
+ end
274
+
275
+ end
276
+ end
277
+ end