attr_memoized 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/Rakefile +6 -0
- data/attr_memoized.gemspec +32 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/attr_memoized.rb +224 -0
- data/lib/attr_memoized/version.rb +3 -0
- metadata +174 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Konstantin Gredeskoul
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/kigster/attr_memoized.svg?branch=master)](https://travis-ci.org/kigster/attr_memoized)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/kigster/attr_memoized/badges/gpa.svg)](https://codeclimate.com/github/kigster/attr_memoized)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/kigster/attr_memoized/badges/coverage.svg)](https://codeclimate.com/github/kigster/attr_memoized/coverage)
|
4
|
+
[![Issue Count](https://codeclimate.com/github/kigster/attr_memoized/badges/issue_count.svg)](https://codeclimate.com/github/kigster/attr_memoized)
|
5
|
+
|
6
|
+
# AttrMemoized
|
7
|
+
|
8
|
+
This is a simple, and yet rather useful **memoization** library, with a specific goal of being **thread-safe** during lazy-loading of attributes. Class method `attr_memoized` automatically generates attribute reader and attribute writer methods. The reader performs a thread-safe lazy-initialization of each attribute. The writer performs a thread-safe assignment. You can disable writer method generation by passing `writer: false` option to `attr_memoized` method.
|
9
|
+
|
10
|
+
Any `attr_memoized` attribute may depend on any number of regular attributes or other `attr_memoized` attributes.
|
11
|
+
|
12
|
+
This gems provides a shorthand syntax for defining lazy-initialized variables as "one-liners", while additionally providing thread-safety guarantees around lazy-initilization of attributes, or attribute assignments.
|
13
|
+
|
14
|
+
#### Caveat
|
15
|
+
|
16
|
+
Note, that if the initialization or assignment returns a "falsey" result (ie, `false` or `nil`), then the attribute will attempt to be re-initialized every time its "reader" method is called. This is not a bug. We treat falsey value as uninitialized by design.
|
17
|
+
|
18
|
+
## Complete Example
|
19
|
+
|
20
|
+
Below we have a `Configuration` class that has several attributes that are all lazy loaded.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require 'redis'
|
24
|
+
# Save config to Redis
|
25
|
+
r = Redis.new
|
26
|
+
#=> #<Redis:0x007fbd8d3a4308>
|
27
|
+
r.set('config_file', '{ "host": "127.0.0.1" }')
|
28
|
+
#=> OK
|
29
|
+
r.set('another_file', '{ "host": "google.com" }')
|
30
|
+
#=> OK
|
31
|
+
r.get('config_file') #=> { "host": "127.0.0.1" }
|
32
|
+
|
33
|
+
require 'attr_memoized'
|
34
|
+
module Concurrent
|
35
|
+
class RedisConfig
|
36
|
+
include AttrMemoized
|
37
|
+
# Now there is an instance and a class methods +#mutex+ are defined.
|
38
|
+
# We also have an instance method +with_lock+, and a class method
|
39
|
+
# +attr_memoized+
|
40
|
+
attr_memoized :contents, -> { redis.get(redis_key) }
|
41
|
+
attr_memoized :redis, -> { Redis.new }
|
42
|
+
attr_memoized :redis_key, -> { 'config_file' }
|
43
|
+
|
44
|
+
def reload_config!(new_key)
|
45
|
+
# +with_lock+ method if offered in place of +synchronize+
|
46
|
+
# to avoid double-locking within the same thread.
|
47
|
+
with_lock do
|
48
|
+
self.redis_key = new_key
|
49
|
+
contents(reload: true)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@config = Concurrent::RedisConfig.new
|
56
|
+
@config.contents
|
57
|
+
#=> { "host": "127.0.0.1" }
|
58
|
+
@config.reload_config!('another_file')
|
59
|
+
#=> { "host": "google.com" }
|
60
|
+
@config.contents
|
61
|
+
#=> { "host": "google.com" }
|
62
|
+
```
|
63
|
+
|
64
|
+
### The Problem
|
65
|
+
|
66
|
+
One of the issues with memoization in multi-threaded environment is that it may lead to unexpected or undefined behavior, due to the situation known as a [_race condition_](https://stackoverflow.com/questions/34510/what-is-a-race-condition).
|
67
|
+
|
68
|
+
Consider the following example:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class Account
|
72
|
+
def self.owner
|
73
|
+
# Slow expensive query
|
74
|
+
@owner ||= ActiveRecord::Base.execute('select ...').first
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# Let's be dangerous:
|
78
|
+
[ Thread.new { Account.owner },
|
79
|
+
Thread.new { Account.owner } ].map(&:join)
|
80
|
+
```
|
81
|
+
|
82
|
+
Ruby evaluates `a||=b` as `a || a=b`, which means that the assignment above won't happen if `a` is "falsey", ie. `false` or `nil`. If the method `self.owner` is not synchronized, then both threads will execute the expensive query, and only the result of the query executed by the second thread will be saved in `@owner`, even though by that time it will already have a value assigned by the first thread, that by that time had already completed.
|
83
|
+
|
84
|
+
Most memoization gems out there, among those that the author had reviewed, did not seem to be concerned with thread safety, which is actually OK under wide ranging situations, particularly if the objects are not meant to be shared across threads.
|
85
|
+
|
86
|
+
But in multi-threaded applications it's important to protect initializers of expensive resources, which is exactly what this library attempts to accomplish.
|
87
|
+
|
88
|
+
|
89
|
+
## Usage
|
90
|
+
|
91
|
+
`AttrMemoized` — the gem's primary module, when included, decorates the receiver with several useful
|
92
|
+
methods:
|
93
|
+
|
94
|
+
* Pre-initialized class method `#mutex`. Each class that includes `AttrMemoized` gets their own mutex.
|
95
|
+
|
96
|
+
* Pre-initialized instance method `#mutex`. Each instance of the class gets it's own dedicated mutex.
|
97
|
+
|
98
|
+
* Convenience method `#with_lock` is provided in place of `#mutex.synhronize` and should be used to wrap any state changes to the class in order to guard against concurrent modification by other threads. It will only use `mutex.synchronize` once per thread, to avoid self-deadlocking.
|
99
|
+
|
100
|
+
* New class method `#attr_memoized` is added, with the following syntax:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
attr_memoized :attribute, [ :attribute, ...], -> { block returning a value } # Proc
|
104
|
+
attr_memoized :attribute, [ :attribute, ...], :instance_method # symbol
|
105
|
+
attr_memoized :attribute, [ :attribute, ...], SomeClass.method(:method_name) # Method instance
|
106
|
+
```
|
107
|
+
|
108
|
+
* In the above definitions:
|
109
|
+
* If a `Proc` is provided as an initializer, it will be called via `#instance_exec` method on the instance and, therefore, can access any public or private method of the instance without the need for `self.` receiver.
|
110
|
+
|
111
|
+
* If the initializer is a `Symbol`, it is expected to be an instance method name, of a method that takes no arguments.
|
112
|
+
|
113
|
+
* Finally, any `Method` instance can also be used.
|
114
|
+
|
115
|
+
* Note, that multiple attribute names can be passed to `#attr_memoized`, and they will be lazy-loaded in the order of access and independently of each other. If the block always returns the same exactly value, then the list may be viewed as aliases. But if the block returns a new value each time its called, then each attribute will be initialized with a different value, eg:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
srand
|
119
|
+
require 'attr_memoized'
|
120
|
+
class RandomNumberGenerator
|
121
|
+
include AttrMemoized
|
122
|
+
attr_memoized :random1,
|
123
|
+
:random2,
|
124
|
+
:random3, -> { rand(2**64) }
|
125
|
+
end
|
126
|
+
|
127
|
+
rng = RandomNumberGenerator.new
|
128
|
+
# each is initialized as it's called, and so they
|
129
|
+
# are all different:
|
130
|
+
rng.random1 #=> 1304594275874777789
|
131
|
+
rng.random2 #=> 12671375021040220422
|
132
|
+
rng.random3 #=> 16656281832060271071
|
133
|
+
|
134
|
+
# second time, they are all already memoized:
|
135
|
+
rng.random1 #=> 1304594275874777789
|
136
|
+
rng.random2 #=> 12671375021040220422
|
137
|
+
rng.random3 #=> 16656281832060271071
|
138
|
+
```
|
139
|
+
|
140
|
+
|
141
|
+
## Installation
|
142
|
+
|
143
|
+
Add this line to your application's Gemfile:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
gem 'attr_memoized'
|
147
|
+
```
|
148
|
+
|
149
|
+
And then execute:
|
150
|
+
|
151
|
+
$ bundle
|
152
|
+
|
153
|
+
Or install it yourself as:
|
154
|
+
|
155
|
+
$ gem install attr_memoized
|
156
|
+
|
157
|
+
|
158
|
+
## Development
|
159
|
+
|
160
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
161
|
+
|
162
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
163
|
+
|
164
|
+
## Contributing
|
165
|
+
|
166
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/kigster/attr_memoized](https://github.com/kigster/attr_memoized).
|
167
|
+
|
168
|
+
## License
|
169
|
+
|
170
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'attr_memoized/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'attr_memoized'
|
8
|
+
spec.version = AttrMemoized::VERSION
|
9
|
+
spec.authors = ['Konstantin Gredeskoul']
|
10
|
+
spec.email = ['kig@reinvent.one']
|
11
|
+
|
12
|
+
spec.summary = %q{Memoize attributes in a thread-safe way. This ruby gem adds a `#attr_memoized` class method, that provides a lazy-loading mechanism for initializing "heavy" attributes, but in a thread-safe way. Instances thus created can be shared among threads.}
|
13
|
+
spec.description = %q{Memoize attributes in a thread-safe way. This ruby gem adds a `#attr_memoized` class method, that provides a lazy-loading mechanism for initializing "heavy" attributes, but in a thread-safe way. Instances thus created can be shared among threads.}
|
14
|
+
spec.homepage = 'https://github.com/kigster/attr_memoized'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1'
|
25
|
+
spec.add_development_dependency 'rake', '~> 12'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
27
|
+
spec.add_development_dependency 'rspec-its'
|
28
|
+
spec.add_development_dependency 'simplecov'
|
29
|
+
spec.add_development_dependency 'irbtools'
|
30
|
+
spec.add_development_dependency 'colored2'
|
31
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
32
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "attr_memoized"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'attr_memoized/version'
|
2
|
+
require 'thread'
|
3
|
+
# This module, when included, decorates the receiver with several useful
|
4
|
+
# methods:
|
5
|
+
#
|
6
|
+
# * Both class and instance methods #mutex are added, that can be used
|
7
|
+
# to guard any shared resources. Each class gets their own class mutex,
|
8
|
+
# and each instance gets it's own separate mutex.
|
9
|
+
#
|
10
|
+
# * new class method #attr_memoized is added, the syntax is as follows:
|
11
|
+
#
|
12
|
+
# attr_memoized :attribute_name, ..., -> { block returning a value }
|
13
|
+
#
|
14
|
+
# * the block in the definition above is called via #instance_exec on the
|
15
|
+
# object (instance of a class) and therefore has access to all private
|
16
|
+
# methods. If the value is a symbol, it is expected to be a method.
|
17
|
+
#
|
18
|
+
# * multiple attribute names are allowed in the #attr_memoized, but they
|
19
|
+
# will all be assigned the result of the block (last argument) when called.
|
20
|
+
# Therefore, typically you would use #attr_memoized with one attribute at
|
21
|
+
# a time, unless you want to have several version of the same variable:
|
22
|
+
#
|
23
|
+
# Example
|
24
|
+
# =======
|
25
|
+
#
|
26
|
+
# class RandomNumbers
|
27
|
+
# include AttrMemoized
|
28
|
+
#
|
29
|
+
# attr_memoized :random, -> { rand(10) }, writer: false
|
30
|
+
# attr_memoized :seed, -> { Time.now.to_i % 57 }
|
31
|
+
#
|
32
|
+
# attr_memoized :number1, :number2, -> { self.class.incr! && rand(10) }
|
33
|
+
#
|
34
|
+
# @calls = 0
|
35
|
+
# class << self
|
36
|
+
# attr_reader :calls
|
37
|
+
# def incr!; @calls = 0 unless defined(@calls); @calls += 1 ; end
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @rn = RandomNumbers.new
|
42
|
+
#
|
43
|
+
# # first call executes the block, and caches it
|
44
|
+
# @rn.number1 # => 3
|
45
|
+
# # and it's saved now, and the block is no longer called
|
46
|
+
# @rn.number1 # => 3
|
47
|
+
# @rn.number2 # => 9
|
48
|
+
# @rn.number2 # => 9
|
49
|
+
# # only 2 times did we ever call incr! method
|
50
|
+
# @rn.class.calls # => 2
|
51
|
+
#
|
52
|
+
# # Now #seed is also lazy-loaded, and also cached
|
53
|
+
# @rn.seed # => 34
|
54
|
+
# # And, we can change it thread safely!
|
55
|
+
# @rn.seed = 64; @rn.seed # => 64
|
56
|
+
#
|
57
|
+
# # Not so with :random, which was defined without the writer:
|
58
|
+
# @rn.random # => 8
|
59
|
+
# @rn.random = 34
|
60
|
+
# # => NoMethodError: undefined method `random=' for #<RandomNumbers:0x007ffb28105178>
|
61
|
+
#
|
62
|
+
#
|
63
|
+
module AttrMemoized
|
64
|
+
|
65
|
+
# We are being a bit paranoid here, so yes we are creating
|
66
|
+
# a central lock used to initialize other class-specific mutexes.
|
67
|
+
# This should only be a problem if you are constantly defining new
|
68
|
+
# classes that include +AttrMemoized++
|
69
|
+
LOCK = Mutex.new.freeze unless defined?(LOCK)
|
70
|
+
#
|
71
|
+
# The types of initializers we support.
|
72
|
+
SUPPORTED_INIT_TYPES = [Proc, Method, Symbol]
|
73
|
+
|
74
|
+
class << self
|
75
|
+
# that's obviously a joke. The name, I mean. Duh.
|
76
|
+
attr_accessor :gil
|
77
|
+
|
78
|
+
|
79
|
+
def included(base)
|
80
|
+
base.class_eval do
|
81
|
+
AttrMemoized::LOCK.synchronize do
|
82
|
+
@mutex ||= Mutex.new
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self
|
86
|
+
attr_reader :mutex
|
87
|
+
#
|
88
|
+
# A class method which, for each attribute in the list,
|
89
|
+
# creates a thread-safe memoized reader and writer (unless writer: false)
|
90
|
+
#
|
91
|
+
# Memoized reader accepts <tt>reload: true</tt> as an optional argument,
|
92
|
+
# which, if provided, forces reinitialization of the variable.
|
93
|
+
#
|
94
|
+
# Example:
|
95
|
+
# ==========
|
96
|
+
#
|
97
|
+
# class LazyConnection
|
98
|
+
# include AttrMemoized
|
99
|
+
# attr_memoized :database_connection, -> { ActiveRecord::Base.connection }
|
100
|
+
#
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# LazyConnection.new.database_connection
|
104
|
+
# #=> <ActiveRecord::Connection::PgSQL::Driver:0xff23234f....>
|
105
|
+
#
|
106
|
+
def attr_memoized(*attributes, **opts)
|
107
|
+
attributes = Array[*attributes]
|
108
|
+
block_or_proc = attributes.pop if SUPPORTED_INIT_TYPES.include?(attributes.last.class)
|
109
|
+
attributes.each do |attribute|
|
110
|
+
define_attribute_writer(attribute) unless opts && opts.has_key?(:writer) && opts[:writer].eql?(false)
|
111
|
+
define_attribute_reader(attribute, block_or_proc)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def define_attribute_reader(attribute, block_or_proc)
|
116
|
+
at_attribute = at(attribute)
|
117
|
+
define_method(attribute) do |**opts|
|
118
|
+
read_memoize(attribute, at_attribute, block_or_proc, **opts)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def define_attribute_writer(attribute)
|
123
|
+
at_attribute = at(attribute)
|
124
|
+
define_method("#{attribute}=".to_sym) do |value|
|
125
|
+
block = -> { self.instance_variable_set(at_attribute, value) }
|
126
|
+
already_locked? ?
|
127
|
+
block.call :
|
128
|
+
with_thread_local_lock { mutex.synchronize(&block) }
|
129
|
+
value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Convert an attribute name into an @variable syntax
|
136
|
+
def at(attr)
|
137
|
+
attr = attr.to_sym unless attr.is_a?(Symbol)
|
138
|
+
@attr_cache ||= {}
|
139
|
+
@attr_cache[attr] || (@attr_cache[attr] = "@#{attr}".to_sym)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# instance method: uses the class mutex to create an instance
|
144
|
+
# mutex, and then uses the instance mutex to wrap instance's
|
145
|
+
# state
|
146
|
+
def mutex
|
147
|
+
return @mutex if @mutex
|
148
|
+
self.class.mutex.synchronize {
|
149
|
+
@mutex ||= Mutex.new
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# This public method is offered in place of a local +#mutex+'s
|
157
|
+
# #synchronize method to guard state changes to the object using
|
158
|
+
# object's mutex and a thread-local flag. The flag prevents
|
159
|
+
# duplicate #synchronize within the same thread on the same +mutex+.
|
160
|
+
def with_lock(&block)
|
161
|
+
already_locked? ?
|
162
|
+
block[] :
|
163
|
+
with_thread_local_lock { mutex.synchronize(&block) }
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# This private method is executed in order to initialize a memoized
|
169
|
+
# attribute.
|
170
|
+
#
|
171
|
+
# @param [Symbol] attribute - name of the attribute
|
172
|
+
# @param [Symbol] at_attribute - symbol representing attribute instance variable
|
173
|
+
# @param [Proc, Method, Symbol] block_or_proc - what to call to get the uncached value
|
174
|
+
# @param [Hash] opts - additional options
|
175
|
+
# @option opts [Boolean] :reload - forces re-initialization of the memoized attribute
|
176
|
+
def read_memoize(attribute, at_attribute, block_or_proc, **opts)
|
177
|
+
var = self.instance_variable_get(at_attribute)
|
178
|
+
return var if var && !reload?(opts)
|
179
|
+
with_lock { assign_value(attribute, at_attribute, block_or_proc, **opts) }
|
180
|
+
self.instance_variable_get(at_attribute)
|
181
|
+
end
|
182
|
+
|
183
|
+
# This private method resolves the initializer argument and returns it's result.
|
184
|
+
def assign_value(attribute, at_attribute, block_or_proc, **opts)
|
185
|
+
# reload the value of +var+ because we are now inside a synchronize block
|
186
|
+
var = self.instance_variable_get(at_attribute)
|
187
|
+
return var if (var && !reload?(opts))
|
188
|
+
|
189
|
+
# now call whatever `was defined on +attr_memoized+ to get the actual value
|
190
|
+
case block_or_proc
|
191
|
+
when Symbol
|
192
|
+
send(block_or_proc)
|
193
|
+
when Method
|
194
|
+
block_or_proc.call
|
195
|
+
when Proc
|
196
|
+
instance_exec(&block_or_proc)
|
197
|
+
else
|
198
|
+
raise ArgumentError, "expected one of #{AttrMemoized::SUPPORTED_INIT_TYPES.map(&:to_s).join(', ')} for attribute #{attribute}, got #{block_or_proc.class}"
|
199
|
+
end.tap do |result|
|
200
|
+
self.instance_variable_set(at_attribute, result)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns +true+ if +opts+ contains reload: +true+
|
205
|
+
def reload?(opts)
|
206
|
+
(opts && opts.has_key?(:reload)) ? opts[:reload] : nil
|
207
|
+
end
|
208
|
+
|
209
|
+
# just a key into Thread.local
|
210
|
+
def object_locked_key
|
211
|
+
@key ||= "this.#{object_id}".to_sym
|
212
|
+
end
|
213
|
+
|
214
|
+
def already_locked?
|
215
|
+
Thread.current[object_locked_key]
|
216
|
+
end
|
217
|
+
|
218
|
+
def with_thread_local_lock
|
219
|
+
raise ArgumentError, 'Already locked!' if already_locked?
|
220
|
+
Thread.current[object_locked_key] = true
|
221
|
+
yield if block_given?
|
222
|
+
Thread.current[object_locked_key] = nil
|
223
|
+
end
|
224
|
+
end
|