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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +0 -41
- data/Gemfile.common +2 -0
- data/Gemfile.lock +81 -77
- data/README.md +4 -4
- data/README.zh-CN.md +165 -0
- data/ar_cache.gemspec +1 -0
- data/lib/ar_cache.rb +31 -16
- data/lib/ar_cache/active_record/associations/association.rb +1 -1
- data/lib/ar_cache/active_record/associations/has_one_through_association.rb +2 -3
- data/lib/ar_cache/active_record/associations/singular_association.rb +1 -1
- 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 +34 -41
- data/lib/ar_cache/marshal.rb +29 -27
- data/lib/ar_cache/mock_table.rb +4 -4
- data/lib/ar_cache/query.rb +21 -12
- data/lib/ar_cache/table.rb +44 -50
- data/lib/ar_cache/version.rb +1 -1
- data/lib/ar_cache/where_clause.rb +34 -25
- data/lib/generators/ar_cache/install_generator.rb +0 -7
- data/lib/generators/ar_cache/templates/configuration.rb +20 -24
- 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
data/lib/ar_cache/marshal.rb
CHANGED
@@ -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
|
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
|
-
|
18
|
-
|
19
|
-
attributes
|
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
|
-
|
24
|
-
|
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
|
39
|
-
where_clause.cache_hash.each_key
|
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
|
-
|
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.
|
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
|
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
|
data/lib/ar_cache/mock_table.rb
CHANGED
data/lib/ar_cache/query.rb
CHANGED
@@ -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.
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
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.
|
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?
|
data/lib/ar_cache/table.rb
CHANGED
@@ -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 { |
|
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
|
-
@
|
28
|
-
|
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
|
-
@
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
@md5 = Digest::MD5.hexdigest("#{coder}-#{@disabled}-#{columns.to_json}")
|
38
|
+
@column_names = columns.map(&:name).freeze
|
38
39
|
|
39
|
-
|
40
|
+
@identity_cache_key = "ar:cache:#{@name}"
|
41
|
+
@short_sha1 = Digest::SHA1.hexdigest("#{@disabled}:#{columns.to_json}").first(7)
|
40
42
|
|
41
|
-
|
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
|
53
|
-
|
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
|
-
|
59
|
+
ArCache.read(identity_cache_key, raw: true) || update_cache
|
60
60
|
end
|
61
61
|
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
ArCache
|
67
|
-
|
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}:#{
|
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}:#{
|
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) :
|
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)
|
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
|
-
|
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
|
105
|
-
indexes.each do |
|
106
|
-
|
107
|
-
column = columns.find { |c| c.name ==
|
108
|
-
raise ArgumentError, "The #{name} table not found #{
|
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
|
data/lib/ar_cache/version.rb
CHANGED
@@ -2,13 +2,20 @@
|
|
2
2
|
|
3
3
|
module ArCache
|
4
4
|
class WhereClause
|
5
|
-
attr_reader :klass, :table, :predicates
|
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
|
-
|
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
|
63
|
-
@original_cache_hash.each { |k, v|
|
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 ||=
|
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
|
-
|
88
|
+
missed_values << cache_hash[key]
|
82
89
|
else
|
83
|
-
|
90
|
+
missed_values << @original_cache_hash[cache_hash[key]]
|
84
91
|
end
|
85
92
|
end
|
86
93
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|