readthis 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  #