moneta 1.2.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +418 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +27 -9
  5. data/CHANGES +28 -0
  6. data/CONTRIBUTORS +4 -2
  7. data/Gemfile +12 -8
  8. data/README.md +20 -18
  9. data/feature_matrix.yaml +2 -11
  10. data/lib/moneta.rb +9 -9
  11. data/lib/moneta/adapters/client.rb +56 -19
  12. data/lib/moneta/adapters/couch.rb +5 -0
  13. data/lib/moneta/adapters/mongo.rb +264 -7
  14. data/lib/moneta/adapters/redis.rb +5 -1
  15. data/lib/moneta/adapters/sequel.rb +45 -464
  16. data/lib/moneta/adapters/sequel/mysql.rb +66 -0
  17. data/lib/moneta/adapters/sequel/postgres.rb +80 -0
  18. data/lib/moneta/adapters/sequel/postgres_hstore.rb +240 -0
  19. data/lib/moneta/adapters/sequel/sqlite.rb +57 -0
  20. data/lib/moneta/adapters/sqlite.rb +7 -7
  21. data/lib/moneta/builder.rb +2 -2
  22. data/lib/moneta/create_support.rb +21 -0
  23. data/lib/moneta/dbm_adapter.rb +31 -0
  24. data/lib/moneta/{mixins.rb → defaults.rb} +1 -302
  25. data/lib/moneta/each_key_support.rb +27 -0
  26. data/lib/moneta/expires_support.rb +60 -0
  27. data/lib/moneta/hash_adapter.rb +68 -0
  28. data/lib/moneta/increment_support.rb +16 -0
  29. data/lib/moneta/lock.rb +6 -1
  30. data/lib/moneta/nil_values.rb +35 -0
  31. data/lib/moneta/option_support.rb +51 -0
  32. data/lib/moneta/pool.rb +38 -6
  33. data/lib/moneta/proxy.rb +1 -1
  34. data/lib/moneta/server.rb +215 -61
  35. data/lib/moneta/shared.rb +13 -7
  36. data/lib/moneta/transformer.rb +50 -8
  37. data/lib/moneta/transformer/config.rb +59 -40
  38. data/lib/moneta/transformer/helper.rb +2 -2
  39. data/lib/moneta/transformer/helper/bson.rb +5 -15
  40. data/lib/moneta/version.rb +1 -1
  41. data/lib/rack/cache/moneta.rb +14 -15
  42. data/moneta.gemspec +14 -8
  43. data/script/benchmarks +7 -3
  44. data/script/contributors +11 -6
  45. data/script/start-couchdb +27 -0
  46. data/script/start-hbase +2 -2
  47. data/script/start-services +3 -3
  48. data/spec/active_support/cache_moneta_store_spec.rb +37 -39
  49. data/spec/features/concurrent_increment.rb +2 -3
  50. data/spec/features/create_expires.rb +15 -15
  51. data/spec/features/default_expires.rb +11 -12
  52. data/spec/features/expires.rb +215 -210
  53. data/spec/features/store.rb +3 -3
  54. data/spec/helper.rb +33 -41
  55. data/spec/moneta/adapters/activerecord/adapter_activerecord_existing_connection_spec.rb +3 -1
  56. data/spec/moneta/adapters/activerecord/adapter_activerecord_spec.rb +15 -7
  57. data/spec/moneta/adapters/activerecord/standard_activerecord_spec.rb +6 -3
  58. data/spec/moneta/adapters/activerecord/standard_activerecord_with_expires_spec.rb +6 -3
  59. data/spec/moneta/adapters/activesupportcache/adapter_activesupportcache_spec.rb +3 -3
  60. data/spec/moneta/adapters/activesupportcache/adapter_activesupportcache_with_default_expires_spec.rb +2 -2
  61. data/spec/moneta/adapters/cassandra/standard_cassandra_spec.rb +1 -1
  62. data/spec/moneta/adapters/client/adapter_client_spec.rb +6 -6
  63. data/spec/moneta/adapters/client/client_helper.rb +24 -0
  64. data/spec/moneta/adapters/client/standard_client_tcp_spec.rb +8 -8
  65. data/spec/moneta/adapters/client/standard_client_unix_spec.rb +23 -7
  66. data/spec/moneta/adapters/couch/adapter_couch_spec.rb +1 -1
  67. data/spec/moneta/adapters/couch/standard_couch_spec.rb +2 -2
  68. data/spec/moneta/adapters/couch/standard_couch_with_expires_spec.rb +2 -2
  69. data/spec/moneta/adapters/datamapper/adapter_datamapper_spec.rb +25 -8
  70. data/spec/moneta/adapters/datamapper/standard_datamapper_spec.rb +2 -2
  71. data/spec/moneta/adapters/datamapper/standard_datamapper_with_expires_spec.rb +2 -2
  72. data/spec/moneta/adapters/datamapper/standard_datamapper_with_repository_spec.rb +2 -2
  73. data/spec/moneta/adapters/daybreak/standard_daybreak_spec.rb +1 -1
  74. data/spec/moneta/adapters/daybreak/standard_daybreak_with_expires_spec.rb +1 -1
  75. data/spec/moneta/adapters/dbm/standard_dbm_spec.rb +1 -1
  76. data/spec/moneta/adapters/dbm/standard_dbm_with_expires_spec.rb +1 -1
  77. data/spec/moneta/adapters/file/standard_file_spec.rb +2 -2
  78. data/spec/moneta/adapters/file/standard_file_with_expires_spec.rb +1 -1
  79. data/spec/moneta/adapters/gdbm/standard_gdbm_spec.rb +1 -1
  80. data/spec/moneta/adapters/gdbm/standard_gdbm_with_expires_spec.rb +1 -1
  81. data/spec/moneta/adapters/kyotocabinet/adapter_kyotocabinet_spec.rb +1 -1
  82. data/spec/moneta/adapters/kyotocabinet/standard_kyotocabinet_spec.rb +2 -2
  83. data/spec/moneta/adapters/kyotocabinet/standard_kyotocabinet_with_expires_spec.rb +2 -2
  84. data/spec/moneta/adapters/leveldb/standard_leveldb_spec.rb +1 -1
  85. data/spec/moneta/adapters/leveldb/standard_leveldb_with_expires_spec.rb +1 -1
  86. data/spec/moneta/adapters/lmdb/standard_lmdb_spec.rb +1 -1
  87. data/spec/moneta/adapters/lmdb/standard_lmdb_with_expires_spec.rb +1 -1
  88. data/spec/moneta/adapters/lruhash/standard_lruhash_spec.rb +1 -1
  89. data/spec/moneta/adapters/lruhash/standard_lruhash_with_expires_spec.rb +1 -1
  90. data/spec/moneta/adapters/memcached/dalli/adapter_memcached_dalli_spec.rb +13 -3
  91. data/spec/moneta/adapters/memcached/native/adapter_memcached_native_spec.rb +13 -3
  92. data/spec/moneta/adapters/memory/standard_memory_spec.rb +1 -1
  93. data/spec/moneta/adapters/memory/standard_memory_with_compress_spec.rb +1 -1
  94. data/spec/moneta/adapters/memory/standard_memory_with_expires_spec.rb +1 -1
  95. data/spec/moneta/adapters/memory/standard_memory_with_json_key_serializer_spec.rb +1 -1
  96. data/spec/moneta/adapters/memory/standard_memory_with_json_serializer_spec.rb +1 -1
  97. data/spec/moneta/adapters/memory/standard_memory_with_json_value_serializer_spec.rb +2 -2
  98. data/spec/moneta/adapters/memory/standard_memory_with_prefix_spec.rb +39 -2
  99. data/spec/moneta/adapters/memory/standard_memory_with_snappy_compress_spec.rb +2 -2
  100. data/spec/moneta/adapters/mongo/adapter_mongo_spec.rb +32 -2
  101. data/spec/moneta/adapters/mongo/adapter_mongo_with_default_expires_spec.rb +6 -4
  102. data/spec/moneta/adapters/mongo/standard_mongo_spec.rb +2 -2
  103. data/spec/moneta/adapters/pstore/standard_pstore_spec.rb +1 -1
  104. data/spec/moneta/adapters/pstore/standard_pstore_with_expires_spec.rb +1 -1
  105. data/spec/moneta/adapters/redis/adapter_redis_spec.rb +13 -3
  106. data/spec/moneta/adapters/redis/standard_redis_spec.rb +9 -2
  107. data/spec/moneta/adapters/sdbm/standard_sdbm_spec.rb +1 -1
  108. data/spec/moneta/adapters/sdbm/standard_sdbm_with_expires_spec.rb +1 -1
  109. data/spec/moneta/adapters/sequel/adapter_sequel_spec.rb +11 -38
  110. data/spec/moneta/adapters/sequel/helper.rb +42 -0
  111. data/spec/moneta/adapters/sequel/standard_sequel_spec.rb +6 -12
  112. data/spec/moneta/adapters/sequel/standard_sequel_with_expires_spec.rb +9 -10
  113. data/spec/moneta/adapters/sqlite/adapter_sqlite_spec.rb +1 -1
  114. data/spec/moneta/adapters/sqlite/standard_sqlite_spec.rb +2 -2
  115. data/spec/moneta/adapters/sqlite/standard_sqlite_with_expires_spec.rb +2 -2
  116. data/spec/moneta/adapters/tdb/standard_tdb_spec.rb +1 -1
  117. data/spec/moneta/adapters/tdb/standard_tdb_with_expires_spec.rb +1 -1
  118. data/spec/moneta/adapters/tokyocabinet/standard_tokyocabinet_spec.rb +1 -1
  119. data/spec/moneta/adapters/tokyocabinet/standard_tokyocabinet_with_expires_spec.rb +1 -1
  120. data/spec/moneta/adapters/yaml/standard_yaml_spec.rb +1 -1
  121. data/spec/moneta/adapters/yaml/standard_yaml_with_expires_spec.rb +1 -1
  122. data/spec/moneta/builder_spec.rb +22 -0
  123. data/spec/moneta/proxies/expires/expires_file_spec.rb +1 -1
  124. data/spec/moneta/proxies/pool/pool_spec.rb +31 -3
  125. data/spec/moneta/proxies/shared/shared_tcp_spec.rb +14 -4
  126. data/spec/moneta/proxies/shared/shared_unix_spec.rb +14 -4
  127. data/spec/moneta/proxies/transformer/transformer_bencode_spec.rb +1 -1
  128. data/spec/moneta/proxies/transformer/transformer_bert_spec.rb +3 -3
  129. data/spec/moneta/proxies/transformer/transformer_bson_spec.rb +2 -2
  130. data/spec/moneta/proxies/transformer/transformer_json_spec.rb +1 -1
  131. data/spec/moneta/proxies/transformer/transformer_key_marshal_spec.rb +1 -1
  132. data/spec/moneta/proxies/transformer/transformer_key_yaml_spec.rb +1 -1
  133. data/spec/moneta/proxies/transformer/transformer_marshal_base64_spec.rb +1 -1
  134. data/spec/moneta/proxies/transformer/transformer_marshal_escape_spec.rb +8 -4
  135. data/spec/moneta/proxies/transformer/transformer_marshal_hex_spec.rb +1 -1
  136. data/spec/moneta/proxies/transformer/transformer_marshal_hmac_spec.rb +1 -1
  137. data/spec/moneta/proxies/transformer/transformer_marshal_prefix_base64_spec.rb +33 -0
  138. data/spec/moneta/proxies/transformer/transformer_marshal_prefix_spec.rb +1 -1
  139. data/spec/moneta/proxies/transformer/transformer_marshal_qp_spec.rb +1 -1
  140. data/spec/moneta/proxies/transformer/transformer_marshal_spec.rb +1 -1
  141. data/spec/moneta/proxies/transformer/transformer_marshal_urlsafe_base64_spec.rb +1 -1
  142. data/spec/moneta/proxies/transformer/transformer_marshal_uuencode_spec.rb +1 -1
  143. data/spec/moneta/proxies/transformer/transformer_msgpack_spec.rb +1 -1
  144. data/spec/moneta/proxies/transformer/transformer_ox_spec.rb +1 -1
  145. data/spec/moneta/proxies/transformer/transformer_php_spec.rb +1 -1
  146. data/spec/moneta/proxies/transformer/transformer_tnet_spec.rb +1 -1
  147. data/spec/moneta/proxies/transformer/transformer_yaml_spec.rb +2 -2
  148. data/spec/moneta/proxies/weak_each_key/weak_each_key_spec.rb +0 -2
  149. data/spec/restserver.rb +15 -0
  150. metadata +47 -66
  151. data/.travis.yml +0 -140
  152. data/lib/moneta/adapters/mongo/base.rb +0 -103
  153. data/lib/moneta/adapters/mongo/moped.rb +0 -163
  154. data/lib/moneta/adapters/mongo/official.rb +0 -156
  155. data/script/reconfigure-couchdb +0 -13
  156. data/spec/moneta/adapters/memcached/dalli/adapter_memcached_dalli_with_default_expires_spec.rb +0 -15
  157. data/spec/moneta/adapters/memcached/native/adapter_memcached_native_with_default_expires_spec.rb +0 -15
  158. data/spec/moneta/adapters/mongo/adapter_mongo_moped_spec.rb +0 -25
  159. data/spec/moneta/adapters/mongo/adapter_mongo_moped_with_default_expires_spec.rb +0 -12
  160. data/spec/moneta/adapters/mongo/adapter_mongo_official_spec.rb +0 -25
  161. data/spec/moneta/adapters/mongo/adapter_mongo_official_with_default_expires_spec.rb +0 -12
  162. data/spec/moneta/adapters/mongo/standard_mongo_moped_spec.rb +0 -7
  163. data/spec/moneta/adapters/mongo/standard_mongo_official_spec.rb +0 -7
  164. data/spec/moneta/adapters/redis/adapter_redis_with_default_expires_spec.rb +0 -10
  165. data/spec/moneta/proxies/proxy/proxy_redis_spec.rb +0 -13
  166. data/spec/quality_spec.rb +0 -51
@@ -0,0 +1,68 @@
1
+ module Moneta
2
+ # @api private
3
+ module HashAdapter
4
+ attr_reader :backend
5
+
6
+ # (see Proxy#key?)
7
+ def key?(key, options = {})
8
+ @backend.has_key?(key)
9
+ end
10
+
11
+ # (see Proxy#load)
12
+ def load(key, options = {})
13
+ @backend[key]
14
+ end
15
+
16
+ # (see Proxy#store)
17
+ def store(key, value, options = {})
18
+ @backend[key] = value
19
+ end
20
+
21
+ # (see Proxy#delete)
22
+ def delete(key, options = {})
23
+ @backend.delete(key)
24
+ end
25
+
26
+ # (see Proxy#clear)
27
+ def clear(options = {})
28
+ @backend.clear
29
+ self
30
+ end
31
+
32
+ # (see Defaults#values_at)
33
+ def values_at(*keys, **options)
34
+ return super unless @backend.respond_to? :values_at
35
+ @backend.values_at(*keys)
36
+ end
37
+
38
+ # (see Defaults#fetch_values)
39
+ def fetch_values(*keys, **options, &defaults)
40
+ return super unless @backend.respond_to? :fetch_values
41
+ defaults ||= {} # prevents KeyError
42
+ @backend.fetch_values(*keys, &defaults)
43
+ end
44
+
45
+ # (see Defaults#slice)
46
+ def slice(*keys, **options)
47
+ return super unless @backend.respond_to? :slice
48
+ @backend.slice(*keys)
49
+ end
50
+
51
+ # (see Defaults#merge!)
52
+ def merge!(pairs, options = {}, &block)
53
+ return super unless method = [:merge!, :update].find do |method|
54
+ @backend.respond_to? method
55
+ end
56
+
57
+ hash = Hash === pairs ? pairs : Hash[pairs.to_a]
58
+ case method
59
+ when :merge!
60
+ @backend.merge!(hash, &block)
61
+ when :update
62
+ @backend.update(hash, &block)
63
+ end
64
+
65
+ self
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ module Moneta
2
+ # @api private
3
+ module IncrementSupport
4
+ # (see Defaults#increment)
5
+ def increment(key, amount = 1, options = {})
6
+ existing = load(key, options)
7
+ value = (existing == nil ? 0 : Integer(existing)) + amount
8
+ store(key, value.to_s, options)
9
+ value
10
+ end
11
+
12
+ def self.included(base)
13
+ base.supports(:increment) if base.respond_to?(:supports)
14
+ end
15
+ end
16
+ end
data/lib/moneta/lock.rb CHANGED
@@ -15,6 +15,7 @@ module Moneta
15
15
  protected
16
16
 
17
17
  def wrap(name, *args, &block)
18
+ self.locks ||= Set.new
18
19
  if locked?
19
20
  yield
20
21
  else
@@ -22,8 +23,12 @@ module Moneta
22
23
  end
23
24
  end
24
25
 
26
+ def locks=(locks)
27
+ Thread.current.thread_variable_set('Moneta::Lock', locks)
28
+ end
29
+
25
30
  def locks
26
- Thread.current['Moneta::Lock'] ||= Set.new
31
+ Thread.current.thread_variable_get('Moneta::Lock')
27
32
  end
28
33
 
29
34
  def lock!(&block)
@@ -0,0 +1,35 @@
1
+ module Moneta
2
+ # This contains overrides of methods in Defaults where additional nil
3
+ # checks are required, because nil values are possible in the store.
4
+ # @api private
5
+ module NilValues
6
+ def fetch_values(*keys, **options)
7
+ values = values_at(*keys, **options)
8
+ return values unless block_given?
9
+ keys.zip(values).map do |key, value|
10
+ if value == nil && !key?(key)
11
+ yield key
12
+ else
13
+ value
14
+ end
15
+ end
16
+ end
17
+
18
+ def slice(*keys, **options)
19
+ keys.zip(values_at(*keys, **options)).reject do |key, value|
20
+ value == nil && !key?(key)
21
+ end
22
+ end
23
+
24
+ def merge!(pairs, options = {})
25
+ pairs.each do |key, value|
26
+ if block_given? && key?(key, options)
27
+ existing = load(key, options)
28
+ value = yield(key, existing, value)
29
+ end
30
+ store(key, value, options)
31
+ end
32
+ self
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ module Moneta
2
+ # @api private
3
+ module OptionSupport
4
+ # Return Moneta store with default options or additional proxies
5
+ #
6
+ # @param [Hash] options Options to merge
7
+ # @return [Moneta store]
8
+ #
9
+ # @api public
10
+ def with(options = nil, &block)
11
+ adapter = self
12
+ if block
13
+ builder = Builder.new(&block)
14
+ builder.adapter(adapter)
15
+ adapter = builder.build.last
16
+ end
17
+ options ? OptionMerger.new(adapter, options) : adapter
18
+ end
19
+
20
+ # Return Moneta store with default option raw: true
21
+ #
22
+ # @return [OptionMerger]
23
+ # @api public
24
+ def raw
25
+ @raw ||=
26
+ begin
27
+ store = with(raw: true, only: [:load, :store, :create, :delete])
28
+ store.instance_variable_set(:@raw, store)
29
+ store
30
+ end
31
+ end
32
+
33
+ # Return Moneta store with default prefix option
34
+ #
35
+ # @param [String] prefix Key prefix
36
+ # @return [OptionMerger]
37
+ # @api public
38
+ def prefix(prefix)
39
+ with(prefix: prefix, except: :clear)
40
+ end
41
+
42
+ # Return Moneta store with default expiration time
43
+ #
44
+ # @param [Integer] expires Default expiration time
45
+ # @return [OptionMerger]
46
+ # @api public
47
+ def expires(expires)
48
+ with(expires: expires, only: [:store, :create, :increment])
49
+ end
50
+ end
51
+ end
data/lib/moneta/pool.rb CHANGED
@@ -87,6 +87,7 @@ module Moneta
87
87
  @waiting_since = [] if @timeout
88
88
  @last_checkout = nil
89
89
  @stopping = false
90
+ @idle_time = nil
90
91
 
91
92
  # Launch the manager thread
92
93
  @thread = run
@@ -126,10 +127,16 @@ module Moneta
126
127
  populate_stores
127
128
 
128
129
  until @stopping && @stores.empty?
130
+ loop_start = Time.now
131
+
129
132
  # Block until a message arrives, or until we time out for some reason
130
- if request = pop
131
- handle_request(request)
132
- end
133
+ request = pop
134
+
135
+ # Record how long we were idle, for stats purposes
136
+ @idle_time = Time.now - loop_start
137
+
138
+ # If a message arrived, handle it
139
+ handle_request(request) if request
133
140
 
134
141
  # Handle any stale checkout requests
135
142
  handle_timed_out_requests
@@ -186,7 +193,7 @@ module Moneta
186
193
  # @return [Integer, nil]
187
194
  def timeout
188
195
  # Time to wait before there will be stores that should be closed
189
- ttl = if @ttl && @last_checkout && !@available.empty?
196
+ ttl = if @ttl && @last_checkout && stores_available? && stores_unneeded?
190
197
  [@ttl - (Time.now - @last_checkout), 0].max
191
198
  end
192
199
 
@@ -199,6 +206,18 @@ module Moneta
199
206
  [ttl, timeout].compact.min
200
207
  end
201
208
 
209
+ def stores_available?
210
+ !@available.empty?
211
+ end
212
+
213
+ def stores_unneeded?
214
+ @stores.length > @min
215
+ end
216
+
217
+ def stores_maxed?
218
+ @max != nil && @stores.length == @max
219
+ end
220
+
202
221
  def pop
203
222
  @mutex.synchronize do
204
223
  @resource.wait(@mutex, timeout) if @inbox.empty?
@@ -218,7 +237,7 @@ module Moneta
218
237
  reply.resolve(ShutdownError.new("Shutting down"))
219
238
  elsif !@available.empty?
220
239
  reply.resolve(@available.pop)
221
- elsif !@max || @stores.length < @max
240
+ elsif !stores_maxed?
222
241
  begin
223
242
  reply.resolve(add_store)
224
243
  rescue => e
@@ -258,7 +277,8 @@ module Moneta
258
277
  waiting: @waiting.length,
259
278
  longest_wait: @timeout && !@waiting_since.empty? ? @waiting_since.first.dup : nil,
260
279
  stopping: @stopping,
261
- last_checkout: @last_checkout && @last_checkout.dup)
280
+ last_checkout: @last_checkout && @last_checkout.dup,
281
+ idle_time: @idle_time.dup)
262
282
  end
263
283
 
264
284
  def handle_request(request)
@@ -298,6 +318,18 @@ module Moneta
298
318
  # by the manager after the ttl
299
319
  def close; end
300
320
 
321
+ def each_key(&block)
322
+ wrap(:each_key) do
323
+ raise NotImplementedError, "each_key is not supported on this proxy" \
324
+ unless supports? :each_key
325
+
326
+ return enum_for(:each_key) { adapter ? adapter.each_key.size : check_out! { adapter.each_key.size } } unless block_given?
327
+
328
+ adapter.each_key(&block)
329
+ self
330
+ end
331
+ end
332
+
301
333
  # Tells the manager to close all stores. It will not be possible to use
302
334
  # the store after this.
303
335
  def stop
data/lib/moneta/proxy.rb CHANGED
@@ -22,7 +22,7 @@ module Moneta
22
22
  raise NotImplementedError, "each_key is not supported on this proxy" \
23
23
  unless supports? :each_key
24
24
 
25
- return enum_for(:each_key) unless block_given?
25
+ return enum_for(:each_key) { adapter.each_key.size } unless block_given?
26
26
  adapter.each_key(&block)
27
27
  self
28
28
  end
data/lib/moneta/server.rb CHANGED
@@ -4,6 +4,154 @@ module Moneta
4
4
  # Moneta server to be used together with Moneta::Adapters::Client
5
5
  # @api public
6
6
  class Server
7
+ TIMEOUT = 1
8
+ MAXSIZE = 0x100000
9
+
10
+ # @api private
11
+ class Connection
12
+ def initialize(io, store)
13
+ @io = io
14
+ @store = store
15
+ @fiber = Fiber.new { run }
16
+ end
17
+
18
+ def resume(result = nil)
19
+ @fiber.resume result
20
+ end
21
+
22
+ private
23
+
24
+ # The return value of this function will be sent to the reactor.
25
+ #
26
+ # @return [:closed,Exception]
27
+ def run
28
+ catch :closed do
29
+ loop { write_dispatch(read_msg) }
30
+ end
31
+ :closed
32
+ rescue => ex
33
+ ex
34
+ ensure
35
+ @io.close unless @io.closed?
36
+ end
37
+
38
+ def dispatch(method, args)
39
+ case method
40
+ when :key?, :load, :delete, :increment, :create, :features
41
+ @store.public_send(method, *args)
42
+ when :store, :clear
43
+ @store.public_send(method, *args)
44
+ nil
45
+ when :each_key
46
+ yield_each(@store.each_key)
47
+ nil
48
+ end
49
+ rescue => ex
50
+ ex
51
+ end
52
+
53
+ def write_dispatch(msg)
54
+ method, *args = msg
55
+ result = dispatch(method, args)
56
+ write(result)
57
+ end
58
+
59
+ def read_msg
60
+ size = read(4).unpack('N').first
61
+ throw :closed, 'Message too big' if size > MAXSIZE
62
+ Marshal.load(read(size))
63
+ end
64
+
65
+ def read(len)
66
+ buffer = ''
67
+ loop do
68
+ begin
69
+ case received = @io.recv_nonblock(len)
70
+ when '', nil
71
+ throw :closed, 'Closed during read'
72
+ else
73
+ buffer << received
74
+ len -= received.bytesize
75
+ end
76
+ rescue IO::WaitReadable, IO::WaitWritable
77
+ yield_to_reactor(:read)
78
+ rescue Errno::ECONNRESET
79
+ throw :closed, 'Closed during read'
80
+ rescue IOError => ex
81
+ if ex.message =~ /closed stream/
82
+ throw :closed, 'Closed during read'
83
+ else
84
+ raise
85
+ end
86
+ end
87
+ break if len == 0
88
+ end
89
+ buffer
90
+ end
91
+
92
+ def write(obj)
93
+ buffer = pack(obj)
94
+ until buffer.empty?
95
+ begin
96
+ len = sendmsg(buffer)
97
+ buffer = buffer.byteslice(len...buffer.length)
98
+ rescue IO::WaitWritable, Errno::EINTR
99
+ yield_to_reactor(:write)
100
+ end
101
+ end
102
+ nil
103
+ end
104
+
105
+ # Detect support for socket#sendmsg_nonblock
106
+ Socket.new(Socket::AF_INET, Socket::SOCK_STREAM).tap do |socket|
107
+ begin
108
+ socket.sendmsg_nonblock('probe')
109
+ rescue Errno::EPIPE, Errno::ENOTCONN
110
+ def sendmsg(msg)
111
+ @io.sendmsg_nonblock(msg)
112
+ end
113
+ rescue NotImplementedError
114
+ def sendmsg(msg)
115
+ @io.write_nonblock(msg)
116
+ end
117
+ end
118
+ end
119
+
120
+ def yield_to_reactor(mode = :read)
121
+ if Fiber.yield(mode) == :close
122
+ throw :closed, 'Closed by reactor'
123
+ end
124
+ end
125
+
126
+ def pack(obj)
127
+ s = Marshal.dump(obj)
128
+ [s.bytesize].pack('N') << s
129
+ end
130
+
131
+ def yield_each(enumerator)
132
+ received_break = false
133
+ loop do
134
+ case msg = read_msg
135
+ when %w{NEXT}
136
+ # This will raise a StopIteration at the end of the enumeration,
137
+ # which will exit the loop.
138
+ write(enumerator.next)
139
+ when %w{BREAK}
140
+ # This is received when the client wants to stop the enumeration.
141
+ received_break = true
142
+ break
143
+ else
144
+ # Otherwise, the client is attempting to call another method within
145
+ # an `each` block.
146
+ write_dispatch(msg)
147
+ end
148
+ end
149
+ ensure
150
+ # This tells the client to stop enumerating
151
+ write(StopIteration.new("Server initiated stop")) unless received_break
152
+ end
153
+ end
154
+
7
155
  # @param [Hash] options
8
156
  # @option options [Integer] :port (9000) TCP port
9
157
  # @option options [String] :socket Alternative Unix socket file name
@@ -11,7 +159,9 @@ module Moneta
11
159
  @store = store
12
160
  @server = start(options)
13
161
  @ios = [@server]
14
- @clients = {}
162
+ @reads = @ios.dup
163
+ @writes = []
164
+ @connections = {}
15
165
  @running = false
16
166
  end
17
167
 
@@ -26,94 +176,89 @@ module Moneta
26
176
  #
27
177
  # @note This method blocks!
28
178
  def run
29
- raise 'Already running' if @running
179
+ raise 'Already running' if running?
30
180
  @stop = false
31
181
  @running = true
32
182
  begin
33
183
  mainloop until @stop
34
184
  ensure
35
- File.unlink(@socket) if @socket
36
- @ios.each { |io| io.close rescue nil }
185
+ @running = false
186
+ @server.close unless @server.closed?
187
+ @ios
188
+ .reject { |io| io == @server }
189
+ .each { |io| close_connection(io) }
190
+ File.unlink(@socket) if @socket rescue nil
37
191
  end
38
192
  end
39
193
 
40
194
  # Stop the server
41
195
  def stop
42
- raise 'Not running' unless @running
196
+ raise 'Not running' unless running?
43
197
  @stop = true
44
198
  @server.close
45
- @server = nil
199
+ nil
46
200
  end
47
201
 
48
202
  private
49
203
 
50
- TIMEOUT = 1
51
- MAXSIZE = 0x100000
52
-
53
204
  def mainloop
54
- if ios = IO.select(@ios, nil, @ios, TIMEOUT)
55
- ios[2].each do |io|
56
- io.close
57
- delete_client(io)
58
- end
59
- ios[0].each do |io|
60
- if io == @server
61
- if client = @server.accept
62
- @ios << client
63
- @clients[client] = ''
64
- end
65
- elsif io.closed? || io.eof?
66
- delete_client(io)
67
- else
68
- handle(io, @clients[io] << io.readpartial(0xFFFF))
69
- end
205
+ if ready = IO.select(@reads, @writes, @ios, TIMEOUT)
206
+ reads, writes, errors = ready
207
+ errors.each { |io| close_connection(io) }
208
+
209
+ @reads -= reads
210
+ reads.each do |io|
211
+ io == @server ? accept_connection : resume(io)
70
212
  end
213
+
214
+ @writes -= writes
215
+ writes.each { |io| resume(io) }
71
216
  end
72
- rescue SignalException => ex
73
- warn "Moneta::Server - #{ex.message}"
74
- raise if ex.signo == 15 || ex.signo == 2 # SIGTERM or SIGINT
75
- rescue => ex
217
+ rescue SignalException => signal
218
+ warn "Moneta::Server - received #{signal}"
219
+ case signal.signo
220
+ when Signal.list['INT'], Signal.list['TERM']
221
+ @stop = true # graceful exit
222
+ end
223
+ rescue IOError => ex
224
+ # We get a lot of these "closed stream" errors, which we ignore
225
+ raise unless ex.message =~ /closed stream/
226
+ rescue Errno::EBADF => ex
76
227
  warn "Moneta::Server - #{ex.message}"
77
228
  end
78
229
 
79
- def delete_client(io)
230
+ def accept_connection
231
+ io = @server.accept
232
+ @connections[io] = Connection.new(io, @store)
233
+ @ios << io
234
+ resume(io)
235
+ ensure
236
+ @reads << @server
237
+ end
238
+
239
+ def delete_connection(io)
80
240
  @ios.delete(io)
81
- @clients.delete(io)
241
+ @reads.delete(io)
242
+ @writes.delete(io)
82
243
  end
83
244
 
84
- def pack(obj)
85
- s = Marshal.dump(obj)
86
- [s.bytesize].pack('N') << s
245
+ def close_connection(io)
246
+ delete_connection(io)
247
+ @connections.delete(io).resume(:close)
87
248
  end
88
249
 
89
- def handle(io, buffer)
90
- return if buffer.bytesize < 8 # At least 4 bytes for the marshalled array
91
- size = buffer[0, 4].unpack('N').first
92
- if size > MAXSIZE
93
- delete_client(io)
94
- return
95
- end
96
- return if buffer.bytesize < 4 + size
97
- buffer.slice!(0, 4)
98
- method, *args = Marshal.load(buffer.slice!(0, size))
99
- case method
100
- when :key?, :load, :delete, :increment, :create
101
- io.write(pack(@store.send(method, *args)))
102
- when :features
103
- # all features except each_key are supported
104
- io.write(pack(@store.features - [:each_key]))
105
- when :store, :clear
106
- @store.send(method, *args)
107
- io.write(@nil ||= pack(nil))
108
- else
109
- raise 'Invalid method call'
250
+ def resume(io)
251
+ case result = @connections[io].resume
252
+ when :closed # graceful exit
253
+ delete_connection(io)
254
+ when Exception # messy exit
255
+ delete_connection(io)
256
+ raise result
257
+ when :read
258
+ @reads << io
259
+ when :write
260
+ @writes << io
110
261
  end
111
- rescue IOError => ex
112
- warn "Moneta::Server - #{ex.message}" unless ex.message =~ /closed/
113
- delete_client(io)
114
- rescue => ex
115
- warn "Moneta::Server - #{ex.message}"
116
- io.write(pack(Exception.new(ex.message)))
117
262
  end
118
263
 
119
264
  def start(options)
@@ -133,5 +278,14 @@ module Moneta
133
278
  TCPServer.open(options[:host] || '127.0.0.1', options[:port] || 9000)
134
279
  end
135
280
  end
281
+
282
+ def stats
283
+ {
284
+ connections: @connections.length,
285
+ reading: @reads.length,
286
+ writing: @writes.length,
287
+ total: @ios.length
288
+ }
289
+ end
136
290
  end
137
291
  end