memorb 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/.circleci/config.yml +52 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +7 -0
- data/README.md +258 -0
- data/Rakefile +4 -0
- data/lib/memorb.rb +34 -0
- data/lib/memorb/agent.rb +30 -0
- data/lib/memorb/errors.rb +11 -0
- data/lib/memorb/integration.rb +277 -0
- data/lib/memorb/integrator_class_methods.rb +44 -0
- data/lib/memorb/key_value_store.rb +88 -0
- data/lib/memorb/method_identifier.rb +33 -0
- data/lib/memorb/ruby_compatibility.rb +55 -0
- data/lib/memorb/version.rb +5 -0
- data/memorb.gemspec +27 -0
- data/spec/memorb/agent_spec.rb +45 -0
- data/spec/memorb/integration_spec.rb +529 -0
- data/spec/memorb/integrator_class_methods_spec.rb +142 -0
- data/spec/memorb/key_value_store_spec.rb +91 -0
- data/spec/memorb/method_identifier_spec.rb +84 -0
- data/spec/memorb_spec.rb +198 -0
- data/spec/spec_helper.rb +69 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
# Memorb
|
2
|
+
|
3
|
+
Memoize instance methods with ease.
|
4
|
+
|
5
|
+
[](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`.
|
data/Rakefile
ADDED
data/lib/memorb.rb
ADDED
@@ -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
|
data/lib/memorb/agent.rb
ADDED
@@ -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,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
|