attr_memoized 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rspec +1 -0
- data/.rubocop.yml +1 -1156
- data/.rubocop_todo.yml +110 -0
- data/.travis.yml +8 -4
- data/.vscode/settings.json +7 -0
- data/Gemfile +2 -0
- data/README.adoc +199 -0
- data/Rakefile +3 -1
- data/attr_memoized.gemspec +13 -10
- data/bin/console +1 -0
- data/lib/attr_memoized.rb +126 -115
- data/lib/attr_memoized/version.rb +3 -1
- metadata +53 -24
- data/README.md +0 -170
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2020-04-30 18:01:43 -0700 using RuboCop version 0.82.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 14
|
10
|
+
# Cop supports --auto-correct.
|
11
|
+
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
12
|
+
# SupportedHashRocketStyles: key, separator, table
|
13
|
+
# SupportedColonStyles: key, separator, table
|
14
|
+
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
15
|
+
Layout/HashAlignment:
|
16
|
+
Exclude:
|
17
|
+
- 'spec/attr_memoized_spec.rb'
|
18
|
+
|
19
|
+
# Offense count: 1
|
20
|
+
# Cop supports --auto-correct.
|
21
|
+
Layout/MultilineBlockLayout:
|
22
|
+
Exclude:
|
23
|
+
- 'spec/support/shared_examples.rb'
|
24
|
+
|
25
|
+
# Offense count: 1
|
26
|
+
# Configuration parameters: IgnoredMethods.
|
27
|
+
Metrics/AbcSize:
|
28
|
+
Max: 36
|
29
|
+
|
30
|
+
# Offense count: 3
|
31
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
32
|
+
# ExcludedMethods: refine
|
33
|
+
Metrics/BlockLength:
|
34
|
+
Max: 65
|
35
|
+
|
36
|
+
# Offense count: 2
|
37
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
38
|
+
Metrics/MethodLength:
|
39
|
+
Max: 48
|
40
|
+
|
41
|
+
# Offense count: 1
|
42
|
+
Naming/AsciiIdentifiers:
|
43
|
+
Exclude:
|
44
|
+
- 'spec/multi_attribute_spec.rb'
|
45
|
+
|
46
|
+
# Offense count: 1
|
47
|
+
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
48
|
+
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
49
|
+
Naming/MemoizedInstanceVariableName:
|
50
|
+
Exclude:
|
51
|
+
- 'lib/attr_memoized.rb'
|
52
|
+
|
53
|
+
# Offense count: 1
|
54
|
+
# Configuration parameters: IgnoredPatterns.
|
55
|
+
# SupportedStyles: snake_case, camelCase
|
56
|
+
Naming/MethodName:
|
57
|
+
EnforcedStyle: snake_case
|
58
|
+
|
59
|
+
# Offense count: 2
|
60
|
+
# Configuration parameters: AllowedChars.
|
61
|
+
Style/AsciiComments:
|
62
|
+
Exclude:
|
63
|
+
- 'lib/attr_memoized.rb'
|
64
|
+
|
65
|
+
# Offense count: 1
|
66
|
+
# Cop supports --auto-correct.
|
67
|
+
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, IgnoredMethods, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
|
68
|
+
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
69
|
+
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
70
|
+
# FunctionalMethods: let, let!, subject, watch
|
71
|
+
# IgnoredMethods: lambda, proc, it
|
72
|
+
Style/BlockDelimiters:
|
73
|
+
Exclude:
|
74
|
+
- 'lib/attr_memoized.rb'
|
75
|
+
|
76
|
+
# Offense count: 1
|
77
|
+
# Cop supports --auto-correct.
|
78
|
+
Style/IfUnlessModifier:
|
79
|
+
Exclude:
|
80
|
+
- 'lib/attr_memoized.rb'
|
81
|
+
|
82
|
+
# Offense count: 1
|
83
|
+
# Cop supports --auto-correct.
|
84
|
+
# Configuration parameters: PreferredDelimiters.
|
85
|
+
Style/PercentLiteralDelimiters:
|
86
|
+
Exclude:
|
87
|
+
- 'spec/support/pet_store.rb'
|
88
|
+
|
89
|
+
# Offense count: 8
|
90
|
+
# Cop supports --auto-correct.
|
91
|
+
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
92
|
+
# SupportedStyles: single_quotes, double_quotes
|
93
|
+
Style/StringLiterals:
|
94
|
+
Exclude:
|
95
|
+
- 'Gemfile'
|
96
|
+
- 'Rakefile'
|
97
|
+
- 'bin/console'
|
98
|
+
- 'spec/multi_attribute_spec.rb'
|
99
|
+
|
100
|
+
# Offense count: 1
|
101
|
+
Style/StructInheritance:
|
102
|
+
Exclude:
|
103
|
+
- 'spec/support/pet_store.rb'
|
104
|
+
|
105
|
+
# Offense count: 24
|
106
|
+
# Cop supports --auto-correct.
|
107
|
+
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
108
|
+
# URISchemes: http, https
|
109
|
+
Layout/LineLength:
|
110
|
+
Max: 271
|
data/.travis.yml
CHANGED
@@ -2,9 +2,13 @@ sudo: false
|
|
2
2
|
language: ruby
|
3
3
|
cache: bundler
|
4
4
|
rvm:
|
5
|
-
-
|
6
|
-
- 2.
|
7
|
-
- 2.
|
8
|
-
|
5
|
+
- 2.4.10
|
6
|
+
- 2.5.8
|
7
|
+
- 2.6.6
|
8
|
+
- 2.7.1
|
9
|
+
- jruby-9.2.11.1
|
10
|
+
before_install:
|
11
|
+
- gem update --system
|
12
|
+
- gem install bundler -v 2.1.4
|
9
13
|
after_success:
|
10
14
|
- bundle exec codeclimate-test-reporter
|
data/Gemfile
CHANGED
data/README.adoc
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
:doctype: book
|
2
|
+
:toc:
|
3
|
+
|
4
|
+
image:https://travis-ci.org/kigster/attr_memoized.svg?branch=master[Build Status,link=https://travis-ci.org/kigster/attr_memoized]
|
5
|
+
image:https://codeclimate.com/github/kigster/attr_memoized/badges/gpa.svg[Code Climate,link=https://codeclimate.com/github/kigster/attr_memoized]
|
6
|
+
image:https://codeclimate.com/github/kigster/attr_memoized/badges/coverage.svg[Test Coverage,link=https://codeclimate.com/github/kigster/attr_memoized/coverage]
|
7
|
+
image:https://codeclimate.com/github/kigster/attr_memoized/badges/issue_count.svg[Issue Count,link=https://codeclimate.com/github/kigster/attr_memoized]
|
8
|
+
|
9
|
+
= AttrMemoized
|
10
|
+
|
11
|
+
This is a simple, and yet powerful *memoization* library, with a specific goal of being *thread-safe* during lazy-loading of expensive to create
|
12
|
+
attributes.
|
13
|
+
Class method
|
14
|
+
`attr_memoized` automatically generates attribute reader and attribute writer methods. The reader performs a thread-safe lazy-initialization of
|
15
|
+
each attribute. The writer performs a thread-safe assignment. You can disable writer method generation by using `attr_memoized_reader` class
|
16
|
+
method instead of the `attr_memoized`.
|
17
|
+
|
18
|
+
This gems provides a shorthand syntax for defining lazy-initialized variables as "one-liners", while additionally providing thread-safety
|
19
|
+
guarantees around lazy-initialization of attributes, or attribute assignments.
|
20
|
+
|
21
|
+
[discrete]
|
22
|
+
WARNING: **Caveat**:
|
23
|
+
If the initialization or assignment returns a "falsey" result (ie, `false` or `nil`), then the attribute will attempt to be re-initialized every
|
24
|
+
time its "reader" method is called. This is not a bug. We treat falsey value as uninitialized by design.
|
25
|
+
|
26
|
+
== Complete Example
|
27
|
+
|
28
|
+
Below we have a `Configuration` class that has several attributes that are all lazy loaded.
|
29
|
+
|
30
|
+
[source,ruby]
|
31
|
+
----
|
32
|
+
require 'redis'
|
33
|
+
require 'attr_memoized'
|
34
|
+
|
35
|
+
module Concurrent
|
36
|
+
class RedisConfig
|
37
|
+
|
38
|
+
include AttrMemoized
|
39
|
+
|
40
|
+
CONTENT_KEY = 'site-content'.freeze
|
41
|
+
|
42
|
+
# This imports instance method #with_lock+, and class methods
|
43
|
+
# #attr_memoized, and #attr_memoized_reader.
|
44
|
+
|
45
|
+
attr_memoized_reader :redis_key, -> { CONTENT_KEY }
|
46
|
+
attr_memoized_reader :redis_config, -> { { host: 'localhost', port: 6379 } }
|
47
|
+
attr_memoized_reader :redis, -> { Redis.new(redis_config) }
|
48
|
+
attr_memoized_reader :contents, -> { redis.get(redis_key) }
|
49
|
+
|
50
|
+
|
51
|
+
# #with_lock method if offered in place of the #synchronize
|
52
|
+
# to avoid double-locking within the same thread.
|
53
|
+
def reload_config!(new_key)
|
54
|
+
with_lock do
|
55
|
+
self.redis_key = new_key
|
56
|
+
contents(reload: true)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
@config = Concurrent::RedisConfig.new
|
63
|
+
@config.contents
|
64
|
+
#=> { "host": "127.0.0.1" }
|
65
|
+
@config.reload_config!('another_file')
|
66
|
+
#=> { "host": "google.com" }
|
67
|
+
@config.contents
|
68
|
+
#=> { "host": "google.com" }
|
69
|
+
----
|
70
|
+
|
71
|
+
=== The Problem
|
72
|
+
|
73
|
+
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 https://stackoverflow.com/questions/34510/what-is-a-race-condition[_race condition_].
|
74
|
+
|
75
|
+
Consider the following example:
|
76
|
+
|
77
|
+
[source,ruby]
|
78
|
+
----
|
79
|
+
class Account
|
80
|
+
def self.owner
|
81
|
+
# Slow expensive query
|
82
|
+
@owner ||= ActiveRecord::Base.execute('select ...').first
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Let's be dangerous:
|
87
|
+
[ Thread.new { Account.owner },
|
88
|
+
Thread.new { Account.owner } ].map(&:join)
|
89
|
+
----
|
90
|
+
|
91
|
+
==== Deeper into the `||=`
|
92
|
+
|
93
|
+
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.
|
94
|
+
|
95
|
+
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.
|
96
|
+
|
97
|
+
But in multi-threaded applications it's important to protect initializers of expensive resources, which is exactly what this library attempts to accomplish.
|
98
|
+
|
99
|
+
== Using `attr_memoized`
|
100
|
+
|
101
|
+
`AttrMemoized` -- the gem's primary module, when included, decorates the receiver with several useful
|
102
|
+
methods:
|
103
|
+
|
104
|
+
* Pre-initialized class method `#attr_memoized_mutex`. Each class that includes `AttrMemoized` gets their own mutex.
|
105
|
+
|
106
|
+
* Pre-initialized instance method `#attr_memoized_mutex`. Each instance of the class gets it's own dedicated mutex.
|
107
|
+
|
108
|
+
* Convenience method `#with_lock` is provided in place of `#attr_memoized_mutex.synchronize` 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.
|
109
|
+
|
110
|
+
* New class method `#attr_memoized` is added, with the following syntax:
|
111
|
+
|
112
|
+
[source,ruby]
|
113
|
+
----
|
114
|
+
attr_memoized :attr, [ :aliases, ], -> { block returning a value } # A proc
|
115
|
+
attr_memoized :attr, [ :aliases, ], :instance_method, arg1: value, ... # A symbol
|
116
|
+
attr_memoized :attr, [ :aliases, ], SomeClass.method(:method_name) # A method
|
117
|
+
----
|
118
|
+
|
119
|
+
* In the above definitions:
|
120
|
+
** 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.
|
121
|
+
|
122
|
+
** If the initializer is a `Symbol`, it is expected to be an instance method name, of a method that accepts keyword arguments - in other words the methods should always have `**opts` as the last argument, even if you are not using them.
|
123
|
+
+
|
124
|
+
*** The reason for this is that you can supply arguments to methods when defining lazy initializations, for instance — take a look at the definition of `pi25` in the provided example `NumericHelper` below.
|
125
|
+
|
126
|
+
** Finally, any `Method` instance can also be used.
|
127
|
+
|
128
|
+
** 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:
|
129
|
+
|
130
|
+
[source,ruby]
|
131
|
+
----
|
132
|
+
Kernel.srand # init random numbers
|
133
|
+
require 'attr_memoized'
|
134
|
+
require 'bigdecimal/math'
|
135
|
+
|
136
|
+
class NumericHelper
|
137
|
+
include AttrMemoized
|
138
|
+
attr_memoized :random1,
|
139
|
+
:random2,
|
140
|
+
:random3, -> { rand(2**64) }
|
141
|
+
|
142
|
+
attr_memoized :pi, :π # call a class method when accessed
|
143
|
+
|
144
|
+
# Returns PI as a string with digits.
|
145
|
+
def self.π(digits: 25)
|
146
|
+
precision = digits
|
147
|
+
result = BigMath.PI(precision)
|
148
|
+
result = result.truncate(precision).to_s
|
149
|
+
result = result[2..-1] # Remove '0.'
|
150
|
+
result = result.split('e').first # Remove 'e1'
|
151
|
+
result.insert(1, '.')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
rng = NumericHelper.new
|
156
|
+
# each is initialized as it's called, and so they
|
157
|
+
# are all different:
|
158
|
+
rng.random1 #=> 1304594275874777789
|
159
|
+
rng.random2 #=> 12671375021040220422
|
160
|
+
rng.random3 #=> 16656281832060271071
|
161
|
+
|
162
|
+
# second time, they are all already memoized:
|
163
|
+
rng.random1 #=> 1304594275874777789
|
164
|
+
rng.random2 #=> 12671375021040220422
|
165
|
+
rng.random3 #=> 16656281832060271071
|
166
|
+
|
167
|
+
rng.pi #=>
|
168
|
+
----
|
169
|
+
|
170
|
+
== Installation
|
171
|
+
|
172
|
+
Add this line to your application's Gemfile:
|
173
|
+
|
174
|
+
[source,ruby]
|
175
|
+
----
|
176
|
+
gem 'attr_memoized'
|
177
|
+
----
|
178
|
+
|
179
|
+
And then execute:
|
180
|
+
|
181
|
+
$ bundle
|
182
|
+
|
183
|
+
Or install it yourself as:
|
184
|
+
|
185
|
+
$ gem install attr_memoized
|
186
|
+
|
187
|
+
== Development
|
188
|
+
|
189
|
+
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.
|
190
|
+
|
191
|
+
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 https://rubygems.org[rubygems.org].
|
192
|
+
|
193
|
+
== Contributing
|
194
|
+
|
195
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/attr_memoized.
|
196
|
+
|
197
|
+
== License
|
198
|
+
|
199
|
+
The gem is available as open source under the terms of the http://opensource.org/licenses/MIT[MIT License].
|
data/Rakefile
CHANGED
data/attr_memoized.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'attr_memoized/version'
|
5
6
|
|
@@ -9,8 +10,8 @@ Gem::Specification.new do |spec|
|
|
9
10
|
spec.authors = ['Konstantin Gredeskoul']
|
10
11
|
spec.email = ['kig@reinvent.one']
|
11
12
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
13
|
+
spec.summary = '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.description = '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
15
|
spec.homepage = 'https://github.com/kigster/attr_memoized'
|
15
16
|
spec.license = 'MIT'
|
16
17
|
|
@@ -21,12 +22,14 @@ Gem::Specification.new do |spec|
|
|
21
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
23
|
spec.require_paths = ['lib']
|
23
24
|
|
24
|
-
spec.add_development_dependency 'bundler'
|
25
|
-
spec.add_development_dependency '
|
26
|
-
spec.add_development_dependency '
|
25
|
+
spec.add_development_dependency 'bundler'
|
26
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
27
|
+
spec.add_development_dependency 'colored2'
|
28
|
+
spec.add_development_dependency 'irbtools'
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
spec.add_development_dependency 'relaxed-rubocop'
|
31
|
+
spec.add_development_dependency 'rspec'
|
27
32
|
spec.add_development_dependency 'rspec-its'
|
33
|
+
spec.add_development_dependency 'rubocop'
|
28
34
|
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
35
|
end
|
data/bin/console
CHANGED
data/lib/attr_memoized.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'attr_memoized/version'
|
2
|
-
require 'thread'
|
3
4
|
# This module, when included, decorates the receiver with several useful
|
4
5
|
# methods:
|
5
6
|
#
|
6
7
|
# * Both class and instance methods #mutex are added, that can be used
|
7
|
-
# to guard any shared resources. Each class gets their own class
|
8
|
-
# and each instance gets it's own separate
|
8
|
+
# to guard any shared resources. Each class gets their own class attr_memoized_mutex.
|
9
|
+
# and each instance gets it's own separate attr_memoized_mutex.
|
9
10
|
#
|
10
11
|
# * new class method #attr_memoized is added, the syntax is as follows:
|
11
12
|
#
|
@@ -20,147 +21,155 @@ require 'thread'
|
|
20
21
|
# Therefore, typically you would use #attr_memoized with one attribute at
|
21
22
|
# a time, unless you want to have several version of the same variable:
|
22
23
|
#
|
23
|
-
#
|
24
|
-
# =======
|
24
|
+
# @example
|
25
25
|
#
|
26
|
-
#
|
27
|
-
#
|
26
|
+
# class RandomNumbers
|
27
|
+
# include AttrMemoized
|
28
28
|
#
|
29
|
-
#
|
30
|
-
#
|
29
|
+
# attr_memoized :random, -> { rand(10) }, writer: false
|
30
|
+
# attr_memoized :seed, -> { Time.now.to_i % 57 }
|
31
31
|
#
|
32
|
-
#
|
32
|
+
# attr_memoized :number1, :number2, -> { self.class.incr! && rand(10) }
|
33
33
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
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
40
|
#
|
41
|
-
#
|
41
|
+
# @rn = RandomNumbers.new
|
42
42
|
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
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
51
|
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
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
56
|
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
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
61
|
#
|
62
62
|
#
|
63
63
|
module AttrMemoized
|
64
|
-
|
65
64
|
# We are being a bit paranoid here, so yes we are creating
|
66
|
-
# a central lock used to initialize other class-specific
|
65
|
+
# a central lock used to initialize other class-specific attr_memoized_mutex.s.
|
67
66
|
# This should only be a problem if you are constantly defining new
|
68
67
|
# classes that include +AttrMemoized++
|
69
68
|
LOCK = Mutex.new.freeze unless defined?(LOCK)
|
70
69
|
#
|
71
70
|
# The types of initializers we support.
|
72
|
-
SUPPORTED_INIT_TYPES = [Proc, Method, Symbol]
|
71
|
+
SUPPORTED_INIT_TYPES = [Proc, Method, Symbol].freeze
|
73
72
|
|
74
73
|
class << self
|
75
|
-
# that's obviously a joke. The name, I mean. Duh.
|
76
|
-
attr_accessor :gil
|
77
|
-
|
78
|
-
|
79
74
|
def included(base)
|
80
75
|
base.class_eval do
|
81
76
|
AttrMemoized::LOCK.synchronize do
|
82
|
-
@
|
77
|
+
@attr_memoized_mutex ||= Mutex.new
|
83
78
|
end
|
84
79
|
|
80
|
+
# noinspection ALL
|
85
81
|
class << self
|
86
|
-
attr_reader :
|
87
|
-
|
82
|
+
attr_reader :attr_memoized_mutex
|
83
|
+
|
88
84
|
# A class method which, for each attribute in the list,
|
89
85
|
# creates a thread-safe memoized reader and writer (unless writer: false)
|
90
86
|
#
|
91
87
|
# Memoized reader accepts <tt>reload: true</tt> as an optional argument,
|
92
88
|
# which, if provided, forces reinitialization of the variable.
|
93
89
|
#
|
94
|
-
#
|
95
|
-
# ==========
|
96
|
-
#
|
97
|
-
# class LazyConnection
|
98
|
-
# include AttrMemoized
|
99
|
-
# attr_memoized :database_connection, -> { ActiveRecord::Base.connection }
|
90
|
+
# @example:
|
100
91
|
#
|
101
|
-
#
|
92
|
+
# class LazyConnection
|
93
|
+
# include AttrMemoized
|
94
|
+
# attr_memoized :database_connection, -> { ActiveRecord::Base.connection }
|
95
|
+
# attr_memoized :redis_pool, -> { ConnectionPool.new { Redis.new } }
|
96
|
+
# end
|
102
97
|
#
|
103
|
-
#
|
104
|
-
#
|
98
|
+
# LazyConnection.new.database_connection
|
99
|
+
# #=> <ActiveRecord::Connection::PgSQL::Driver:0xff23234f....>
|
105
100
|
#
|
101
|
+
# @param [Symbol] name of the attribute
|
102
|
+
# @param [Symbol] another name of the attribute, etc...
|
103
|
+
# @param [Proc,Symbol,Method] callable — something to call for lazy initialization
|
104
|
+
# @param [Hash] options — you can define arguments here to be passed to a method or proc
|
106
105
|
def attr_memoized(*attributes, **opts)
|
107
|
-
attributes
|
108
|
-
|
106
|
+
attributes = Array[*attributes]
|
107
|
+
callable = attributes.pop
|
108
|
+
unless SUPPORTED_INIT_TYPES.include?(callable.class)
|
109
|
+
raise ArgumentError, "Invalid argument #{callable} to attr_memoized. Expecting one of: #{SUPPORTED_INIT_TYPES.map(&:to_s)}"
|
110
|
+
end
|
111
|
+
|
112
|
+
writer = opts.delete(:writer)
|
109
113
|
attributes.each do |attribute|
|
110
|
-
|
111
|
-
|
114
|
+
__define_attribute_writer(attribute, **opts) unless writer == false
|
115
|
+
__define_attribute_reader(attribute, callable, **opts)
|
112
116
|
end
|
113
117
|
end
|
114
118
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
+
# Memoized Reader only
|
120
|
+
def attr_memoized_reader(*attrs, **opts)
|
121
|
+
attr_memoized(*attrs, writer: false, **opts)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def __define_attribute_reader(attribute, callable, **opts)
|
127
|
+
at_attribute = __at_var(attribute)
|
128
|
+
define_method(attribute) do |*|
|
129
|
+
__read_memoize(attribute, at_attribute, callable, **opts)
|
119
130
|
end
|
120
131
|
end
|
121
132
|
|
122
|
-
def
|
123
|
-
at_attribute =
|
133
|
+
def __define_attribute_writer(attribute, **_opts)
|
134
|
+
at_attribute = __at_var(attribute)
|
124
135
|
define_method("#{attribute}=".to_sym) do |value|
|
125
|
-
|
126
|
-
already_locked? ?
|
127
|
-
block.call :
|
128
|
-
with_thread_local_lock { mutex.synchronize(&block) }
|
136
|
+
with_lock { instance_variable_set(at_attribute, value) }
|
129
137
|
value
|
130
138
|
end
|
131
139
|
end
|
132
140
|
|
133
|
-
private
|
134
|
-
|
135
141
|
# Convert an attribute name into an @variable syntax
|
136
|
-
def
|
137
|
-
attr
|
142
|
+
def __at_var(attr)
|
143
|
+
attr = attr.to_sym unless attr.is_a?(Symbol)
|
138
144
|
@attr_cache ||= {}
|
139
145
|
@attr_cache[attr] || (@attr_cache[attr] = "@#{attr}".to_sym)
|
140
146
|
end
|
141
147
|
end
|
142
148
|
|
143
|
-
# instance method: uses the class
|
144
|
-
#
|
145
|
-
#
|
146
|
-
def
|
147
|
-
return @
|
148
|
-
|
149
|
-
|
149
|
+
# instance method: uses the class +attr_memoized_mutex+ to create an instance
|
150
|
+
# attr_memoized_mutex and then uses the instance attr_memoized_mutex to wrap instance's state
|
151
|
+
# @return [Mutex] mutex
|
152
|
+
def attr_memoized_mutex
|
153
|
+
return @attr_memoized_mutex if @attr_memoized_mutex
|
154
|
+
|
155
|
+
self.class.attr_memoized_mutex.synchronize {
|
156
|
+
@attr_memoized_mutex ||= Mutex.new
|
150
157
|
}
|
151
158
|
end
|
152
159
|
end
|
153
160
|
end
|
154
161
|
end
|
155
162
|
|
156
|
-
# This
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
163
|
+
# This method offers "thread-local locking": meaning that the synchronize
|
164
|
+
# block is never called twice from the same thread, thus avoiding deadlocks.
|
165
|
+
#
|
166
|
+
# @param [Proc] block block to wrap in a synchronize unless we are already under one
|
160
167
|
def with_lock(&block)
|
161
|
-
|
162
|
-
block
|
163
|
-
|
168
|
+
if __locked?
|
169
|
+
block.call
|
170
|
+
else
|
171
|
+
__with_thread_local_lock { attr_memoized_mutex.synchronize(&block) }
|
172
|
+
end
|
164
173
|
end
|
165
174
|
|
166
175
|
private
|
@@ -168,57 +177,59 @@ module AttrMemoized
|
|
168
177
|
# This private method is executed in order to initialize a memoized
|
169
178
|
# attribute.
|
170
179
|
#
|
171
|
-
# @param [Symbol]
|
172
|
-
# @param [Symbol]
|
173
|
-
# @param [Proc, Method, Symbol]
|
174
|
-
# @param [Hash]
|
180
|
+
# @param [Symbol] attribute - name of the attribute
|
181
|
+
# @param [Symbol] at_attribute - symbol representing attribute instance variable
|
182
|
+
# @param [Proc, Method, Symbol] callable - what to call to get the uncached value
|
183
|
+
# @param [Hash] opts - additional options
|
175
184
|
# @option opts [Boolean] :reload - forces re-initialization of the memoized attribute
|
176
|
-
def
|
177
|
-
var =
|
178
|
-
return var if var && !
|
179
|
-
|
180
|
-
|
185
|
+
def __read_memoize(attribute, at_attribute, callable, **opts)
|
186
|
+
var = instance_variable_get(at_attribute)
|
187
|
+
return var if var && !__reload?(opts)
|
188
|
+
|
189
|
+
with_lock { __assign_value(attribute, at_attribute, callable, **opts) }
|
190
|
+
instance_variable_get(at_attribute)
|
181
191
|
end
|
182
192
|
|
183
193
|
# This private method resolves the initializer argument and returns it's result.
|
184
|
-
def
|
194
|
+
def __assign_value(attribute, at_attribute, callable, **opts)
|
185
195
|
# reload the value of +var+ because we are now inside a synchronize block
|
186
|
-
var =
|
187
|
-
return var if
|
196
|
+
var = instance_variable_get(at_attribute)
|
197
|
+
return var if var && !__reload?(opts)
|
188
198
|
|
189
199
|
# now call whatever `was defined on +attr_memoized+ to get the actual value
|
190
|
-
case
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
200
|
+
case callable
|
201
|
+
when Symbol
|
202
|
+
send(callable, **opts)
|
203
|
+
when Method
|
204
|
+
callable.call(**opts)
|
205
|
+
when Proc
|
206
|
+
instance_exec(&callable)
|
207
|
+
else
|
208
|
+
raise ArgumentError, "expected one of #{AttrMemoized::SUPPORTED_INIT_TYPES.map(&:to_s).join(', ')} for attribute #{attribute}, got #{callable.class}"
|
199
209
|
end.tap do |result|
|
200
|
-
|
210
|
+
instance_variable_set(at_attribute, result)
|
201
211
|
end
|
202
212
|
end
|
203
213
|
|
204
214
|
# Returns +true+ if +opts+ contains reload: +true+
|
205
|
-
def
|
206
|
-
|
215
|
+
def __reload?(opts)
|
216
|
+
opts.delete(:reload)
|
207
217
|
end
|
208
218
|
|
209
219
|
# just a key into Thread.local
|
210
|
-
def
|
220
|
+
def __object_lock_key
|
211
221
|
@key ||= "this.#{object_id}".to_sym
|
212
222
|
end
|
213
223
|
|
214
|
-
def
|
215
|
-
Thread.current[
|
224
|
+
def __locked?
|
225
|
+
Thread.current[__object_lock_key]
|
216
226
|
end
|
217
227
|
|
218
|
-
def
|
219
|
-
raise ArgumentError, 'Already locked!' if
|
220
|
-
|
228
|
+
def __with_thread_local_lock
|
229
|
+
raise ArgumentError, 'Already locked!' if __locked?
|
230
|
+
|
231
|
+
Thread.current[__object_lock_key] = true
|
221
232
|
yield if block_given?
|
222
|
-
Thread.current[
|
233
|
+
Thread.current[__object_lock_key] = nil
|
223
234
|
end
|
224
235
|
end
|