kasket 4.5.1 → 4.9.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: 6e712453519dbc2e9d65176a7b2643f5d93f5fbc
4
- data.tar.gz: fe75bab997f46475cb7fdcf23302d02f85335e9f
2
+ SHA256:
3
+ metadata.gz: e1399453146b7dcf0974eb42a7b80375291b2978415269d686ff66ccb77b9f53
4
+ data.tar.gz: 4649d5f06595e558f859472439733208e62229841901bf9dc81b602c3f78e07e
5
5
  SHA512:
6
- metadata.gz: 5c7846aae753e06fd1a39bff78cb6bc84b6602cd8c6245dc2bff0d4b9705ded35723f005f2e27b4bf838e05afba59542d5c826793c90be5d6afd4fc702ad2532
7
- data.tar.gz: 4ae174cf74a6176f843ff40bffcc965ccb9ea78dba55a3f6d19f2621b2d96ae218d0521391173c0b250e586c4e427c07c3e41d37e5779aed2619342a56604616
6
+ metadata.gz: 7a6eeea3f1cdfe9bb349379cb07f4d13a21bfa0867ed09e9795746d91f74a69349ee15eb83d76e6f4776a717d8d4f2b8ce1479d41a095ac2e275502e3b54c99c
7
+ data.tar.gz: fd34ac51846f1748a47cf0de7b2c5f27ef843abe93429285e3830a8a8c3afb53822f7eab19452af3df5c99483323e1c0d9fd2a727ddda535626e58408a5a28e1
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Kasket [![Build Status](https://secure.travis-ci.org/zendesk/kasket.svg)](http://travis-ci.org/zendesk/kasket)
1
+ # Kasket [![Build status](https://circleci.com/gh/zendesk/kasket.svg?style=svg)](https://circleci.com/gh/zendesk/kasket)
2
2
 
3
3
  ### Puts a cap on your queries
4
4
  A caching layer for ActiveRecord (3.x and 4.x)
@@ -34,8 +34,8 @@ This will include the required modules into ActiveRecord.
34
34
  By default, Kasket will cache each instance collection with a maximum length of 100.
35
35
  You can override this by passing the `:max_collection_size` option to the `Kasket.setup` call:
36
36
 
37
- ```
38
- Kasket.setup(:max_collection_size => 50)
37
+ ```ruby
38
+ Kasket.setup(max_collection_size: 50)
39
39
  ```
40
40
 
41
41
  #### Write-Through Caching
@@ -44,8 +44,8 @@ By default, when a model is saved, Kasket will invalidate cache entries by delet
44
44
  You can pass ':write_through => true' to the `Kasket.setup` call to get write-through cache
45
45
  semantics instead. In this mode, the model will be updated in the cache as well as the database.
46
46
 
47
- ```
48
- Kasket.setup(:write_through => true)
47
+ ```ruby
48
+ Kasket.setup(write_through: true)
49
49
  ```
50
50
 
51
51
  ## Configuring caching of your models
@@ -55,7 +55,7 @@ configuration.
55
55
 
56
56
  If you have an `Account` model, you can can do the simplest caching configuration like:
57
57
 
58
- ```
58
+ ```ruby
59
59
  Account.has_kasket
60
60
  ```
61
61
 
@@ -65,7 +65,7 @@ All other calls (say, `Account.find_by_subdomain('zendesk')`) are untouched.
65
65
 
66
66
  If you wanted to configure a caching index on the subdomain attribute of the Account model, you would simply write
67
67
 
68
- ```
68
+ ```ruby
69
69
  Account.has_kasket_on :subdomain
70
70
  ```
71
71
 
@@ -81,6 +81,7 @@ The goal of Kasket is to be as safe as possible to use, so the cache is expired
81
81
  * When you save a model instance
82
82
  * When your database schema changes
83
83
  * When you install a new version of Kasket
84
+ * After a global or per-model TTL
84
85
  * When you ask it to
85
86
 
86
87
  ### Cache expiry on instance save
@@ -96,12 +97,26 @@ If you somehow change your table schema, all cache entries for that table will a
96
97
 
97
98
  All Kasket cache keys contain the Kasket version number, so upgrading Kasket will expire all Kasket cache entries.
98
99
 
100
+ ### Cache expiry by TTL
101
+
102
+ Sometimes caches like memcache can become incoherent. One layer of mitigation for this problem is to specify the maximum length a value may stay in cache before being expired and re-calculated. You can configure an optional default TTL value at setup:
103
+
104
+ ```ruby
105
+ Kasket.setup(default_expires_in: 24.hours)
106
+ ```
107
+
108
+ You can further specify per-model TTL values:
109
+
110
+ ```ruby
111
+ Account.kasket_expires_in 5.minutes
112
+ ```
113
+
99
114
  ### Manually expiring caches
100
115
 
101
116
  If you have model methods that update the database behind the back of ActiveRecord, you need to mark these methods
102
117
  as being dirty.
103
118
 
104
- ```
119
+ ```ruby
105
120
  Account.kasket_dirty_methods :update_last_action
106
121
  ```
107
122
 
data/lib/kasket.rb CHANGED
@@ -15,7 +15,11 @@ module Kasket
15
15
  autoload :SelectManagerMixin, 'kasket/select_manager_mixin'
16
16
  autoload :RelationMixin, 'kasket/relation_mixin'
17
17
 
18
- CONFIGURATION = { max_collection_size: 100, write_through: false } # rubocop:disable Style/MutableConstant
18
+ CONFIGURATION = { # rubocop:disable Style/MutableConstant
19
+ max_collection_size: 100,
20
+ write_through: false,
21
+ default_expires_in: nil
22
+ }
19
23
 
20
24
  module_function
21
25
 
@@ -23,13 +27,14 @@ module Kasket
23
27
  return if ActiveRecord::Base.respond_to?(:has_kasket)
24
28
 
25
29
  CONFIGURATION[:max_collection_size] = options[:max_collection_size] if options[:max_collection_size]
26
- CONFIGURATION[:write_through] = options[:write_through] if options[:write_through]
30
+ CONFIGURATION[:write_through] = options[:write_through] if options[:write_through]
31
+ CONFIGURATION[:default_expires_in] = options[:default_expires_in] if options[:default_expires_in]
27
32
 
28
33
  ActiveRecord::Base.extend(Kasket::ConfigurationMixin)
29
34
 
30
35
  if defined?(ActiveRecord::Relation)
31
- ActiveRecord::Relation.send(:include, Kasket::RelationMixin)
32
- Arel::SelectManager.send(:include, Kasket::SelectManagerMixin)
36
+ ActiveRecord::Relation.include Kasket::RelationMixin
37
+ Arel::SelectManager.include Kasket::SelectManagerMixin
33
38
  end
34
39
  end
35
40
 
@@ -37,10 +42,15 @@ module Kasket
37
42
  @cache_store = ActiveSupport::Cache.lookup_store(options)
38
43
  end
39
44
 
40
- def self.cache
45
+ def self.cache_store
41
46
  @cache_store ||= Rails.cache
42
47
  end
43
48
 
49
+ # Alias cache_store to cache
50
+ class << self
51
+ alias_method :cache, :cache_store
52
+ end
53
+
44
54
  # Keys are the records being saved.
45
55
  # Values are either the saved record, or nil if the record has been destroyed.
46
56
  def self.pending_records
@@ -37,7 +37,7 @@ module Kasket
37
37
  else
38
38
  key = attribute_value_pairs.map do |attribute, value|
39
39
  column = columns_hash[attribute.to_s]
40
- value = nil if value.blank?
40
+ value = nil if value.blank? && value != false
41
41
  "#{attribute}=#{kasket_quoted_value_for_column(value, column)}"
42
42
  end.join('/')
43
43
 
@@ -91,7 +91,10 @@ module Kasket
91
91
  @kasket_ttl = time
92
92
  end
93
93
 
94
- attr_reader :kasket_ttl
94
+ def kasket_ttl
95
+ @kasket_ttl ||= nil
96
+ @kasket_ttl || Kasket::CONFIGURATION[:default_expires_in]
97
+ end
95
98
 
96
99
  private
97
100
 
@@ -6,8 +6,8 @@ module Kasket
6
6
  # SELECT * FROM `users` WHERE (`users`.`id` = 2) LIMIT 1
7
7
  # 'SELECT * FROM \'posts\' WHERE (\'posts\'.\'id\' = 574019247) '
8
8
 
9
- AND = /\s+AND\s+/i
10
- VALUE = /'?(\d+|\?|(?:(?:[^']|''|\\')*))'?/ # Matches: 123, ?, '123', '12''3'
9
+ AND = /\s+AND\s+/i.freeze
10
+ VALUE = /'?(\d+|\?|(?:(?:[^']|''|\\')*))'?/.freeze # Matches: 123, ?, '123', '12''3'
11
11
 
12
12
  def initialize(model_class)
13
13
  @model_class = model_class
@@ -8,6 +8,7 @@ module Kasket
8
8
  end
9
9
  end
10
10
 
11
+ # *args can be replaced with (sql, *args) once we stop supporting Rails < 5.2
11
12
  def find_by_sql_with_kasket(*args)
12
13
  sql = args[0]
13
14
 
@@ -16,7 +17,11 @@ module Kasket
16
17
  if ActiveRecord::VERSION::MAJOR < 5
17
18
  sql.to_kasket_query(self, args[1])
18
19
  else
19
- sql.to_kasket_query(self, args[1].map(&:value_for_database))
20
+ if ActiveRecord::VERSION::STRING < '5.2'
21
+ sql.to_kasket_query(self, args[1].map(&:value_for_database))
22
+ else
23
+ sql.to_kasket_query(self)
24
+ end
20
25
  end
21
26
  else
22
27
  kasket_parser.parse(sanitize_sql(sql))
@@ -28,11 +33,27 @@ module Kasket
28
33
  find_by_sql_with_kasket_on_id_array(query[:key])
29
34
  else
30
35
  if value = Kasket.cache.read(query[:key])
31
- if value.is_a?(Array)
36
+ # Identified a specific edge case where memcached server returns 0x00 binary protocol response with no data
37
+ # when the node is being rebooted which causes the Dalli memcached client to return a TrueClass object instead of nil
38
+ # see: https://github.com/petergoldstein/dalli/blob/31dabf19d3dd94b348a00a59fe5a7b8fa80ce3ad/lib/dalli/server.rb#L520
39
+ # and: https://github.com/petergoldstein/dalli/issues/390
40
+ #
41
+ # The code in this first condition of TrueClass === true will
42
+ # skip the kasket cache for these specific objects and go directly to SQL for retrieval.
43
+ result_set = if value.is_a?(TrueClass)
44
+ find_by_sql_without_kasket(*args)
45
+ elsif value.is_a?(Array)
32
46
  filter_pending_records(find_by_sql_with_kasket_on_id_array(value))
33
47
  else
34
48
  filter_pending_records(Array.wrap(value).collect { |record| instantiate(record.dup) })
35
49
  end
50
+
51
+ payload = {
52
+ record_count: result_set.length,
53
+ class_name: to_s
54
+ }
55
+
56
+ ActiveSupport::Notifications.instrument('instantiation.active_record', payload) { result_set }
36
57
  else
37
58
  store_in_kasket(query[:key], find_by_sql_without_kasket(*args))
38
59
  end
@@ -76,7 +97,7 @@ module Kasket
76
97
  if records.size == 1
77
98
  records.first.store_in_kasket(key)
78
99
  elsif records.empty?
79
- ActiveRecord::Base.logger.info("[KASKET] would have stored an empty resultset") if ActiveRecord::Base.logger
100
+ ActiveRecord::Base.logger.debug("[KASKET] would have stored an empty resultset") if ActiveRecord::Base.logger
80
101
  elsif records.size <= Kasket::CONFIGURATION[:max_collection_size]
81
102
  if records.all?(&:kasket_cacheable?)
82
103
  instance_keys = records.map(&:store_in_kasket)
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
3
  module RelationMixin
4
+ # binds can be removed when support for Rails < 5 is removed
4
5
  def to_kasket_query(binds = nil)
5
6
  if arel.is_a?(Arel::SelectManager)
6
7
  if ActiveRecord::VERSION::MAJOR < 5
7
8
  arel.to_kasket_query(klass, (binds || bind_values))
9
+ elsif ActiveRecord::VERSION::STRING < '5.2'
10
+ arel.to_kasket_query(klass, (@values[:where].to_h.values + Array(@values[:limit])))
8
11
  else
9
- arel.to_kasket_query(klass, (@values[:where].binds.map(&:value_for_database) + Array(@values[:limit])))
12
+ arel.to_kasket_query(klass)
10
13
  end
11
14
  end
12
15
  rescue TypeError # unsupported object in ast
13
- return nil
16
+ nil
14
17
  end
15
18
  end
16
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
3
  module SelectManagerMixin
4
+ # binds can be removed once we stop supporting Rails < 5.2
4
5
  def to_kasket_query(klass, binds = [])
5
6
  begin
6
7
  query = Kasket::Visitor.new(klass, binds).accept(ast)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
- VERSION = '4.5.1'
3
+ VERSION = '4.9.0'
4
4
  class Version
5
5
  MAJOR = Kasket::VERSION.split('.')[0]
6
6
  MINOR = Kasket::VERSION.split('.')[1]
@@ -3,6 +3,7 @@ require 'arel'
3
3
 
4
4
  module Kasket
5
5
  class Visitor < Arel::Visitors::Visitor
6
+ # binds can be removed once we stop supporting Rails < 5.2
6
7
  def initialize(model_class, binds)
7
8
  @model_class = model_class
8
9
  @binds = binds.dup
@@ -75,6 +76,7 @@ module Kasket
75
76
  def visit_Arel_Nodes_JoinSource(node, *_)
76
77
  return :unsupported if !node.left || node.right.any?
77
78
  return :unsupported unless node.left.is_a?(Arel::Table)
79
+
78
80
  visit(node.left)
79
81
  end
80
82
 
@@ -85,6 +87,7 @@ module Kasket
85
87
  def visit_Arel_Nodes_And(node, *_)
86
88
  attributes = node.children.map { |child| visit(child) }
87
89
  return :unsupported if attributes.include?(:unsupported)
90
+
88
91
  attributes.sort! { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s }
89
92
  { attributes: attributes }
90
93
  end
@@ -112,18 +115,26 @@ module Kasket
112
115
  end
113
116
 
114
117
  def literal(node, *_)
115
- if node == '?'
116
- @binds.shift.last.to_s
118
+ if ActiveRecord::VERSION::STRING < '5.2'
119
+ if node == '?'
120
+ @binds.shift.last.to_s
121
+ else
122
+ node.to_s
123
+ end
117
124
  else
118
125
  node.to_s
119
126
  end
120
127
  end
121
128
 
122
- def visit_Arel_Nodes_BindParam(_x, *_)
129
+ def visit_Arel_Nodes_BindParam(node, *_)
123
130
  if ActiveRecord::VERSION::MAJOR < 5
124
131
  visit(@binds.shift[1])
125
132
  else
126
- visit(@binds.shift)
133
+ if ActiveRecord::VERSION::STRING < '5.2'
134
+ visit(@binds.shift)
135
+ else
136
+ visit(node.value.value) unless node.value.value.nil?
137
+ end
127
138
  end
128
139
  end
129
140
 
@@ -131,11 +142,13 @@ module Kasket
131
142
  node.map {|value| visit(value) }
132
143
  end
133
144
 
134
- def visit_Arel_Nodes_Casted(node, *_)
135
- case node.val
136
- when nil then nil
137
- when String then node.val
138
- else quoted(node.val)
145
+ if ActiveRecord::VERSION::STRING < '5.2'
146
+ def visit_Arel_Nodes_Casted(node, *_)
147
+ case node.val
148
+ when nil then nil
149
+ when String then node.val
150
+ else quoted(node.val)
151
+ end
139
152
  end
140
153
  end
141
154
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kasket
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.1
4
+ version: 4.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mick Staugaard
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-10-18 00:00:00.000000000 Z
12
+ date: 2021-07-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -17,22 +17,22 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '3.2'
20
+ version: '4.2'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '5.2'
23
+ version: '6.1'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
27
27
  requirements:
28
28
  - - ">="
29
29
  - !ruby/object:Gem::Version
30
- version: '3.2'
30
+ version: '4.2'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.2'
33
+ version: '6.1'
34
34
  - !ruby/object:Gem::Dependency
35
- name: rake
35
+ name: bump
36
36
  requirement: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: mocha
63
+ name: minitest
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
@@ -74,7 +74,7 @@ dependencies:
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  - !ruby/object:Gem::Dependency
77
- name: wwtd
77
+ name: minitest-rg
78
78
  requirement: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
@@ -88,7 +88,7 @@ dependencies:
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  - !ruby/object:Gem::Dependency
91
- name: bump
91
+ name: mocha
92
92
  requirement: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
@@ -102,7 +102,7 @@ dependencies:
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  - !ruby/object:Gem::Dependency
105
- name: minitest
105
+ name: rake
106
106
  requirement: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
@@ -116,7 +116,7 @@ dependencies:
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  - !ruby/object:Gem::Dependency
119
- name: minitest-rg
119
+ name: timecop
120
120
  requirement: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="
@@ -130,7 +130,7 @@ dependencies:
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  - !ruby/object:Gem::Dependency
133
- name: timecop
133
+ name: wwtd
134
134
  requirement: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
@@ -173,15 +173,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
173
173
  requirements:
174
174
  - - ">="
175
175
  - !ruby/object:Gem::Version
176
- version: 2.2.0
176
+ version: 2.5.0
177
177
  required_rubygems_version: !ruby/object:Gem::Requirement
178
178
  requirements:
179
179
  - - ">="
180
180
  - !ruby/object:Gem::Version
181
181
  version: '0'
182
182
  requirements: []
183
- rubyforge_project:
184
- rubygems_version: 2.5.1
183
+ rubygems_version: 3.2.22
185
184
  signing_key:
186
185
  specification_version: 4
187
186
  summary: A write back caching layer on active record