ar_cache 1.2.0 → 2.1.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,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