ar_cache 1.5.0 → 2.0.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.
@@ -2,66 +2,68 @@
2
2
 
3
3
  module ArCache
4
4
  module Marshal
5
+ delegate :expires_in, :cache_lock?, to: ArCache::Configuration
6
+ delegate :dump_attributes, :load_attributes, to: ArCache
7
+
5
8
  def delete(*ids)
6
9
  return -1 if disabled?
7
10
 
8
- ArCache::Store.delete_multi(ids.map { |id| primary_cache_key(id) })
11
+ ArCache.delete_multi(ids.map { |id| primary_cache_key(id) })
9
12
  end
10
13
 
11
- # WARNING:
12
- # In order to ensure that the written data is consistent with the database,
13
- # only the record from the query can be written.
14
14
  def write(records)
15
15
  return -1 if disabled?
16
16
 
17
- cache_hash = {}
18
- records.each do |record|
19
- attributes = record.attributes_before_type_cast
20
- key = nil
21
-
17
+ records.each do |attributes|
18
+ key = primary_cache_key(attributes[primary_key])
19
+ ArCache.write(key, dump_attributes(attributes), unless_exist: cache_lock?, raw: true, expires_in: expires_in)
22
20
  unique_indexes.each_with_index do |index, i|
23
- if i.zero? # is primary key
24
- key = primary_cache_key(attributes[primary_key])
25
- cache_hash[key] = attributes
26
- else
27
- cache_hash[cache_key(attributes, index)] = key
28
- end
21
+ # The first index is primary key, should skip it.
22
+ ArCache.write(cache_key(attributes, index), key, raw: true, expires_in: expires_in) unless i.zero?
29
23
  end
30
24
  end
31
-
32
- ArCache::Store.write_multi(cache_hash)
33
25
  rescue Encoding::UndefinedConversionError
34
26
  0
35
27
  end
36
28
 
37
- def read(where_clause, select_values, &block)
38
- entries_hash = ArCache::Store.read_multi(where_clause.cache_hash.keys)
39
- where_clause.cache_hash.each_key { |k| where_clause.add_missed_values(k) unless entries_hash.key?(k) }
29
+ def read(where_clause, select_values = nil, &block) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
30
+ entries_hash = ArCache.read_multi(*where_clause.cache_hash.keys, raw: true)
31
+ where_clause.cache_hash.each_key do |k|
32
+ v = entries_hash[k]
33
+
34
+ if v.nil?
35
+ where_clause.add_missed_values(k)
36
+ elsif v == ArCache::PLACEHOLDER
37
+ where_clause.add_missed_values(k)
38
+ where_clause.add_blank_primary_cache_key(k)
39
+ entries_hash.delete(k)
40
+ else
41
+ entries_hash[k] = load_attributes(v)
42
+ end
43
+ end
40
44
 
41
45
  records = []
42
46
 
43
47
  entries_hash.each do |k, entry|
44
- entry = entry.slice(*select_values) if select_values
45
- wrong_key = detect_wrong_key(entry, where_clause.to_h)
48
+ wrong_key = detect_wrong_column(entry, where_clause.to_h)
46
49
 
47
50
  if wrong_key
48
51
  where_clause.add_missed_values(k)
49
- where_clause.add_invalid_keys(k) if column_indexes.include?(wrong_key)
52
+ where_clause.add_invalid_second_cache_key(k) if column_indexes.include?(wrong_key)
50
53
  else
54
+ entry = entry.slice(*select_values) if select_values
51
55
  records << instantiate(where_clause.klass, entry, &block)
52
56
  end
53
57
  end
54
58
 
55
59
  where_clause.delete_invalid_keys
56
-
57
60
  records
58
61
  end
59
62
 
60
- private def detect_wrong_key(entry, where_values_hash)
63
+ private def detect_wrong_column(entry, where_values_hash)
61
64
  where_values_hash.detect do |k, v|
62
- next unless entry.key?(k)
63
-
64
65
  value = entry[k]
66
+
65
67
  if v.is_a?(Array)
66
68
  return k unless v.include?(value)
67
69
  else
@@ -11,12 +11,12 @@ module ArCache
11
11
  true
12
12
  end
13
13
 
14
- def version
15
- -1
14
+ def cache_key_prefix
15
+ ''
16
16
  end
17
17
 
18
- def update_version(...)
19
- -1
18
+ def update_cache
19
+ ''
20
20
  end
21
21
 
22
22
  def primary_key
@@ -2,6 +2,10 @@
2
2
 
3
3
  module ArCache
4
4
  class Query
5
+ @lock_statement = 'FOR SHARE'
6
+ singleton_class.attr_accessor :lock_statement
7
+ delegate :lock_statement, :lock_statement=, to: 'self.class'
8
+
5
9
  attr_reader :relation, :table, :where_clause
6
10
 
7
11
  def initialize(relation)
@@ -12,19 +16,24 @@ module ArCache
12
16
 
13
17
  def exec_queries(&block) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
14
18
  return [] if relation.where_clause.contradiction?
15
- return ArCache.skip { relation.send(:exec_queries, &block) } unless exec_queries_cacheable?
19
+ return ArCache.skip_cache { relation.send(:exec_queries, &block) } unless exec_queries_cacheable?
16
20
 
17
21
  records = table.read(where_clause, @select_values, &block)
18
22
 
19
- missed_relation = if records.empty?
20
- relation
21
- elsif where_clause.missed_hash.any?
22
- relation.rewhere(where_clause.missed_hash)
23
- end
24
-
25
- if missed_relation
26
- records += relation.find_by_sql(missed_relation.arel, &block).tap do |rs|
27
- table.write(rs) if relation.select_values.empty?
23
+ if where_clause.missed_hash.any?
24
+ begin
25
+ missed_relation = relation.rewhere(where_clause.missed_hash).reselect('*').lock(lock_statement)
26
+ missed_relation.arel.singleton_class.attr_accessor(:klass_and_select_values)
27
+ missed_relation.arel.klass_and_select_values = [relation.klass, @select_values]
28
+ missed_relation.connection.transaction do
29
+ records += missed_relation.find_by_sql(missed_relation.arel, &block)
30
+ end
31
+ rescue ::ActiveRecord::StatementInvalid => e
32
+ raise e if relation.connection.class.name != 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
33
+ raise e if lock_statement == 'LOCK IN SHARE MODE'
34
+
35
+ self.lock_statement = 'LOCK IN SHARE MODE'
36
+ retry
28
37
  end
29
38
  end
30
39
 
@@ -39,7 +48,7 @@ module ArCache
39
48
  end
40
49
 
41
50
  def exec_queries_cacheable? # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
42
- return false if relation.klass.ar_cache_table.disabled?
51
+ return false if table.disabled?
43
52
  return false if relation.skip_query_cache_value
44
53
  return false if relation.lock_value
45
54
  return false if relation.distinct_value
@@ -48,7 +57,7 @@ module ArCache
48
57
  return false if relation.left_outer_joins_values.any?
49
58
  return false if relation.offset_value
50
59
  return false if relation.eager_loading?
51
- return false if relation.connection.transaction_manager.changed_table?(table.name)
60
+ return false if relation.connection.transaction_manager.transaction_table?(table.name)
52
61
  return false unless relation.from_clause.empty?
53
62
  return false unless where_clause.cacheable?
54
63
  return false unless select_values_cacheable?
@@ -4,41 +4,45 @@ module ArCache
4
4
  class Table
5
5
  include Marshal
6
6
 
7
- OPTIONS = %i[disabled select_disabled unique_indexes].freeze
8
-
9
7
  singleton_class.attr_reader :all
10
8
 
11
- attr_reader :name, :primary_key, :unique_indexes, :column_indexes, :column_names, :md5, :cache_key_prefix
12
-
13
- delegate :connection, to: ActiveRecord::Base, private: true
14
-
15
9
  @lock = Mutex.new
16
-
17
10
  @all = []
18
11
 
19
12
  def self.new(table_name)
20
13
  @lock.synchronize do
21
- @all.find { |table| table.name == table_name } || super
14
+ table = @all.find { |t| t.name == table_name }
15
+
16
+ unless table
17
+ table = super
18
+ @all << table
19
+ end
20
+
21
+ table
22
22
  end
23
23
  end
24
24
 
25
+ attr_reader :name, :primary_key, :unique_indexes, :column_indexes, :column_names, :identity_cache_key, :short_sha1
26
+
25
27
  def initialize(table_name)
26
28
  @name = table_name
27
- @cache_key_prefix = "arcache:#{@name}:version"
28
- @primary_key = connection.primary_key(@name)
29
- columns = connection.columns(@name)
29
+ @primary_key = ::ActiveRecord::Base.connection.primary_key(@name)
30
+
30
31
  options = ArCache::Configuration.get_table_options(@name)
31
- @unique_indexes = normalize_unique_indexes(options.delete(:unique_indexes), columns).freeze
32
- options.each { |k, v| instance_variable_set("@#{k}", v) }
33
- @disabled = true if @primary_key.nil? # ArCache is depend on primary key implementation.
34
- @column_names = columns.map(&:name).freeze
32
+ @disabled = @primary_key.nil? ? true : options[:disabled] # ArCache can't work if primary key does not exist.
33
+ @select_disabled = options[:select_disabled]
34
+
35
+ columns = ::ActiveRecord::Base.connection.columns(@name)
36
+ @unique_indexes = normalize_unique_indexes(options[:unique_indexes], columns).freeze
35
37
  @column_indexes = @unique_indexes.flatten.uniq.freeze
36
- coder = ArCache::Configuration.coder
37
- @md5 = Digest::MD5.hexdigest("#{coder}-#{@disabled}-#{columns.to_json}")
38
+ @column_names = columns.map(&:name).freeze
38
39
 
39
- ArCache::Record.store(self)
40
+ @identity_cache_key = "ar:cache:#{@name}"
41
+ @short_sha1 = Digest::SHA1.hexdigest("#{@disabled}:#{columns.to_json}").first(7)
40
42
 
41
- self.class.all << self
43
+ # For avoid to skip Arcache read cache, must delete cache when disable Arcache.
44
+ # For keep table's schema is consistent, must delete cache after modified the table.
45
+ ArCache.delete(@identity_cache_key) if disabled? || !cache_key_prefix.start_with?("#{@identity_cache_key}:#{@short_sha1}") # rubocop:disable Layout/LineLength
42
46
  end
43
47
 
44
48
  def disabled?
@@ -49,26 +53,23 @@ module ArCache
49
53
  @select_disabled
50
54
  end
51
55
 
52
- def version
53
- version = ArCache::Store.read(cache_key_prefix)
54
- unless version
55
- version = ArCache::Record.version(self)
56
- ArCache::Store.write(cache_key_prefix, version)
57
- end
56
+ def cache_key_prefix
57
+ return '' if disabled?
58
58
 
59
- version
59
+ ArCache.read(identity_cache_key, raw: true) || update_cache
60
60
  end
61
61
 
62
- def update_version
63
- return -1 if disabled?
62
+ # In order to avoid cache avalanche, we must set cache_key_prefix never expired.
63
+ def update_cache
64
+ return '' if disabled?
64
65
 
65
- version = ArCache::Record.update_version(self)
66
- ArCache::Store.write(cache_key_prefix, version)
67
- version
66
+ key = "#{identity_cache_key}:#{short_sha1}:#{Time.now.to_f}"
67
+ ArCache.write(identity_cache_key, key, raw: true, expires_in: 100.years)
68
+ key
68
69
  end
69
70
 
70
71
  def primary_cache_key(id)
71
- "#{cache_key_prefix}:#{version}:#{primary_key}=#{id}"
72
+ "#{cache_key_prefix}:#{primary_key}=#{id}"
72
73
  end
73
74
 
74
75
  def cache_key(where_values_hash, index, multi_values_key = nil, key_value = nil)
@@ -77,39 +78,32 @@ module ArCache
77
78
  "#{column}=#{value}"
78
79
  end.sort.join('&')
79
80
 
80
- "#{cache_key_prefix}:#{version}:#{where_value}"
81
+ "#{cache_key_prefix}:#{where_value}"
81
82
  end
82
83
 
83
84
  private def normalize_unique_indexes(indexes, columns)
84
- indexes = indexes.empty? ? query_unique_indexes(columns) : custom_unique_indexes(indexes, columns)
85
+ indexes = indexes.empty? ? query_unique_indexes(columns) : validate_unique_indexes(indexes, columns)
85
86
  (indexes - [primary_key]).sort_by(&:size).unshift([primary_key])
86
87
  end
87
88
 
88
- private def query_unique_indexes(columns) # rubocop:disable Metrics/CyclomaticComplexity
89
- connection.indexes(name).filter_map do |index|
89
+ private def query_unique_indexes(columns)
90
+ ::ActiveRecord::Base.connection.indexes(name).filter_map do |index|
90
91
  next unless index.unique
92
+ next unless index.columns.is_a?(Array)
91
93
 
92
94
  index.columns.each do |column|
93
- column = columns.find { |c| c.name == column }
94
- next if column.null
95
- next if column.type == :datetime
95
+ next if columns.none? { |c| c.name == column }
96
96
  end
97
97
 
98
98
  index.columns
99
- rescue NoMethodError # The index.columns maybe isn't Array type
100
- next
101
99
  end
102
100
  end
103
101
 
104
- private def custom_unique_indexes(indexes, columns)
105
- indexes.each do |index|
106
- index.each do |field|
107
- column = columns.find { |c| c.name == field }
108
- raise ArgumentError, "The #{name} table not found #{field.inspect} column" if column.nil?
109
-
110
- if column.type == :datetime
111
- raise ArgumentError, "The #{field.inspect} is datetime type, ArCache do't support datetime type"
112
- end
102
+ private def validate_unique_indexes(indexes, columns)
103
+ indexes.each do |attrs|
104
+ attrs.each do |attr|
105
+ column = columns.find { |c| c.name == attr }
106
+ raise ArgumentError, "The #{name} table not found #{attr} column" if column.nil?
113
107
  end
114
108
  end
115
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArCache
4
- VERSION = '1.5.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -2,13 +2,20 @@
2
2
 
3
3
  module ArCache
4
4
  class WhereClause
5
- attr_reader :klass, :table, :predicates, :invalid_keys
5
+ attr_reader :klass, :table, :predicates
6
6
 
7
7
  def initialize(klass, predicates)
8
8
  @klass = klass
9
9
  @table = klass.ar_cache_table
10
10
  @predicates = predicates
11
- @missed_values = []
11
+ end
12
+
13
+ def missed_values
14
+ @missed_values ||= []
15
+ end
16
+
17
+ def invalid_keys
18
+ @invalid_keys ||= []
12
19
  end
13
20
 
14
21
  def cacheable?
@@ -24,7 +31,7 @@ module ArCache
24
31
  count = 0
25
32
 
26
33
  bool = index.all? do |column|
27
- where_values_hash.key?(column).tap do
34
+ (Thread.current[:ar_cache_reflection] ? where_values_hash.key?(column) : where_values_hash[column]).tap do
28
35
  if where_values_hash[column].is_a?(Array)
29
36
  @multi_values_key = column
30
37
  count += 1
@@ -59,8 +66,8 @@ module ArCache
59
66
  return @cache_hash if primary_key_index?
60
67
 
61
68
  @original_cache_hash = @cache_hash
62
- @cache_hash = ArCache::Store.read_multi(@cache_hash.keys)
63
- @original_cache_hash.each { |k, v| @missed_values << v unless @cache_hash.key?(k) }
69
+ @cache_hash = ArCache.read_multi(*@cache_hash.keys, raw: true)
70
+ @original_cache_hash.each { |k, v| missed_values << v unless @cache_hash.key?(k) }
64
71
  @cache_hash = @cache_hash.invert
65
72
 
66
73
  @cache_hash
@@ -73,26 +80,28 @@ module ArCache
73
80
  end
74
81
 
75
82
  def missed_hash
76
- @missed_hash ||= @missed_values.empty? ? {} : { (@multi_values_key || @index.first) => @missed_values }
83
+ @missed_hash ||= missed_values.empty? ? {} : { (@multi_values_key || @index.first) => missed_values }
77
84
  end
78
85
 
79
86
  def add_missed_values(key)
80
87
  if primary_key_index?
81
- @missed_values << cache_hash[key]
88
+ missed_values << cache_hash[key]
82
89
  else
83
- @missed_values << @original_cache_hash[cache_hash[key]]
90
+ missed_values << @original_cache_hash[cache_hash[key]]
84
91
  end
85
92
  end
86
93
 
87
- def add_invalid_keys(key)
88
- @invalid_keys ||= []
89
- @invalid_keys << key
90
- @invalid_keys << cache_hash[key] unless primary_key_index?
91
- @invalid_keys
94
+ def add_blank_primary_cache_key(key)
95
+ invalid_keys << key
96
+ end
97
+
98
+ def add_invalid_second_cache_key(key)
99
+ # invalid_keys << key # The primary key index is reliable.
100
+ invalid_keys << cache_hash[key] unless primary_key_index?
92
101
  end
93
102
 
94
103
  def delete_invalid_keys
95
- ArCache::Store.delete_multi(@invalid_keys) if @invalid_keys
104
+ ArCache.delete_multi(invalid_keys) if invalid_keys.any?
96
105
  end
97
106
 
98
107
  # This module is based on ActiveRecord::Relation::WhereClause modified
@@ -100,13 +109,10 @@ module ArCache
100
109
  def where_values_hash
101
110
  @where_values_hash ||= equalities(predicates).each_with_object({}) do |node, hash|
102
111
  # Don't support Arel::Nodes::NamedFunction.
103
- # But we don't judge it, because it will raise exception if it is Arel::Nodes::NamedFunction object.
104
112
  next if table.name != node.left.relation.name
105
113
 
106
114
  name = node.left.name.to_s
107
115
  value = extract_node_value(node.right)
108
- next if value.respond_to?(:size) && value.size > ArCache::Configuration.column_length
109
-
110
116
  hash[name] = value
111
117
  end
112
118
  rescue NoMethodError, ActiveModel::RangeError
@@ -133,15 +139,18 @@ module ArCache
133
139
  end
134
140
 
135
141
  private def extract_node_value(node)
136
- value = case node
137
- when Array
138
- node.map { |v| extract_node_value(v) }
139
- when Arel::Nodes::BindParam
140
- node.value.value_for_database # Maybe raise ActiveModel::RangeError
141
- when Arel::Nodes::Casted, Arel::Nodes::Quoted
142
- node.value_for_database # Maybe raise ActiveModel::RangeError
143
- end
142
+ case node
143
+ when Array
144
+ node.map { |v| extract_node_value(v) }
145
+ when Arel::Nodes::BindParam
146
+ value_for_database(node.value)
147
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
148
+ value_for_database(node)
149
+ end
150
+ end
144
151
 
152
+ private def value_for_database(node)
153
+ value = node.value_for_database # Maybe raise ActiveModel::RangeError
145
154
  value.is_a?(Date) ? value.to_s : value
146
155
  end
147
156
  end