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.
@@ -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
@@ -2,9 +2,13 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - jruby-9.1.9.0
6
- - 2.3.4
7
- - 2.4.1
8
- before_install: gem install bundler -v 1.15.1
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
@@ -0,0 +1,7 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Memoized",
4
+ "memoization",
5
+ "mutex"
6
+ ]
7
+ }
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in attr_memoized.gemspec
@@ -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
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = %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.}
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', '~> 1'
25
- spec.add_development_dependency 'rake', '~> 12'
26
- spec.add_development_dependency 'rspec', '~> 3'
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "attr_memoized"
@@ -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 mutex,
8
- # and each instance gets it's own separate mutex.
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
- # Example
24
- # =======
24
+ # @example
25
25
  #
26
- # class RandomNumbers
27
- # include AttrMemoized
26
+ # class RandomNumbers
27
+ # include AttrMemoized
28
28
  #
29
- # attr_memoized :random, -> { rand(10) }, writer: false
30
- # attr_memoized :seed, -> { Time.now.to_i % 57 }
29
+ # attr_memoized :random, -> { rand(10) }, writer: false
30
+ # attr_memoized :seed, -> { Time.now.to_i % 57 }
31
31
  #
32
- # attr_memoized :number1, :number2, -> { self.class.incr! && rand(10) }
32
+ # attr_memoized :number1, :number2, -> { self.class.incr! && rand(10) }
33
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
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
- # @rn = RandomNumbers.new
41
+ # @rn = RandomNumbers.new
42
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
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
- # # 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
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
- # # 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>
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 mutexes.
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
- @mutex ||= Mutex.new
77
+ @attr_memoized_mutex ||= Mutex.new
83
78
  end
84
79
 
80
+ # noinspection ALL
85
81
  class << self
86
- attr_reader :mutex
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
- # Example:
95
- # ==========
96
- #
97
- # class LazyConnection
98
- # include AttrMemoized
99
- # attr_memoized :database_connection, -> { ActiveRecord::Base.connection }
90
+ # @example:
100
91
  #
101
- # end
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
- # LazyConnection.new.database_connection
104
- # #=> <ActiveRecord::Connection::PgSQL::Driver:0xff23234f....>
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 = Array[*attributes]
108
- block_or_proc = attributes.pop if SUPPORTED_INIT_TYPES.include?(attributes.last.class)
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
- define_attribute_writer(attribute) unless opts && opts.has_key?(:writer) && opts[:writer].eql?(false)
111
- define_attribute_reader(attribute, block_or_proc)
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
- 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
+ # 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 define_attribute_writer(attribute)
123
- at_attribute = 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
- block = -> { self.instance_variable_set(at_attribute, value) }
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 at(attr)
137
- attr = attr.to_sym unless attr.is_a?(Symbol)
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 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
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 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+.
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
- already_locked? ?
162
- block[] :
163
- with_thread_local_lock { mutex.synchronize(&block) }
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] 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
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 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)
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 assign_value(attribute, at_attribute, block_or_proc, **opts)
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 = self.instance_variable_get(at_attribute)
187
- return var if (var && !reload?(opts))
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 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}"
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
- self.instance_variable_set(at_attribute, result)
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 reload?(opts)
206
- (opts && opts.has_key?(:reload)) ? opts[:reload] : nil
215
+ def __reload?(opts)
216
+ opts.delete(:reload)
207
217
  end
208
218
 
209
219
  # just a key into Thread.local
210
- def object_locked_key
220
+ def __object_lock_key
211
221
  @key ||= "this.#{object_id}".to_sym
212
222
  end
213
223
 
214
- def already_locked?
215
- Thread.current[object_locked_key]
224
+ def __locked?
225
+ Thread.current[__object_lock_key]
216
226
  end
217
227
 
218
- def with_thread_local_lock
219
- raise ArgumentError, 'Already locked!' if already_locked?
220
- Thread.current[object_locked_key] = true
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[object_locked_key] = nil
233
+ Thread.current[__object_lock_key] = nil
223
234
  end
224
235
  end