ar_cache 1.1.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.
@@ -1,59 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
-
5
3
  module ArCache
6
4
  class Configuration
7
- singleton_class.attr_accessor :disabled, :select_disabled, :expires_in, :read_uncommitted, :index_column_max_size
8
- singleton_class.attr_reader :cache_store, :tables_options, :coder
9
-
10
- def self.configure
11
- block_given? ? yield(self) : self
12
- end
5
+ class << self
6
+ attr_writer :cache_lock
7
+ attr_reader :cache_store, :tables_options
8
+ attr_accessor :disabled, :select_disabled, :expires_in
13
9
 
14
- def self.cache_store=(cache_store)
15
- unless cache_store.is_a?(ActiveSupport::Cache::Store)
16
- raise ArgumentError, 'The cache_store must be an ActiveSupport::Cache::Store object'
10
+ def configure
11
+ block_given? ? yield(self) : self
17
12
  end
18
13
 
19
- @cache_store = cache_store
20
- end
14
+ def cache_lock?
15
+ @cache_lock
16
+ end
21
17
 
22
- def self.tables_options=(options)
23
- options.each do |name, hash|
24
- raise ArgumentError, "The #{name.inspect} must be converted to Symbol type" unless name.is_a?(Symbol)
18
+ def redis?
19
+ @redis
20
+ end
25
21
 
26
- hash.each_key do |k|
27
- raise ArgumentError, "The #{k.inspect} must be converted to Symbol type" unless k.is_a?(Symbol)
28
- end
22
+ def memcached?
23
+ @memcached
29
24
  end
30
25
 
31
- @tables_options = options
32
- end
26
+ def cache_store=(cache_store)
27
+ if !cache_store.is_a?(ActiveSupport::Cache::Store) # rubocop:disable Style/GuardClause
28
+ raise ArgumentError, 'The cache_store must be an ActiveSupport::Cache::Store object'
29
+ elsif cache_store.class.name == 'ActiveSupport::Cache::RedisCacheStore' # rubocop:disable Style/ClassEqualityComparison
30
+ @redis = true
31
+ elsif cache_store.class.name == 'ActiveSupport::Cache::MemCacheStore' # rubocop:disable Style/ClassEqualityComparison
32
+ @memcached = true
33
+ end
33
34
 
34
- def self.coder=(coder)
35
- raise ArgumentError, 'The coder only support use YAML or JSON' unless [::YAML, ::JSON].include?(coder)
35
+ @cache_store = cache_store
36
+ end
36
37
 
37
- @coder = coder
38
- end
38
+ def tables_options=(options)
39
+ @tables_options = options.deep_symbolize_keys
40
+ end
39
41
 
40
- def self.get_table_options(name)
41
- options = tables_options[name.to_sym] || {}
42
- options[:disabled] = disabled unless options.key?(:disabled)
43
- options[:select_disabled] = select_disabled unless options.key?(:select_disabled)
44
- options[:ignored_columns] = Array(options[:ignored_columns]).map(&:to_s)
45
- options[:unique_indexes] = Array(options[:unique_indexes]).map { |index| Array(index).map(&:to_s).uniq }.uniq
46
- options
42
+ def get_table_options(name)
43
+ options = tables_options[name.to_sym] || {}
44
+ options[:disabled] = disabled unless options.key?(:disabled)
45
+ options[:select_disabled] = select_disabled unless options.key?(:select_disabled)
46
+ options[:unique_indexes] = Array(options[:unique_indexes]).map { |index| Array(index).map(&:to_s).uniq }.uniq
47
+ options
48
+ end
47
49
  end
48
-
49
- # The set default values
50
- @cache_store = defined?(Rails) ? Rails.cache : ActiveSupport::Cache::MemoryStore.new
51
- @tables_options = {}
52
- @coder = ::YAML
53
- @disabled = false
54
- @select_disabled = true
55
- @expires_in = 604_800 # 1 week
56
- @read_uncommitted = false
57
- @index_column_max_size = 64
58
50
  end
59
51
  end
@@ -2,65 +2,67 @@
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
65
  value = entry[k]
63
- next if value.nil?
64
66
 
65
67
  if v.is_a?(Array)
66
68
  return k unless v.include?(value)
@@ -7,24 +7,16 @@ module ArCache
7
7
  true
8
8
  end
9
9
 
10
- def enabled?
11
- false
12
- end
13
-
14
10
  def select_disabled?
15
11
  true
16
12
  end
17
13
 
18
- def select_enabled?
19
- false
20
- end
21
-
22
- def version
23
- -1
14
+ def cache_key_prefix
15
+ ''
24
16
  end
25
17
 
26
- def update_version(...)
27
- -1
18
+ def update_cache
19
+ ''
28
20
  end
29
21
 
30
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,21 +16,25 @@ 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 relation.skip_ar_cache.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
- if table.ignored_columns.any? && relation.select_values.any?
27
- missed_relation = missed_relation.reselect(table.column_names)
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
- records += relation.find_by_sql(missed_relation.arel, &block).tap { |rs| table.write(rs) }
30
38
  end
31
39
 
32
40
  records_order(records)
@@ -39,16 +47,17 @@ module ArCache
39
47
  records
40
48
  end
41
49
 
42
- private def exec_queries_cacheable? # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
43
- return false if relation.klass.ar_cache_table.disabled?
50
+ def exec_queries_cacheable? # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
51
+ return false if table.disabled?
44
52
  return false if relation.skip_query_cache_value
45
53
  return false if relation.lock_value
54
+ return false if relation.distinct_value
46
55
  return false if relation.group_values.any?
47
56
  return false if relation.joins_values.any?
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?
@@ -66,7 +75,7 @@ module ArCache
66
75
  (@select_values - table.column_names).empty?
67
76
  end
68
77
 
69
- private def order_values_cacheable?
78
+ private def order_values_cacheable? # rubocop:disable Metrics/CyclomaticComplexity
70
79
  return true if where_clause.single?
71
80
 
72
81
  size = relation.order_values.size
@@ -81,6 +90,7 @@ module ArCache
81
90
  when String
82
91
  @order_name, @order_desc = first_order_value.downcase.split
83
92
  return false unless table.column_names.include?(@order_name)
93
+ return false unless ['asc', 'desc', nil].include?(@order_desc)
84
94
 
85
95
  @order_desc = @order_desc == 'desc'
86
96
  else
@@ -4,80 +4,72 @@ module ArCache
4
4
  class Table
5
5
  include Marshal
6
6
 
7
- OPTIONS = %i[disabled select_disabled unique_indexes ignored_columns].freeze
8
-
9
7
  singleton_class.attr_reader :all
10
8
 
11
- attr_reader :name, :primary_key, :unique_indexes, :column_indexes, :column_names, :md5,
12
- :ignored_columns, :cache_key_prefix
13
-
14
- delegate :connection, to: ActiveRecord::Base, private: true
15
-
16
9
  @lock = Mutex.new
17
-
18
10
  @all = []
19
11
 
20
12
  def self.new(table_name)
21
13
  @lock.synchronize do
22
- @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
23
22
  end
24
23
  end
25
24
 
25
+ attr_reader :name, :primary_key, :unique_indexes, :column_indexes, :column_names, :identity_cache_key, :short_sha1
26
+
26
27
  def initialize(table_name)
27
28
  @name = table_name
28
- @cache_key_prefix = "arcache:#{@name}:version"
29
- @primary_key = connection.primary_key(@name)
30
- columns = connection.columns(@name)
29
+ @primary_key = ::ActiveRecord::Base.connection.primary_key(@name)
30
+
31
31
  options = ArCache::Configuration.get_table_options(@name)
32
- @unique_indexes = normalize_unique_indexes(options.delete(:unique_indexes), columns).freeze
33
- options.each { |k, v| instance_variable_set("@#{k}", v) }
34
- @disabled = true if @primary_key.nil? # ArCache is depend on primary key implementation.
35
- @column_names = (columns.map(&:name) - @ignored_columns).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
36
37
  @column_indexes = @unique_indexes.flatten.uniq.freeze
37
- coder = ArCache::Configuration.coder
38
- @md5 = Digest::MD5.hexdigest("#{coder}-#{@disabled}-#{columns.to_json}-#{@ignored_columns.to_json}")
38
+ @column_names = columns.map(&:name).freeze
39
39
 
40
- ArCache::Record.store(self)
40
+ @identity_cache_key = "ar:cache:#{@name}"
41
+ @short_sha1 = Digest::SHA1.hexdigest("#{@disabled}:#{columns.to_json}").first(7)
41
42
 
42
- 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
43
46
  end
44
47
 
45
48
  def disabled?
46
- @disabled || ArCache.skip_cache?
47
- end
48
-
49
- def enabled?
50
- !disabled?
49
+ @disabled
51
50
  end
52
51
 
53
52
  def select_disabled?
54
53
  @select_disabled
55
54
  end
56
55
 
57
- def select_enabled?
58
- !@select_disabled
59
- end
60
-
61
- def version
62
- version = ArCache::Store.read(cache_key_prefix)
63
- unless version
64
- version = ArCache::Record.version(self)
65
- ArCache::Store.write(cache_key_prefix, version)
66
- end
56
+ def cache_key_prefix
57
+ return '' if disabled?
67
58
 
68
- version
59
+ ArCache.read(identity_cache_key, raw: true) || update_cache
69
60
  end
70
61
 
71
- def update_version
72
- 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?
73
65
 
74
- version = ArCache::Record.update_version(self)
75
- ArCache::Store.write(cache_key_prefix, version)
76
- 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
77
69
  end
78
70
 
79
71
  def primary_cache_key(id)
80
- "#{cache_key_prefix}:#{version}:#{primary_key}=#{id}"
72
+ "#{cache_key_prefix}:#{primary_key}=#{id}"
81
73
  end
82
74
 
83
75
  def cache_key(where_values_hash, index, multi_values_key = nil, key_value = nil)
@@ -86,39 +78,32 @@ module ArCache
86
78
  "#{column}=#{value}"
87
79
  end.sort.join('&')
88
80
 
89
- "#{cache_key_prefix}:#{version}:#{where_value}"
81
+ "#{cache_key_prefix}:#{where_value}"
90
82
  end
91
83
 
92
84
  private def normalize_unique_indexes(indexes, columns)
93
- 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)
94
86
  (indexes - [primary_key]).sort_by(&:size).unshift([primary_key])
95
87
  end
96
88
 
97
- private def query_unique_indexes(columns) # rubocop:disable Metrics/CyclomaticComplexity
98
- connection.indexes(name).filter_map do |index|
89
+ private def query_unique_indexes(columns)
90
+ ::ActiveRecord::Base.connection.indexes(name).filter_map do |index|
99
91
  next unless index.unique
92
+ next unless index.columns.is_a?(Array)
100
93
 
101
94
  index.columns.each do |column|
102
- column = columns.find { |c| c.name == column }
103
- next if column.null
104
- next if column.type == :datetime
95
+ next if columns.none? { |c| c.name == column }
105
96
  end
106
97
 
107
98
  index.columns
108
- rescue NoMethodError # The index.columns maybe isn't Array type
109
- next
110
99
  end
111
100
  end
112
101
 
113
- private def custom_unique_indexes(indexes)
114
- indexes.each do |index|
115
- index.each do |column|
116
- column = columns.find { |c| c.name == column }
117
- raise ArgumentError, "The #{name} table not found #{column.inspect} column" if column.nil?
118
-
119
- if column.type == :datetime
120
- raise ArgumentError, "The #{column.inspect} is datetime type, ArCache do't support datetime type"
121
- 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?
122
107
  end
123
108
  end
124
109
  end