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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +0 -10
- data/Gemfile.common +2 -0
- data/Gemfile.lock +89 -80
- data/README.md +20 -6
- data/README.zh-CN.md +165 -0
- data/ar_cache.gemspec +1 -0
- data/lib/ar_cache.rb +42 -15
- data/lib/ar_cache/active_record/associations/has_one_through_association.rb +10 -7
- data/lib/ar_cache/active_record/associations/singular_association.rb +2 -4
- 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 +4 -5
- 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 +3 -4
- data/lib/ar_cache/active_record/relation.rb +7 -14
- data/lib/ar_cache/configuration.rb +34 -42
- data/lib/ar_cache/marshal.rb +28 -26
- data/lib/ar_cache/mock_table.rb +4 -12
- data/lib/ar_cache/query.rb +25 -15
- data/lib/ar_cache/table.rb +45 -60
- 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 +20 -25
- metadata +23 -5
- data/lib/ar_cache/record.rb +0 -54
- 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,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'yaml'
|
4
|
-
|
5
3
|
module ArCache
|
6
4
|
class Configuration
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
14
|
+
def cache_lock?
|
15
|
+
@cache_lock
|
16
|
+
end
|
21
17
|
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
def redis?
|
19
|
+
@redis
|
20
|
+
end
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
22
|
+
def memcached?
|
23
|
+
@memcached
|
29
24
|
end
|
30
25
|
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
35
|
+
@cache_store = cache_store
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
def tables_options=(options)
|
39
|
+
@tables_options = options.deep_symbolize_keys
|
40
|
+
end
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
data/lib/ar_cache/marshal.rb
CHANGED
@@ -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
|
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
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)
|
data/lib/ar_cache/mock_table.rb
CHANGED
@@ -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
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def version
|
23
|
-
-1
|
14
|
+
def cache_key_prefix
|
15
|
+
''
|
24
16
|
end
|
25
17
|
|
26
|
-
def
|
27
|
-
|
18
|
+
def update_cache
|
19
|
+
''
|
28
20
|
end
|
29
21
|
|
30
22
|
def primary_key
|
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,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.
|
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
|
-
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
|
-
|
43
|
-
return false if
|
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.
|
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
|
data/lib/ar_cache/table.rb
CHANGED
@@ -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 { |
|
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?
|
46
|
-
@disabled
|
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
|
58
|
-
|
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
|
-
|
59
|
+
ArCache.read(identity_cache_key, raw: true) || update_cache
|
69
60
|
end
|
70
61
|
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
ArCache
|
76
|
-
|
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}:#{
|
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}:#{
|
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) :
|
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)
|
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
|
-
|
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
|
114
|
-
indexes.each do |
|
115
|
-
|
116
|
-
column = columns.find { |c| c.name ==
|
117
|
-
raise ArgumentError, "The #{name} table not found #{
|
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
|