ar_cache 1.2.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,59 +1,64 @@
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
5
+ class << self
6
+ attr_writer :cache_lock, :lock_statement
7
+ attr_reader :cache_store, :tables_options
8
+ attr_accessor :disabled, :select_disabled, :expires_in
9
9
 
10
- def self.configure
11
- block_given? ? yield(self) : self
12
- end
10
+ def configure
11
+ block_given? ? yield(self) : self
12
+ end
13
13
 
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'
14
+ def cache_lock?
15
+ @cache_lock
17
16
  end
18
17
 
19
- @cache_store = cache_store
20
- end
18
+ def redis?
19
+ @redis
20
+ end
21
21
 
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)
22
+ def memcached?
23
+ @memcached
24
+ end
25
25
 
26
- hash.each_key do |k|
27
- raise ArgumentError, "The #{k.inspect} must be converted to Symbol type" unless k.is_a?(Symbol)
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
28
33
  end
29
- end
30
34
 
31
- @tables_options = options
32
- end
35
+ @cache_store = cache_store
36
+ end
33
37
 
34
- def self.coder=(coder)
35
- raise ArgumentError, 'The coder only support use YAML or JSON' unless [::YAML, ::JSON].include?(coder)
38
+ def tables_options=(options)
39
+ @tables_options = options.deep_symbolize_keys
40
+ end
36
41
 
37
- @coder = coder
38
- end
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
39
49
 
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
50
+ def lock_statement
51
+ @lock_statement ||= case ::ActiveRecord::Base.connection.class.name
52
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
53
+ 'FOR SHARE'
54
+ when 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
55
+ 'LOCK IN SHARE MODE'
56
+ when 'ActiveRecord::ConnectionAdapters::SQLite3Adapter'
57
+ raise "SQLite3 don't support lock statement, please use cache lock."
58
+ else
59
+ raise "Arcache can't identify database, please defined lock statement or use cache lock"
60
+ end
61
+ end
47
62
  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
63
  end
59
64
  end
@@ -2,65 +2,75 @@
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
- def write(records)
14
+ def write(records) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
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
17
+ records.each do |attributes|
18
+ key = primary_cache_key(attributes[primary_key])
19
+ stringify_attributes = dump_attributes(attributes)
20
+ bool = ArCache.write(key, stringify_attributes, unless_exist: cache_lock?, raw: true, expires_in: expires_in)
21
+ if cache_lock? && !bool
22
+ value = ArCache.read(key, raw: true)
23
+ next if value == ArCache::PLACEHOLDER
24
+ next ArCache.lock_key(key) if value != stringify_attributes
25
+ end
21
26
 
22
27
  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
28
+ # The first index is primary key, should skip it.
29
+ ArCache.write(cache_key(attributes, index), key, raw: true, expires_in: expires_in) unless i.zero?
29
30
  end
30
31
  end
31
-
32
- ArCache::Store.write_multi(cache_hash)
33
32
  rescue Encoding::UndefinedConversionError
34
33
  0
35
34
  end
36
35
 
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) }
36
+ def read(where_clause, select_values = nil, &block) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
37
+ entries_hash = ArCache.read_multi(*where_clause.cache_hash.keys, raw: true)
38
+ where_clause.cache_hash.each_key do |k|
39
+ v = entries_hash[k]
40
+
41
+ case v
42
+ when nil
43
+ where_clause.add_missed_values(k)
44
+ when ArCache::PLACEHOLDER
45
+ where_clause.add_missed_values(k)
46
+ where_clause.add_blank_primary_cache_key(k)
47
+ entries_hash.delete(k)
48
+ else
49
+ entries_hash[k] = load_attributes(v)
50
+ end
51
+ end
40
52
 
41
53
  records = []
42
54
 
43
55
  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)
56
+ wrong_key = detect_wrong_column(entry, where_clause.to_h)
46
57
 
47
58
  if wrong_key
48
59
  where_clause.add_missed_values(k)
49
- where_clause.add_invalid_keys(k) if column_indexes.include?(wrong_key)
60
+ where_clause.add_invalid_second_cache_key(k) if column_indexes.include?(wrong_key)
50
61
  else
62
+ entry = entry.slice(*select_values) if select_values
51
63
  records << instantiate(where_clause.klass, entry, &block)
52
64
  end
53
65
  end
54
66
 
55
67
  where_clause.delete_invalid_keys
56
-
57
68
  records
58
69
  end
59
70
 
60
- private def detect_wrong_key(entry, where_values_hash)
71
+ private def detect_wrong_column(entry, where_values_hash)
61
72
  where_values_hash.detect do |k, v|
62
73
  value = entry[k]
63
- next if value.nil?
64
74
 
65
75
  if v.is_a?(Array)
66
76
  return k unless v.include?(value)
@@ -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,8 @@
2
2
 
3
3
  module ArCache
4
4
  class Query
5
+ delegate :lock_statement, :cache_lock?, to: ArCache::Configuration
6
+
5
7
  attr_reader :relation, :table, :where_clause
6
8
 
7
9
  def initialize(relation)
@@ -12,21 +14,22 @@ module ArCache
12
14
 
13
15
  def exec_queries(&block) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
14
16
  return [] if relation.where_clause.contradiction?
15
- return ArCache.skip { relation.send(:exec_queries, &block) } unless exec_queries_cacheable?
17
+ return ArCache.skip_cache { relation.send(:exec_queries, &block) } unless exec_queries_cacheable?
16
18
 
17
19
  records = table.read(where_clause, @select_values, &block)
18
20
 
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)
21
+ if where_clause.missed_hash.any?
22
+ missed_relation = relation.rewhere(where_clause.missed_hash).reselect('*')
23
+ missed_relation = missed_relation.lock(lock_statement) unless cache_lock?
24
+ missed_relation.arel.singleton_class.attr_accessor(:klass_and_select_values)
25
+ missed_relation.arel.klass_and_select_values = [relation.klass, @select_values]
26
+ if cache_lock?
27
+ records += missed_relation.find_by_sql(missed_relation.arel, &block)
28
+ else
29
+ missed_relation.connection.transaction do
30
+ records += missed_relation.find_by_sql(missed_relation.arel, &block)
31
+ end
28
32
  end
29
- records += relation.find_by_sql(missed_relation.arel, &block).tap { |rs| table.write(rs) }
30
33
  end
31
34
 
32
35
  records_order(records)
@@ -39,16 +42,17 @@ module ArCache
39
42
  records
40
43
  end
41
44
 
42
- private def exec_queries_cacheable? # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
43
- return false if relation.klass.ar_cache_table.disabled?
45
+ def exec_queries_cacheable? # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
46
+ return false if table.disabled?
44
47
  return false if relation.skip_query_cache_value
45
48
  return false if relation.lock_value
49
+ return false if relation.distinct_value
46
50
  return false if relation.group_values.any?
47
51
  return false if relation.joins_values.any?
48
52
  return false if relation.left_outer_joins_values.any?
49
53
  return false if relation.offset_value
50
54
  return false if relation.eager_loading?
51
- return false if relation.connection.transaction_manager.changed_table?(table.name)
55
+ return false if relation.connection.transaction_manager.transaction_table?(table.name)
52
56
  return false unless relation.from_clause.empty?
53
57
  return false unless where_clause.cacheable?
54
58
  return false unless select_values_cacheable?
@@ -66,7 +70,7 @@ module ArCache
66
70
  (@select_values - table.column_names).empty?
67
71
  end
68
72
 
69
- private def order_values_cacheable?
73
+ private def order_values_cacheable? # rubocop:disable Metrics/CyclomaticComplexity
70
74
  return true if where_clause.single?
71
75
 
72
76
  size = relation.order_values.size
@@ -81,6 +85,7 @@ module ArCache
81
85
  when String
82
86
  @order_name, @order_desc = first_order_value.downcase.split
83
87
  return false unless table.column_names.include?(@order_name)
88
+ return false unless ['asc', 'desc', nil].include?(@order_desc)
84
89
 
85
90
  @order_desc = @order_desc == 'desc'
86
91
  else
@@ -4,42 +4,45 @@ 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?
@@ -50,26 +53,23 @@ module ArCache
50
53
  @select_disabled
51
54
  end
52
55
 
53
- def version
54
- version = ArCache::Store.read(cache_key_prefix)
55
- unless version
56
- version = ArCache::Record.version(self)
57
- ArCache::Store.write(cache_key_prefix, version)
58
- end
56
+ def cache_key_prefix
57
+ return '' if disabled?
59
58
 
60
- version
59
+ ArCache.read(identity_cache_key, raw: true) || update_cache
61
60
  end
62
61
 
63
- def update_version
64
- 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?
65
65
 
66
- version = ArCache::Record.update_version(self)
67
- ArCache::Store.write(cache_key_prefix, version)
68
- version
66
+ key = "#{identity_cache_key}:#{short_sha1}:#{Time.now.to_f}"
67
+ ArCache.write(identity_cache_key, key, raw: true, expires_in: 20.years)
68
+ key
69
69
  end
70
70
 
71
71
  def primary_cache_key(id)
72
- "#{cache_key_prefix}:#{version}:#{primary_key}=#{id}"
72
+ "#{cache_key_prefix}:#{primary_key}=#{id}"
73
73
  end
74
74
 
75
75
  def cache_key(where_values_hash, index, multi_values_key = nil, key_value = nil)
@@ -78,39 +78,32 @@ module ArCache
78
78
  "#{column}=#{value}"
79
79
  end.sort.join('&')
80
80
 
81
- "#{cache_key_prefix}:#{version}:#{where_value}"
81
+ "#{cache_key_prefix}:#{where_value}"
82
82
  end
83
83
 
84
84
  private def normalize_unique_indexes(indexes, columns)
85
- 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)
86
86
  (indexes - [primary_key]).sort_by(&:size).unshift([primary_key])
87
87
  end
88
88
 
89
- private def query_unique_indexes(columns) # rubocop:disable Metrics/CyclomaticComplexity
90
- connection.indexes(name).filter_map do |index|
89
+ private def query_unique_indexes(columns)
90
+ ::ActiveRecord::Base.connection.indexes(name).filter_map do |index|
91
91
  next unless index.unique
92
+ next unless index.columns.is_a?(Array)
92
93
 
93
94
  index.columns.each do |column|
94
- column = columns.find { |c| c.name == column }
95
- next if column.null
96
- next if column.type == :datetime
95
+ next if columns.none? { |c| c.name == column }
97
96
  end
98
97
 
99
98
  index.columns
100
- rescue NoMethodError # The index.columns maybe isn't Array type
101
- next
102
99
  end
103
100
  end
104
101
 
105
- private def custom_unique_indexes(indexes)
106
- indexes.each do |index|
107
- index.each do |column|
108
- column = columns.find { |c| c.name == column }
109
- raise ArgumentError, "The #{name} table not found #{column.inspect} column" if column.nil?
110
-
111
- if column.type == :datetime
112
- raise ArgumentError, "The #{column.inspect} is datetime type, ArCache do't support datetime type"
113
- 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?
114
107
  end
115
108
  end
116
109
  end