readthis 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a7b4dfe48dce8fa54170963b720ee202dedb19df
4
- data.tar.gz: 0e611efe5dcd71df78491618472a0394ec408a69
2
+ SHA256:
3
+ metadata.gz: 9ce73086f82fd00b89f037d6aba17fee9ba94b76766f020c65f1305c1faa122d
4
+ data.tar.gz: f63760bc92f7a0f4023b2e4817192282b5e77e34b61c73e6a5f7d4b60bc3ce02
5
5
  SHA512:
6
- metadata.gz: 0f436a2f365f761d8be21ee93041f65b990b2f2ea92d8e95486bc127dbe8f4f3a27cad1250237a0cc1a8314e8b6992ab6698e6a8f9455e1d89ab4dab2b5b3148
7
- data.tar.gz: 9e18f5b63c9bfc05b1e1e8f110a7957a7d9c6cedb70868e91c810509e42a93985b0fd6d84619c4880e58398d63d5a2e1ed0def088a760849d4dbd1275dd9c4f8
6
+ metadata.gz: bcd82892c461b1b34f9fa15873cb4948db25974e39acdb34615de49155346f6e858543c06f07207f0c00bfc3b8064d2856c4c05f49edab9c36b5884dc78b4c63
7
+ data.tar.gz: 51d85402830b660eba320626bd4a4db3928f33c3a3f77fbc2934fe553793b2809bd1074a8abaea9d63183001c75f1e5ee5f5b0d612991da2badc05e703c31be5
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Parker Selbert
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Readthis is a Redis backed cache client for Ruby. It is a drop in replacement
10
10
  for any `ActiveSupport` compliant cache and can also be used for [session
11
- storage](#session-storage). Above all Readthis emphasizes performance,
11
+ storage](#session-storage). Above all Readthis emphasizes performance,
12
12
  simplicity, and explicitness.
13
13
 
14
14
  For new projects there isn't any reason to stick with Memcached. Redis is as
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'readthis'
2
4
 
3
5
  module ActiveSupport
@@ -5,6 +7,6 @@ module ActiveSupport
5
7
  # the ActiveSupport `cache_store` is set to `:readthis_store` it will resolve
6
8
  # to `Readthis::Cache`.
7
9
  module Cache
8
- ReadthisStore ||= Readthis::Cache # rubocop:disable Style/ConstantName
10
+ ReadthisStore ||= Readthis::Cache # rubocop:disable Naming/ConstantName
9
11
  end
10
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'readthis/cache'
2
4
  require 'readthis/errors'
3
5
  require 'readthis/serializers'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'readthis/entity'
2
4
  require 'readthis/expanders'
3
5
  require 'readthis/passthrough'
@@ -6,6 +8,9 @@ require 'redis'
6
8
  require 'connection_pool'
7
9
 
8
10
  module Readthis
11
+ # Readthis is a Redis backed cache client. It is a drop in replacement for
12
+ # any `ActiveSupport` compliant cache Above all Readthis emphasizes
13
+ # performance, simplicity, and explicitness.
9
14
  class Cache
10
15
  attr_reader :entity, :notifications, :options, :pool, :scripts
11
16
 
@@ -156,7 +161,7 @@ module Readthis
156
161
  count = options.fetch(:count, 1000)
157
162
  deleted = 0
158
163
 
159
- until cursor == '0'.freeze
164
+ until cursor == '0'
160
165
  cursor, matched = store.scan(cursor || 0, match: namespaced, count: count)
161
166
 
162
167
  if matched.any?
@@ -218,7 +223,18 @@ module Readthis
218
223
  # Increment a key in the store.
219
224
  #
220
225
  # If the key doesn't exist it will be initialized at 0. If the key exists
221
- # but it isn't a Fixnum it will be initialized at 0.
226
+ # but it isn't a Fixnum it will be coerced to 0.
227
+ #
228
+ # Note that this method does *not* use Redis' native `incr` or `incrby`
229
+ # commands. Those commands only work with number-like strings, and are
230
+ # incompatible with the encoded values Readthis writes to the store. The
231
+ # behavior of `incrby` is preserved as much as possible, but incrementing
232
+ # is not an atomic action. If multiple clients are incrementing the same
233
+ # key there will be a "last write wins" race condition, causing incorrect
234
+ # counts.
235
+ #
236
+ # If you absolutely require correct counts it is better to use the Redis
237
+ # client directly.
222
238
  #
223
239
  # @param [String] key Key for lookup
224
240
  # @param [Fixnum] amount Value to increment by
@@ -226,20 +242,20 @@ module Readthis
226
242
  #
227
243
  # @example
228
244
  #
229
- # cache.increment('counter') # => 0
230
245
  # cache.increment('counter') # => 1
231
246
  # cache.increment('counter', 2) # => 3
232
247
  #
233
248
  def increment(key, amount = 1, options = {})
234
- invoke(:increment, key) do |_store|
235
- alter(key, amount, options)
249
+ invoke(:increment, key) do |store|
250
+ alter(store, key, amount, options)
236
251
  end
237
252
  end
238
253
 
239
254
  # Decrement a key in the store.
240
255
  #
241
256
  # If the key doesn't exist it will be initialized at 0. If the key exists
242
- # but it isn't a Fixnum it will be initialized at 0.
257
+ # but it isn't a Fixnum it will be coerced to 0. Like `increment`, this
258
+ # does not make use of the native `decr` or `decrby` commands.
243
259
  #
244
260
  # @param [String] key Key for lookup
245
261
  # @param [Fixnum] amount Value to decrement by
@@ -252,8 +268,8 @@ module Readthis
252
268
  # cache.decrement('counter', 2) # => 17
253
269
  #
254
270
  def decrement(key, amount = 1, options = {})
255
- invoke(:decrement, key) do |_store|
256
- alter(key, amount * -1, options)
271
+ invoke(:decrement, key) do |store|
272
+ alter(store, key, -amount, options)
257
273
  end
258
274
  end
259
275
 
@@ -365,17 +381,27 @@ module Readthis
365
381
  end
366
382
  end
367
383
 
368
- # Clear the entire cache. This flushes the current database, no
369
- # globbing is applied.
384
+ # Clear the entire cache by flushing the current database.
370
385
  #
371
- # @param [Hash] _options Options, only present for compatibility
386
+ # This flushes everything in the current database, with no globbing
387
+ # applied. Data in other numbered databases will be preserved.
388
+ #
389
+ # @option options [Hash] :async Flush the database asynchronously, only
390
+ # supported in Redis 4.0+
372
391
  #
373
392
  # @example
374
393
  #
375
394
  # cache.clear #=> 'OK'
376
- #
377
- def clear(_options = nil)
378
- invoke(:clear, '*', &:flushdb)
395
+ # cache.clear(async: true) #=> 'OK'
396
+ #
397
+ def clear(options = {})
398
+ invoke(:clear, '*') do |store|
399
+ if options[:async]
400
+ store.flushdb(async: true)
401
+ else
402
+ store.flushdb
403
+ end
404
+ end
379
405
  end
380
406
 
381
407
  protected
@@ -389,8 +415,8 @@ module Readthis
389
415
  end
390
416
 
391
417
  def write_entity(key, value, store, options)
392
- namespaced = encode(namespaced_key(key, options))
393
- dumped = encode(entity.dump(value, options))
418
+ namespaced = namespaced_key(key, options)
419
+ dumped = entity.dump(value, options)
394
420
 
395
421
  if (expiration = options[:expires_in])
396
422
  store.setex(namespaced, coerce_expiration(expiration), dumped)
@@ -401,20 +427,34 @@ module Readthis
401
427
 
402
428
  private
403
429
 
404
- def alter(key, amount, options)
405
- delta = read(key, options).to_i + amount
406
- write(key, delta, options)
407
- delta
408
- end
430
+ def alter(store, key, amount, options)
431
+ options = merged_options(options)
432
+ namespaced = namespaced_key(key, options)
409
433
 
410
- def coerce_expiration(expires_in)
411
- Float(expires_in).ceil
434
+ loaded = entity.load(store.get(namespaced))
435
+ change = loaded.to_i + amount
436
+ dumped = entity.dump(change, options)
437
+ expiration = fallback_expiration(store, namespaced, options)
438
+
439
+ if expiration
440
+ store.setex(namespaced, coerce_expiration(expiration), dumped)
441
+ else
442
+ store.set(namespaced, dumped)
443
+ end
444
+
445
+ change
412
446
  end
413
447
 
414
- def encode(string)
415
- string = string.frozen? ? string.dup : string
448
+ def fallback_expiration(store, key, options)
449
+ options.fetch(:expires_in) do
450
+ ttl = store.ttl(key)
416
451
 
417
- string.force_encoding(Encoding::BINARY)
452
+ ttl > 0 ? ttl : nil
453
+ end
454
+ end
455
+
456
+ def coerce_expiration(expires_in)
457
+ Float(expires_in).ceil
418
458
  end
419
459
 
420
460
  def instrument(name, key)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zlib'
2
4
 
3
5
  module Readthis
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Readthis
2
4
  # This is the base error that all other specific errors inherit from,
3
5
  # making it possible to rescue the `ReadthisError` superclass.
@@ -1,9 +1,38 @@
1
1
  module Readthis
2
+ # Expander methods are used to transform an object into a string suitable for
3
+ # use as a cache key.
2
4
  module Expanders
5
+ # Expand an object into a suitable cache key.
6
+ #
7
+ # The behavior of `expand_key` is largely modeled on the `expand_cache_key`
8
+ # method from `ActiveSupport::Cache`, with some subtle additions.
9
+ #
10
+ # @param [Object] key An object to stringify. Arrays, hashes, and objects
11
+ # that respond to either `cache_key` or `to_param` have direct support.
12
+ # All other objects will be coerced to a string, and frozen strings will
13
+ # be duplicated.
14
+ #
15
+ # @return [String] A cache key string.
16
+ #
17
+ # @example String expansion
18
+ #
19
+ # Readthis::Expanders.expand_key('typical-key')
20
+ # 'typical-key'
21
+ #
22
+ # @example Array string expansion
23
+ #
24
+ # Readthis::Expanders.expand_key(['a', 'b', [:c, 'd'], 1])
25
+ # 'a/b/c/d/1'
26
+ #
27
+ # @example Hash expansion
28
+ #
29
+ # Readthis::Expanders.expand_key(c: 1, 'a' => 3, b: 2)
30
+ # 'a=3/b=2/c=1'
31
+ #
3
32
  def self.expand_key(key)
4
33
  case
5
- when key.respond_to?(:cache_key)
6
- key.cache_key
34
+ when key.is_a?(String)
35
+ key.frozen? ? key.dup : key
7
36
  when key.is_a?(Array)
8
37
  key.flat_map { |elem| expand_key(elem) }.join('/')
9
38
  when key.is_a?(Hash)
@@ -11,6 +40,8 @@ module Readthis
11
40
  .sort_by { |hkey, _| hkey.to_s }
12
41
  .map { |hkey, val| "#{hkey}=#{val}" }
13
42
  .join('/')
43
+ when key.respond_to?(:cache_key)
44
+ key.cache_key
14
45
  when key.respond_to?(:to_param)
15
46
  key.to_param
16
47
  else
@@ -18,14 +49,32 @@ module Readthis
18
49
  end
19
50
  end
20
51
 
21
- def self.namespace_key(key, namespace)
52
+ # Prepend a namespace to a key after expanding it.
53
+ #
54
+ # @param [Object] key An object to stringify.
55
+ # @param [String] namespace An optional namespace to prepend, if `nil` it
56
+ # is ignored.
57
+ #
58
+ # @return [String] A binary encoded string combining the namespace and key.
59
+ #
60
+ # @example Applying a namespace
61
+ #
62
+ # Knuckles::Expanders.namespace_key('alpha', 'greek')
63
+ # 'greek:alpha'
64
+ #
65
+ # @example Omitting a namespace
66
+ #
67
+ # Knuckles::Expanders.namespace_key('alpha', nil)
68
+ # 'alpha'
69
+ #
70
+ def self.namespace_key(key, namespace = nil)
22
71
  expanded = expand_key(key)
23
72
 
24
73
  if namespace
25
74
  "#{namespace}:#{expanded}"
26
75
  else
27
76
  expanded
28
- end
77
+ end.force_encoding(Encoding::BINARY)
29
78
  end
30
79
  end
31
80
  end
@@ -1,11 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Readthis
4
+ # The `Passthrough` serializer performs no encoding on objects. It should be
5
+ # used when caching simple string objects when the overhead of marshalling or
6
+ # other serialization isn't desired.
4
7
  module Passthrough
8
+ # Dump an object to string, without performing any encoding on it.
9
+ #
10
+ # @param [Object] value Any object to be dumped as a string. Frozen strings
11
+ # will be duplicated.
12
+ #
13
+ # @return [String] The converted object.
14
+ #
5
15
  def self.dump(value)
6
- value.dup
16
+ case value
17
+ when String then value.dup
18
+ else value.to_s
19
+ end
7
20
  end
8
21
 
22
+ # Load an object without modifying it at all.
23
+ #
24
+ # @param [String] value The object to return, expected to be a string.
25
+ #
26
+ # @return [String] The original value.
9
27
  def self.load(value)
10
28
  value
11
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Readthis
2
4
  # The `Scripts` class is used to conveniently execute lua scripts. The first
3
5
  # time a command is run it is stored on the server and subsequently referred
@@ -48,7 +50,7 @@ module Readthis
48
50
  end
49
51
 
50
52
  def abs_path(filename)
51
- dir = File.expand_path(File.dirname(__FILE__))
53
+ dir = File.expand_path(__dir__)
52
54
 
53
55
  File.join(dir, '../../script', filename)
54
56
  end
@@ -1,8 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'readthis/errors'
3
5
  require 'readthis/passthrough'
4
6
 
5
7
  module Readthis
8
+ # Instances of the `Serializers` class are used to configure the use of
9
+ # multiple "serializers" for a single cache.
10
+ #
11
+ # Readthis uses Ruby's `Marshal` module for serializing all values by
12
+ # default. This isn't always the fastest option, and depending on your use
13
+ # case it may be desirable to use a faster but less flexible serializer.
14
+ #
15
+ # By default Readthis knows about 3 different serializers:
16
+ #
17
+ # * Marshal
18
+ # * JSON
19
+ # * Passthrough
20
+ #
21
+ # If all cached data can safely be represented as a string then use the
22
+ # pass-through serializer:
23
+ #
24
+ # Readthis::Cache.new(marshal: Readthis::Passthrough)
25
+ #
26
+ # You can introduce up to four additional serializers by configuring
27
+ # `serializers` on the Readthis module. For example, if you wanted to use the
28
+ # extremely fast Oj library for JSON serialization:
29
+ #
30
+ # Readthis.serializers << Oj
31
+ # # Freeze the serializers to ensure they aren't changed at runtime.
32
+ # Readthis.serializers.freeze!
33
+ # Readthis::Cache.new(marshal: Oj)
34
+ #
35
+ # Be aware that the *order in which you add serializers matters*. Serializers
36
+ # are sticky and a flag is stored with each cached value. If you subsequently
37
+ # go to deserialize values and haven't configured the same serializers in the
38
+ # same order your application will raise errors.
6
39
  class Serializers
7
40
  # Defines the default set of three serializers: Marshal, Passthrough, and
8
41
  # JSON. With a hard limit of 7 that leaves 4 additional slots.
@@ -32,7 +65,7 @@ module Readthis
32
65
  # @param [Module] serializer Any object that responds to `dump` and `load`
33
66
  # @return [self] Returns itself for possible chaining
34
67
  #
35
- # @example
68
+ # @example Adding Oj as an accepted serializer
36
69
  #
37
70
  # serializers = Readthis::Serializers.new
38
71
  # serializers << Oj
@@ -53,16 +86,24 @@ module Readthis
53
86
 
54
87
  # Freeze the serializers hash, preventing modification.
55
88
  #
89
+ # @return [self] The serializer instance.
90
+ #
56
91
  def freeze!
57
92
  serializers.freeze
93
+
94
+ self
58
95
  end
59
96
 
60
97
  # Reset the instance back to the default state. Useful for cleanup during
61
98
  # testing.
62
99
  #
100
+ # @return [self] The serializer instance.
101
+ #
63
102
  def reset!
64
103
  @serializers = BASE_SERIALIZERS.dup
65
104
  @inverted = @serializers.invert
105
+
106
+ self
66
107
  end
67
108
 
68
109
  # Find a flag for a serializer object.
@@ -72,7 +113,7 @@ module Readthis
72
113
  # @raise [UnknownSerializerError] Indicates that a serializer was
73
114
  # specified, but hasn't been configured for usage.
74
115
  #
75
- # @example
116
+ # @example Find the JSON serializer's flag
76
117
  #
77
118
  # serializers.assoc(JSON) #=> 1
78
119
  #
@@ -91,7 +132,7 @@ module Readthis
91
132
  # @param [Number] flag Integer to look up the serializer object by
92
133
  # @return [Module] The serializer object
93
134
  #
94
- # @example
135
+ # @example Find the serializer associated with flag 1
95
136
  #
96
137
  # serializers.rassoc(1) #=> Marshal
97
138
  #