moneta 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +176 -0
  3. data/.travis.yml +57 -23
  4. data/CHANGES +12 -0
  5. data/Gemfile +89 -64
  6. data/README.md +40 -14
  7. data/feature_matrix.yaml +1 -0
  8. data/lib/action_dispatch/middleware/session/moneta_store.rb +1 -0
  9. data/lib/active_support/cache/moneta_store.rb +5 -5
  10. data/lib/moneta.rb +9 -1
  11. data/lib/moneta/adapters/activerecord.rb +35 -19
  12. data/lib/moneta/adapters/activesupportcache.rb +3 -7
  13. data/lib/moneta/adapters/cassandra.rb +24 -16
  14. data/lib/moneta/adapters/client.rb +13 -9
  15. data/lib/moneta/adapters/couch.rb +220 -80
  16. data/lib/moneta/adapters/datamapper.rb +1 -0
  17. data/lib/moneta/adapters/file.rb +9 -6
  18. data/lib/moneta/adapters/hbase.rb +1 -1
  19. data/lib/moneta/adapters/kyotocabinet.rb +8 -7
  20. data/lib/moneta/adapters/leveldb.rb +1 -1
  21. data/lib/moneta/adapters/lmdb.rb +3 -4
  22. data/lib/moneta/adapters/lruhash.rb +29 -62
  23. data/lib/moneta/adapters/memcached.rb +1 -0
  24. data/lib/moneta/adapters/memcached/dalli.rb +1 -1
  25. data/lib/moneta/adapters/memcached/native.rb +10 -8
  26. data/lib/moneta/adapters/mongo.rb +1 -0
  27. data/lib/moneta/adapters/mongo/base.rb +3 -3
  28. data/lib/moneta/adapters/mongo/moped.rb +12 -13
  29. data/lib/moneta/adapters/mongo/official.rb +7 -8
  30. data/lib/moneta/adapters/null.rb +1 -2
  31. data/lib/moneta/adapters/pstore.rb +3 -2
  32. data/lib/moneta/adapters/redis.rb +3 -3
  33. data/lib/moneta/adapters/restclient.rb +12 -3
  34. data/lib/moneta/adapters/riak.rb +2 -2
  35. data/lib/moneta/adapters/sequel.rb +112 -119
  36. data/lib/moneta/adapters/sqlite.rb +3 -3
  37. data/lib/moneta/adapters/tokyotyrant.rb +1 -1
  38. data/lib/moneta/builder.rb +0 -1
  39. data/lib/moneta/enumerable.rb +38 -0
  40. data/lib/moneta/expires.rb +12 -12
  41. data/lib/moneta/fallback.rb +84 -0
  42. data/lib/moneta/lock.rb +1 -1
  43. data/lib/moneta/logger.rb +2 -2
  44. data/lib/moneta/mixins.rb +12 -10
  45. data/lib/moneta/optionmerger.rb +0 -1
  46. data/lib/moneta/pool.rb +301 -31
  47. data/lib/moneta/proxy.rb +2 -2
  48. data/lib/moneta/server.rb +9 -12
  49. data/lib/moneta/shared.rb +1 -1
  50. data/lib/moneta/stack.rb +6 -6
  51. data/lib/moneta/synchronize.rb +3 -3
  52. data/lib/moneta/transformer.rb +19 -17
  53. data/lib/moneta/transformer/config.rb +6 -5
  54. data/lib/moneta/transformer/helper.rb +3 -3
  55. data/lib/moneta/transformer/helper/bson.rb +18 -15
  56. data/lib/moneta/utils.rb +3 -9
  57. data/lib/moneta/version.rb +1 -1
  58. data/lib/moneta/weak_each_key.rb +2 -4
  59. data/lib/rack/cache/moneta.rb +16 -13
  60. data/lib/rack/moneta_rest.rb +2 -2
  61. data/lib/rack/session/moneta.rb +3 -4
  62. data/moneta.gemspec +8 -0
  63. data/script/benchmarks +55 -32
  64. data/script/reconfigure-couchdb +13 -0
  65. data/script/start-hbase +1 -0
  66. data/script/start-services +2 -10
  67. data/spec/active_support/cache_moneta_store_spec.rb +3 -1
  68. data/spec/features/concurrent_create.rb +31 -10
  69. data/spec/features/concurrent_increment.rb +27 -19
  70. data/spec/features/increment.rb +41 -41
  71. data/spec/helper.rb +2 -42
  72. data/spec/moneta/adapters/activesupportcache/adapter_activesupportcache_spec.rb +4 -1
  73. data/spec/moneta/adapters/activesupportcache/adapter_activesupportcache_with_default_expires_spec.rb +4 -1
  74. data/spec/moneta/adapters/activesupportcache/standard_activesupportcache_spec.rb +14 -0
  75. data/spec/moneta/adapters/couch/adapter_couch_spec.rb +199 -2
  76. data/spec/moneta/adapters/couch/standard_couch_spec.rb +8 -2
  77. data/spec/moneta/adapters/couch/standard_couch_with_expires_spec.rb +7 -1
  78. data/spec/moneta/adapters/faraday_helper.rb +9 -0
  79. data/spec/moneta/adapters/lruhash/adapter_lruhash_spec.rb +2 -2
  80. data/spec/moneta/adapters/memcached/adapter_memcached_spec.rb +1 -1
  81. data/spec/moneta/adapters/memcached/dalli/adapter_memcached_dalli_spec.rb +1 -1
  82. data/spec/moneta/adapters/memcached/dalli/adapter_memcached_dalli_with_default_expires_spec.rb +1 -1
  83. data/spec/moneta/adapters/memcached/dalli/standard_memcached_dalli_spec.rb +1 -1
  84. data/spec/moneta/adapters/memcached/native/adapter_memcached_native_spec.rb +1 -1
  85. data/spec/moneta/adapters/memcached/native/adapter_memcached_native_with_default_expires_spec.rb +1 -1
  86. data/spec/moneta/adapters/memcached/native/standard_memcached_native_spec.rb +1 -1
  87. data/spec/moneta/adapters/memcached/standard_memcached_spec.rb +1 -1
  88. data/spec/moneta/adapters/{memcached/helper.rb → memcached_helper.rb} +0 -0
  89. data/spec/moneta/adapters/null/null_adapter_spec.rb +1 -1
  90. data/spec/moneta/adapters/restclient/adapter_restclient_spec.rb +7 -5
  91. data/spec/moneta/adapters/restclient/helper.rb +12 -0
  92. data/spec/moneta/adapters/restclient/standard_restclient_spec.rb +9 -6
  93. data/spec/moneta/adapters/riak/standard_riak_with_expires_spec.rb +4 -0
  94. data/spec/moneta/adapters/tokyotyrant/adapter_tokyotyrant_spec.rb +6 -2
  95. data/spec/moneta/adapters/tokyotyrant/helper.rb +12 -0
  96. data/spec/moneta/adapters/tokyotyrant/standard_tokyotyrant_spec.rb +5 -2
  97. data/spec/moneta/adapters/tokyotyrant/standard_tokyotyrant_with_expires_spec.rb +5 -1
  98. data/spec/moneta/proxies/enumerable/enumerable_spec.rb +26 -0
  99. data/spec/moneta/proxies/fallback/fallback_spec.rb +42 -0
  100. data/spec/moneta/proxies/pool/pool_spec.rb +319 -6
  101. data/spec/restserver.rb +40 -0
  102. metadata +122 -7
  103. data/script/install-kyotocabinet +0 -17
@@ -91,7 +91,7 @@ module Moneta
91
91
 
92
92
  # (see Default#create)
93
93
  def create(key, value, options = {})
94
- @create.execute!(key,value)
94
+ @create.execute!(key, value)
95
95
  true
96
96
  rescue SQLite3::ConstraintException
97
97
  # If you know a better way to detect whether an insert-ignore
@@ -102,7 +102,7 @@ module Moneta
102
102
 
103
103
  # (see Proxy#close)
104
104
  def close
105
- @stmts.each {|s| s.close }
105
+ @stmts.each { |s| s.close }
106
106
  @backend.close
107
107
  nil
108
108
  end
@@ -134,7 +134,7 @@ module Moneta
134
134
 
135
135
  # (see Proxy#merge!)
136
136
  def merge!(pairs, options = {})
137
- transaction = if block_given?; @backend.transaction end
137
+ transaction = @backend.transaction if block_given?
138
138
 
139
139
  if block_given?
140
140
  existing = Hash[slice(*pairs.map { |k, _| k }.to_a)]
@@ -48,7 +48,7 @@ module Moneta
48
48
  def load(key, options = {})
49
49
  value = @backend[key]
50
50
  # raise if there is an error and the error is not "no record"
51
- error if value.nil? && @backend.ecode != ENOREC
51
+ error if value == nil && @backend.ecode != ENOREC
52
52
  value && unpack(value)
53
53
  end
54
54
 
@@ -79,5 +79,4 @@ Please check your Moneta builder block:
79
79
  }) if args != expected
80
80
  end
81
81
  end
82
-
83
82
  end
@@ -0,0 +1,38 @@
1
+ module Moneta
2
+ # Adds the Ruby {Enumerable} API to the store. The underlying store must
3
+ # support `:each_key`.
4
+ #
5
+ # @example Adding to a builder
6
+ # Moneta.build do
7
+ # # It should be the top middleware
8
+ # use :Enumerable
9
+ # adapter :DBM
10
+ # end
11
+ #
12
+ # @api public
13
+ class Enumerable < Proxy
14
+ include ::Enumerable
15
+
16
+ def initialize(adapter, options = {})
17
+ raise "Adapter must support :each_key" unless adapter.supports? :each_key
18
+ super
19
+ end
20
+
21
+ # Enumerate over all pairs in the store
22
+ #
23
+ # @overload each
24
+ # @return [Enumerator]
25
+ #
26
+ # @overload each
27
+ # @yieldparam pair [Array<(Object, Object)>] Each pair is yielded
28
+ # @return [self]
29
+ #
30
+ def each
31
+ return enum_for(:each) unless block_given?
32
+ each_key { |key| yield key, load(key) }
33
+ self
34
+ end
35
+
36
+ alias each_pair each
37
+ end
38
+ end
@@ -21,14 +21,14 @@ module Moneta
21
21
  def key?(key, options = {})
22
22
  # Transformer might raise exception
23
23
  load_entry(key, options) != nil
24
- rescue Exception
24
+ rescue
25
25
  super(key, Utils.without(options, :expires))
26
26
  end
27
27
 
28
28
  # (see Proxy#load)
29
29
  def load(key, options = {})
30
30
  return super if options.include?(:raw)
31
- value, expires = load_entry(key, options)
31
+ value, = load_entry(key, options)
32
32
  value
33
33
  end
34
34
 
@@ -64,8 +64,8 @@ module Moneta
64
64
  entry = invalidate_entry(key, entry, new_expires) do |new_entry|
65
65
  updates[key] = new_entry
66
66
  end
67
- next if entry.nil?
68
- value, _ = entry
67
+ next if entry == nil
68
+ value, = entry
69
69
  value
70
70
  end
71
71
  end
@@ -90,12 +90,12 @@ module Moneta
90
90
  entry = invalidate_entry(key, entry, new_expires) do |new_entry|
91
91
  updates[key] = new_entry
92
92
  end
93
- if entry.nil?
93
+ if entry == nil
94
94
  value = if block_given?
95
95
  yield key
96
96
  end
97
97
  else
98
- value, _ = entry
98
+ value, = entry
99
99
  end
100
100
  value
101
101
  end
@@ -113,26 +113,26 @@ module Moneta
113
113
  entry = invalidate_entry(key, entry, new_expires) do |new_entry|
114
114
  updates[key] = new_entry
115
115
  end
116
- next if entry.nil?
117
- value, _ = entry
116
+ next if entry == nil
117
+ value, = entry
118
118
  [key, value]
119
119
  end.reject(&:nil?)
120
120
  end
121
121
  end
122
122
 
123
123
  # (see Defaults#merge!)
124
- def merge!(pairs, options={})
124
+ def merge!(pairs, options = {})
125
125
  expires = expires_at(options)
126
126
  options = Utils.without(options, :expires)
127
127
 
128
128
  block = if block_given?
129
129
  lambda do |key, old_entry, entry|
130
130
  old_entry = invalidate_entry(key, old_entry)
131
- if old_entry.nil?
131
+ if old_entry == nil
132
132
  entry # behave as if no replace is happening
133
133
  else
134
- old_value, _ = old_entry
135
- new_value, _ = entry
134
+ old_value, = old_entry
135
+ new_value, = entry
136
136
  new_entry(yield(key, old_value, new_value), expires)
137
137
  end
138
138
  end
@@ -0,0 +1,84 @@
1
+ module Moneta
2
+ # Provides a fallback to a second store when an exception is raised
3
+ #
4
+ # @example Basic usage - catches any {IOError} and falls back to {Moneta::Adapters:Null}
5
+ # Moneta.build do
6
+ # use :Fallback
7
+ # adapter :Client
8
+ # end
9
+ #
10
+ # @example Specifying an exception to rescue
11
+ # Moneta.build do
12
+ # use :Fallback, rescue: Redis::CannotConnectError
13
+ # adapter :Redis
14
+ # end
15
+ #
16
+ # @example Specifying a different fallback
17
+ # Moneta.build do
18
+ # use :Fallback do
19
+ # # This is a new builder context
20
+ # adapter :Memory
21
+ # end
22
+ # adapter :File, dir: 'cache'
23
+ # end
24
+ #
25
+ # @api public
26
+ class Fallback < Wrapper
27
+ # @param [Moneta store] adapter The underlying store
28
+ # @param [Hash] options
29
+ # @option options [Moneta store] :fallback (:Null store) The store to fall
30
+ # back on
31
+ # @option options [Class|Array<Class>] :rescue ([IOError]) The list
32
+ # of exceptions that should be rescued
33
+ # @yieldreturn [Moneta store] Moneta store built using the builder API
34
+ def initialize(adapter, options = {}, &block)
35
+ super
36
+
37
+ @fallback =
38
+ if block_given?
39
+ ::Moneta.build(&block)
40
+ elsif options.key?(:fallback)
41
+ options.delete(:fallback)
42
+ else
43
+ ::Moneta::Adapters::Null.new
44
+ end
45
+
46
+ @rescue =
47
+ case options[:rescue]
48
+ when nil
49
+ [::IOError]
50
+ when Array
51
+ options[:rescue]
52
+ else
53
+ [options[:rescue]]
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def wrap(name, *args, &block)
60
+ yield
61
+ rescue => e
62
+ raise unless @rescue.any? { |rescuable| rescuable === e }
63
+ fallback(name, *args, &block)
64
+ end
65
+
66
+ def fallback(name, *args, &block)
67
+ result =
68
+ case name
69
+ when :values_at, :fetch_values, :slice
70
+ keys, options = args
71
+ @fallback.public_send(name, *keys, **options, &block)
72
+ else
73
+ @fallback.public_send(name, *args, &block)
74
+ end
75
+
76
+ # Don't expose the fallback class to the caller
77
+ if result == @fallback
78
+ self
79
+ else
80
+ result
81
+ end
82
+ end
83
+ end
84
+ end
@@ -16,7 +16,7 @@ module Moneta
16
16
 
17
17
  def wrap(name, *args, &block)
18
18
  if locked?
19
- block.call
19
+ yield
20
20
  else
21
21
  lock!(&block)
22
22
  end
@@ -29,7 +29,7 @@ module Moneta
29
29
  def format(entry)
30
30
  args = entry[:args]
31
31
  args.pop if Hash === args.last && args.last.empty?
32
- args = args.map {|a| dump(a) }.join(', ')
32
+ args = args.map { |a| dump(a) }.join(', ')
33
33
  if entry[:error]
34
34
  "#{@prefix}#{entry[:method]}(#{args}) raised error: #{entry[:error].message}\n"
35
35
  else
@@ -66,7 +66,7 @@ module Moneta
66
66
  ret = yield
67
67
  @logger.log(method: method, args: args, return: (method == :clear ? 'self' : ret))
68
68
  ret
69
- rescue Exception => error
69
+ rescue => error
70
70
  @logger.log(method: method, args: args, error: error)
71
71
  raise
72
72
  end
@@ -22,10 +22,10 @@ module Moneta
22
22
  # @return [OptionMerger]
23
23
  # @api public
24
24
  def raw
25
- @raw_store ||=
25
+ @raw ||=
26
26
  begin
27
27
  store = with(raw: true, only: [:load, :store, :create, :delete])
28
- store.instance_variable_set(:@raw_store, store)
28
+ store.instance_variable_set(:@raw, store)
29
29
  store
30
30
  end
31
31
  end
@@ -151,8 +151,7 @@ module Moneta
151
151
  # Explicitly close the store
152
152
  # @return nil
153
153
  # @api public
154
- def close
155
- end
154
+ def close; end
156
155
 
157
156
  # Fetch a value with a key
158
157
  #
@@ -338,7 +337,7 @@ module Moneta
338
337
  # @yieldreturn [Object] The value to use for overwriting
339
338
  # @return [self]
340
339
  # @api public
341
- def merge!(pairs, options={})
340
+ def merge!(pairs, options = {})
342
341
  pairs.each do |key, value|
343
342
  if block_given?
344
343
  existing = load(key, options)
@@ -391,7 +390,7 @@ module Moneta
391
390
  end
392
391
  end
393
392
 
394
- def merge!(pairs, options={})
393
+ def merge!(pairs, options = {})
395
394
  pairs.each do |key, value|
396
395
  if block_given? && key?(key, options)
397
396
  existing = load(key, options)
@@ -407,7 +406,8 @@ module Moneta
407
406
  module IncrementSupport
408
407
  # (see Defaults#increment)
409
408
  def increment(key, amount = 1, options = {})
410
- value = Utils.to_int(load(key, options)) + amount
409
+ existing = load(key, options)
410
+ value = (existing == nil ? 0 : Integer(existing)) + amount
411
411
  store(key, value.to_s, options)
412
412
  value
413
413
  end
@@ -513,7 +513,7 @@ module Moneta
513
513
  end
514
514
 
515
515
  # (see Defaults#merge!)
516
- def merge!(pairs, options={}, &block)
516
+ def merge!(pairs, options = {}, &block)
517
517
  return super unless method = [:merge!, :update].find do |method|
518
518
  @backend.respond_to? method
519
519
  end
@@ -612,8 +612,10 @@ module Moneta
612
612
  end
613
613
  end
614
614
 
615
- def self.included(base)
616
- base.supports(:expires) if base.respond_to?(:supports)
615
+ class << self
616
+ def included(base)
617
+ base.supports(:expires) if base.respond_to?(:supports)
618
+ end
617
619
  end
618
620
  end
619
621
  end
@@ -40,4 +40,3 @@ module Moneta
40
40
  end
41
41
  end
42
42
  end
43
-
@@ -1,69 +1,339 @@
1
- require 'thread'
1
+ require 'set'
2
2
 
3
3
  module Moneta
4
- # Creates a pool of stores.
5
- # Each thread gets its own store.
4
+ # Creates a thread-safe pool. Stores are in the pool are transparently
5
+ # checked in and out in order to perform operations.
6
+ #
7
+ # A `max` setting can be specified in order to limit the pool size. If `max`
8
+ # stores are all checked out at once, the next check-out will block until one
9
+ # of the other stores are checked in.
10
+ #
11
+ # A `ttl` setting can be specified, giving the number of seconds to
12
+ # wait without any activity before shrinking the pool size back down to the
13
+ # min size.
14
+ #
15
+ # A `timeout` setting can be specified, giving the number of seconds to wait
16
+ # when checking out a store, before an error is raised. When the pool has a
17
+ # `:max` size, a timeout is highly advisable.
6
18
  #
7
19
  # @example Add `Moneta::Pool` to proxy stack
8
20
  # Moneta.build do
9
21
  # use(:Pool) do
10
- # # Every thread gets its own instance
11
22
  # adapter :MemcachedNative
12
23
  # end
13
24
  # end
14
25
  #
26
+ # @example Add `Moneta::Pool` that contains at least 2 stores, and closes any extras after 60 seconds of inactivity
27
+ # Moneta.build do
28
+ # use(:Pool, min: 2, ttl: 60) do
29
+ # adapter :Sqlite, file: 'test.db'
30
+ # end
31
+ # end
32
+ #
33
+ # @example Add `Moneta::Pool` with a max of 10 stores, and a timeout of 5 seconds for checkout
34
+ # Moneta.build do
35
+ # use(:Pool, max: 10, timeout: 5) do
36
+ # adapter :Sqlite, file: 'test.db'
37
+ # end
38
+ # end
39
+ #
15
40
  # @api public
16
41
  class Pool < Wrapper
42
+ # @api private
43
+ class ShutdownError < ::RuntimeError; end
44
+ class TimeoutError < ::RuntimeError; end
45
+
46
+ # @api private
47
+ class Reply
48
+ attr_reader :resource
49
+
50
+ def initialize(mutex)
51
+ @mutex = mutex
52
+ @resource = ::ConditionVariable.new
53
+ @value = nil
54
+ end
55
+
56
+ def resolve(value)
57
+ @mutex.synchronize do
58
+ raise "Already resolved" if @value
59
+ @value = value
60
+ @resource.signal
61
+ end
62
+ nil
63
+ end
64
+
65
+ def wait
66
+ @resource.wait(@mutex)
67
+ @value
68
+ end
69
+ end
70
+
71
+ # @api private
72
+ class PoolManager
73
+ def initialize(builder, min: 0, max: nil, ttl: nil, timeout: nil)
74
+ @builder = builder
75
+ @min = min
76
+ @max = max
77
+ @ttl = ttl
78
+ @timeout = timeout
79
+
80
+ @inbox = []
81
+ @mutex = ::Mutex.new
82
+ @resource = ::ConditionVariable.new
83
+
84
+ @stores = Set.new
85
+ @available = []
86
+ @waiting = []
87
+ @waiting_since = [] if @timeout
88
+ @last_checkout = nil
89
+ @stopping = false
90
+
91
+ # Launch the manager thread
92
+ @thread = run
93
+ end
94
+
95
+ def stats
96
+ push(:stats, reply: true)
97
+ end
98
+
99
+ def stop
100
+ push(:stop)
101
+ nil
102
+ ensure
103
+ @thread.value
104
+ end
105
+
106
+ def kill!
107
+ @thread.kill
108
+ nil
109
+ end
110
+
111
+ def check_out
112
+ reply = push(:check_out, reply: true)
113
+ raise reply if Exception === reply
114
+ reply
115
+ end
116
+
117
+ def check_in(store)
118
+ push(:check_in, store)
119
+ end
120
+
121
+ private
122
+
123
+ def run
124
+ Thread.new do
125
+ begin
126
+ populate_stores
127
+
128
+ until @stopping && @stores.empty?
129
+ # Block until a message arrives, or until we time out for some reason
130
+ if request = pop
131
+ handle_request(request)
132
+ end
133
+
134
+ # Handle any stale checkout requests
135
+ handle_timed_out_requests
136
+ # Drop any stores that are no longer needed
137
+ remove_unneeded_stores
138
+ end
139
+ rescue => e
140
+ reject_waiting(e.message)
141
+ raise
142
+ end
143
+ end
144
+ end
145
+
146
+ def populate_stores
147
+ return if @stopping
148
+ @available.push(add_store) while @stores.length < @min
149
+ end
150
+
151
+ # If the last checkout was more than timeout ago, drop any available stores
152
+ def remove_unneeded_stores
153
+ return unless @stopping || (@ttl && @last_checkout && Time.now - @last_checkout >= @ttl)
154
+ while (@stopping || @stores.length > @min) and store = @available.pop
155
+ store.close rescue nil
156
+ @stores.delete(store)
157
+ end
158
+ end
159
+
160
+ # If there are checkout requests that have been waiting too long,
161
+ # feed them timeout errors.
162
+ def handle_timed_out_requests
163
+ while @timeout && !@waiting.empty? && (Time.now - @waiting_since.first) >= @timeout
164
+ waiting_since = @waiting_since.shift
165
+ @waiting.shift.resolve(TimeoutError.new("Waited %<secs>f seconds" % { secs: Time.now - waiting_since }))
166
+ end
167
+ end
168
+
169
+ # This is called from outside the loop thread
170
+ def push(message, what = nil, reply: nil)
171
+ @mutex.synchronize do
172
+ raise ShutdownError, "Pool has been shutdown" if reply && !@thread.alive?
173
+ reply &&= Reply.new(@mutex)
174
+ @inbox.push([message, what, reply])
175
+ @resource.signal
176
+ reply.wait if reply
177
+ end
178
+ end
179
+
180
+ # This method calculates the number of seconds to wait for a signal on
181
+ # the condition variable, or `nil` if there is no need to time out.
182
+ #
183
+ # Calculated based on the `:ttl` and `:timeout` options used during
184
+ # construction.
185
+ #
186
+ # @return [Integer, nil]
187
+ def timeout
188
+ # Time to wait before there will be stores that should be closed
189
+ ttl = if @ttl && @last_checkout && !@available.empty?
190
+ [@ttl - (Time.now - @last_checkout), 0].max
191
+ end
192
+
193
+ # Time to wait
194
+ timeout = if @timeout && !@waiting_since.empty?
195
+ longest_waiting = @waiting_since.first
196
+ [@timeout - (Time.now - longest_waiting), 0].max
197
+ end
198
+
199
+ [ttl, timeout].compact.min
200
+ end
201
+
202
+ def pop
203
+ @mutex.synchronize do
204
+ @resource.wait(@mutex, timeout) if @inbox.empty?
205
+ @inbox.shift
206
+ end
207
+ end
208
+
209
+ def add_store
210
+ store = @builder.build.last
211
+ @stores.add(store)
212
+ store
213
+ end
214
+
215
+ def handle_check_out(reply)
216
+ @last_checkout = Time.now
217
+ if @stopping
218
+ reply.resolve(ShutdownError.new("Shutting down"))
219
+ elsif !@available.empty?
220
+ reply.resolve(@available.pop)
221
+ elsif !@max || @stores.length < @max
222
+ begin
223
+ reply.resolve(add_store)
224
+ rescue => e
225
+ reply.resolve(e)
226
+ end
227
+ else
228
+ @waiting.push(reply)
229
+ @waiting_since.push(Time.now) if @timeout
230
+ end
231
+ end
232
+
233
+ def handle_stop
234
+ @stopping = true
235
+ # Reject anyone left waiting
236
+ reject_waiting "Shutting down"
237
+ end
238
+
239
+ def reject_waiting(reason)
240
+ while reply = @waiting.shift
241
+ reply.resolve(ShutdownError.new(reason))
242
+ end
243
+ @waiting_since = [] if @timeout
244
+ end
245
+
246
+ def handle_check_in(store)
247
+ if !@waiting.empty?
248
+ @waiting.shift.resolve(store)
249
+ @waiting_since.shift if @timeout
250
+ else
251
+ @available.push(store)
252
+ end
253
+ end
254
+
255
+ def handle_stats(reply)
256
+ reply.resolve(stores: @stores.length,
257
+ available: @available.length,
258
+ waiting: @waiting.length,
259
+ longest_wait: @timeout && !@waiting_since.empty? ? @waiting_since.first.dup : nil,
260
+ stopping: @stopping,
261
+ last_checkout: @last_checkout && @last_checkout.dup)
262
+ end
263
+
264
+ def handle_request(request)
265
+ cmd, what, reply = request
266
+ case cmd
267
+ when :check_out
268
+ handle_check_out(reply)
269
+ when :check_in
270
+ # A checkin request
271
+ handle_check_in(what)
272
+ when :stats
273
+ handle_stats(reply)
274
+ when :stop
275
+ # Graceful exit
276
+ handle_stop
277
+ end
278
+ end
279
+ end
280
+
17
281
  # @param [Hash] options
18
- # @option options [String] :mutex (::Mutex.new) Mutex object
282
+ # @option options [Integer] :min (0) The minimum pool size
283
+ # @option options [Integer] :max The maximum pool size. If not specified,
284
+ # there is no maximum.
285
+ # @option options [Numeric] :ttl The number of seconds to keep
286
+ # stores above the minumum number around for without activity. If
287
+ # not specified, stores will never be removed.
288
+ # @option options [Numeric] :timeout The number of seconds to wait for a
289
+ # store to become available. If not specified, will wait forever.
290
+ # @yield A builder context for speciying how to construct stores
19
291
  def initialize(options = {}, &block)
20
292
  super(nil)
21
- @mutex = options[:mutex] || ::Mutex.new
22
293
  @id = "Moneta::Pool(#{object_id})"
23
- @builder = Builder.new(&block)
24
- @pool, @all = [], []
294
+ @manager = PoolManager.new(Builder.new(&block), **options)
25
295
  end
26
296
 
27
- def close
28
- @mutex.synchronize do
29
- raise '#close can only be called when no thread is using the pool' if @all.size != @pool.size
30
- @all.each(&:close)
31
- @all = @pool = nil
32
- end
297
+ # Closing has no effect on the pool, as stores are closed in the background
298
+ # by the manager after the ttl
299
+ def close; end
300
+
301
+ # Tells the manager to close all stores. It will not be possible to use
302
+ # the store after this.
303
+ def stop
304
+ @manager.stop
305
+ nil
306
+ end
307
+
308
+ def stats
309
+ @manager.stats
33
310
  end
34
311
 
35
312
  protected
36
313
 
37
314
  def adapter
38
- Thread.current[@id]
315
+ Thread.current.thread_variable_get(@id)
316
+ end
317
+
318
+ def adapter=(store)
319
+ Thread.current.thread_variable_set(@id, store)
39
320
  end
40
321
 
41
322
  def wrap(*args, &block)
42
- if checked_out?
323
+ if adapter
43
324
  yield
44
325
  else
45
326
  check_out!(&block)
46
327
  end
47
328
  end
48
329
 
49
- def checked_out?
50
- Thread.current.key?(@id) && Thread.current[@id] != nil
51
- end
52
-
53
330
  def check_out!
54
- store = Thread.current[@id] = pop
331
+ store = @manager.check_out
332
+ self.adapter = store
55
333
  yield
56
334
  ensure
57
- Thread.current[@id] = nil
58
- @mutex.synchronize { @pool << store }
59
- end
60
-
61
- def pop
62
- unless store = @mutex.synchronize { @pool.pop }
63
- store = @builder.build.last
64
- @mutex.synchronize { @all << store }
65
- end
66
- store
335
+ self.adapter = nil
336
+ @manager.check_in store if store
67
337
  end
68
338
  end
69
339
  end