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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -16
- data/Gemfile.common +2 -0
- data/Gemfile.lock +89 -80
- data/README.md +4 -4
- data/README.zh-CN.md +165 -0
- data/ar_cache.gemspec +1 -0
- data/lib/ar_cache.rb +51 -18
- data/lib/ar_cache/active_record/associations/association.rb +1 -1
- data/lib/ar_cache/active_record/associations/has_one_through_association.rb +11 -9
- data/lib/ar_cache/active_record/associations/singular_association.rb +2 -6
- data/lib/ar_cache/active_record/connection_adapters/abstract/database_statements.rb +20 -15
- data/lib/ar_cache/active_record/connection_adapters/abstract/transaction.rb +37 -32
- data/lib/ar_cache/active_record/core.rb +5 -4
- data/lib/ar_cache/active_record/insert_all.rb +1 -4
- data/lib/ar_cache/active_record/model_schema.rb +1 -7
- data/lib/ar_cache/active_record/persistence.rb +4 -5
- data/lib/ar_cache/active_record/relation.rb +7 -8
- data/lib/ar_cache/configuration.rb +46 -41
- data/lib/ar_cache/marshal.rb +36 -26
- data/lib/ar_cache/mock_table.rb +4 -4
- data/lib/ar_cache/query.rb +20 -15
- data/lib/ar_cache/table.rb +44 -51
- data/lib/ar_cache/version.rb +1 -1
- data/lib/ar_cache/where_clause.rb +35 -26
- data/lib/generators/ar_cache/install_generator.rb +0 -7
- data/lib/generators/ar_cache/templates/configuration.rb +29 -29
- metadata +23 -5
- data/lib/ar_cache/record.rb +0 -52
- data/lib/ar_cache/store.rb +0 -38
- data/lib/generators/ar_cache/templates/migrate/create_ar_cache_records.rb.tt +0 -16
@@ -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
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def configure
|
11
|
+
block_given? ? yield(self) : self
|
12
|
+
end
|
13
13
|
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
18
|
+
def redis?
|
19
|
+
@redis
|
20
|
+
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def memcached?
|
23
|
+
@memcached
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
|
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
|
-
|
32
|
-
|
35
|
+
@cache_store = cache_store
|
36
|
+
end
|
33
37
|
|
34
|
-
|
35
|
-
|
38
|
+
def tables_options=(options)
|
39
|
+
@tables_options = options.deep_symbolize_keys
|
40
|
+
end
|
36
41
|
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
data/lib/ar_cache/marshal.rb
CHANGED
@@ -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
|
11
|
+
ArCache.delete_multi(ids.map { |id| primary_cache_key(id) })
|
9
12
|
end
|
10
13
|
|
11
|
-
#
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
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
|
39
|
-
where_clause.cache_hash.each_key
|
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
|
-
|
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.
|
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
|
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)
|
data/lib/ar_cache/mock_table.rb
CHANGED
data/lib/ar_cache/query.rb
CHANGED
@@ -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.
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
missed_relation
|
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
|
-
|
43
|
-
return false if
|
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.
|
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
|
data/lib/ar_cache/table.rb
CHANGED
@@ -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 { |
|
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
|
-
@
|
29
|
-
|
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
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
38
|
-
@md5 = Digest::MD5.hexdigest("#{coder}-#{@disabled}-#{columns.to_json}-#{@ignored_columns.to_json}")
|
38
|
+
@column_names = columns.map(&:name).freeze
|
39
39
|
|
40
|
-
|
40
|
+
@identity_cache_key = "ar:cache:#{@name}"
|
41
|
+
@short_sha1 = Digest::SHA1.hexdigest("#{@disabled}:#{columns.to_json}").first(7)
|
41
42
|
|
42
|
-
|
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
|
54
|
-
|
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
|
-
|
59
|
+
ArCache.read(identity_cache_key, raw: true) || update_cache
|
61
60
|
end
|
62
61
|
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
ArCache
|
68
|
-
|
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}:#{
|
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}:#{
|
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) :
|
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)
|
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
|
-
|
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
|
106
|
-
indexes.each do |
|
107
|
-
|
108
|
-
column = columns.find { |c| c.name ==
|
109
|
-
raise ArgumentError, "The #{name} table not found #{
|
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
|