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.
- 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
|