ar_cache 1.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 +7 -0
- data/.github/workflows/main.yml +68 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +56 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +7 -0
- data/Gemfile.common +17 -0
- data/Gemfile.lock +192 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/Rakefile +28 -0
- data/ar_cache.gemspec +34 -0
- data/bin/activerecord-test +20 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/gemfiles/rails-6-1 +3 -0
- data/gemfiles/rails-edge +3 -0
- data/lib/ar_cache.rb +22 -0
- data/lib/ar_cache/active_record.rb +36 -0
- data/lib/ar_cache/active_record/associations/has_one_through_association.rb +39 -0
- data/lib/ar_cache/active_record/associations/singular_association.rb +18 -0
- data/lib/ar_cache/active_record/connection_adapters/abstract/database_statements.rb +76 -0
- data/lib/ar_cache/active_record/connection_adapters/abstract/transaction.rb +90 -0
- data/lib/ar_cache/active_record/core.rb +21 -0
- data/lib/ar_cache/active_record/insert_all.rb +17 -0
- data/lib/ar_cache/active_record/model_schema.rb +27 -0
- data/lib/ar_cache/active_record/persistence.rb +23 -0
- data/lib/ar_cache/active_record/relation.rb +48 -0
- data/lib/ar_cache/configuration.rb +59 -0
- data/lib/ar_cache/log_subscriber.rb +8 -0
- data/lib/ar_cache/marshal.rb +81 -0
- data/lib/ar_cache/mock_table.rb +55 -0
- data/lib/ar_cache/query.rb +95 -0
- data/lib/ar_cache/record.rb +54 -0
- data/lib/ar_cache/store.rb +38 -0
- data/lib/ar_cache/table.rb +126 -0
- data/lib/ar_cache/version.rb +5 -0
- data/lib/ar_cache/where_clause.rb +151 -0
- data/lib/generators/ar_cache/install_generator.rb +22 -0
- data/lib/generators/ar_cache/templates/configuration.rb +34 -0
- data/lib/generators/ar_cache/templates/migrate/create_ar_cache_records.rb.tt +16 -0
- metadata +107 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArCache
|
|
4
|
+
class Record < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
|
|
5
|
+
self.table_name = 'ar_cache_records'
|
|
6
|
+
|
|
7
|
+
default_scope { skip_ar_cache }
|
|
8
|
+
|
|
9
|
+
def self.get(table_name)
|
|
10
|
+
find_by(table_name: table_name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.version(table)
|
|
14
|
+
(get(table.name) || store(table)).version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.update_version(table)
|
|
18
|
+
record = get(table.name)
|
|
19
|
+
return store(table).version unless record
|
|
20
|
+
|
|
21
|
+
record.update_version
|
|
22
|
+
record.version
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.store(table)
|
|
26
|
+
record = get(table.name) || new(table_name: table.name)
|
|
27
|
+
record.store(table)
|
|
28
|
+
record
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def store(table)
|
|
32
|
+
with_optimistic_retry do
|
|
33
|
+
self.version += 1 unless table_md5 == table.md5
|
|
34
|
+
self.table_md5 = table.md5
|
|
35
|
+
|
|
36
|
+
save! if changed?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_version
|
|
41
|
+
with_optimistic_retry do
|
|
42
|
+
self.version += 1
|
|
43
|
+
save!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def with_optimistic_retry
|
|
48
|
+
yield
|
|
49
|
+
rescue ::ActiveRecord::StaleObjectError
|
|
50
|
+
reload
|
|
51
|
+
retry
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArCache
|
|
4
|
+
class Store
|
|
5
|
+
@options = { raw: true, expires_in: ArCache::Configuration.expires_in }.freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
delegate :delete, :delete_multi, :clear, :exist?, to: 'ArCache::Configuration.cache_store'
|
|
9
|
+
|
|
10
|
+
def write(name, value)
|
|
11
|
+
ArCache::Configuration.cache_store.write(name, dump(value), @options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def write_multi(hash)
|
|
15
|
+
hash.each { |k, v| hash[k] = dump(v) }
|
|
16
|
+
ArCache::Configuration.cache_store.write_multi(hash, @options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read(name)
|
|
20
|
+
load(ArCache::Configuration.cache_store.read(name, @options))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read_multi(names)
|
|
24
|
+
entries = ArCache::Configuration.cache_store.read_multi(*names, @options)
|
|
25
|
+
entries.each { |k, v| entries[k] = load(v) }
|
|
26
|
+
entries
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private def dump(value)
|
|
30
|
+
ArCache::Configuration.coder.dump(value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def load(value)
|
|
34
|
+
ArCache::Configuration.coder.load(value) if value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArCache
|
|
4
|
+
class Table
|
|
5
|
+
include Marshal
|
|
6
|
+
|
|
7
|
+
OPTIONS = %i[disabled select_disabled unique_indexes ignored_columns].freeze
|
|
8
|
+
|
|
9
|
+
singleton_class.attr_reader :all
|
|
10
|
+
|
|
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
|
+
@lock = Mutex.new
|
|
17
|
+
|
|
18
|
+
@all = []
|
|
19
|
+
|
|
20
|
+
def self.new(table_name)
|
|
21
|
+
@lock.synchronize do
|
|
22
|
+
@all.find { |table| table.name == table_name } || super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(table_name)
|
|
27
|
+
@name = table_name
|
|
28
|
+
@cache_key_prefix = "arcache:#{@name}:version"
|
|
29
|
+
@primary_key = connection.primary_key(@name)
|
|
30
|
+
columns = connection.columns(@name)
|
|
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
|
|
36
|
+
@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}")
|
|
39
|
+
|
|
40
|
+
ArCache::Record.store(self)
|
|
41
|
+
|
|
42
|
+
self.class.all << self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def disabled?
|
|
46
|
+
@disabled
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enabled?
|
|
50
|
+
!disabled?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def select_disabled?
|
|
54
|
+
@select_disabled
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def select_enabled?
|
|
58
|
+
!@select_disabled
|
|
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
|
|
67
|
+
|
|
68
|
+
version
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_version
|
|
72
|
+
return -1 if disabled?
|
|
73
|
+
|
|
74
|
+
version = ArCache::Record.update_version(self)
|
|
75
|
+
ArCache::Store.write(cache_key_prefix, version)
|
|
76
|
+
version
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def primary_cache_key(id)
|
|
80
|
+
"#{cache_key_prefix}:#{version}:#{primary_key}=#{id}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cache_key(where_values_hash, index, multi_values_key = nil, key_value = nil)
|
|
84
|
+
where_value = index.map do |column|
|
|
85
|
+
value = column == multi_values_key ? key_value : where_values_hash[column]
|
|
86
|
+
"#{column}=#{value}"
|
|
87
|
+
end.sort.join('&')
|
|
88
|
+
|
|
89
|
+
"#{cache_key_prefix}:#{version}:#{where_value}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private def normalize_unique_indexes(indexes, columns)
|
|
93
|
+
indexes = indexes.empty? ? query_unique_indexes(columns) : custom_unique_indexes(indexes, columns)
|
|
94
|
+
(indexes - [primary_key]).sort_by(&:size).unshift([primary_key])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def query_unique_indexes(columns) # rubocop:disable Metrics/CyclomaticComplexity
|
|
98
|
+
connection.indexes(name).filter_map do |index|
|
|
99
|
+
next unless index.unique
|
|
100
|
+
|
|
101
|
+
index.columns.each do |column|
|
|
102
|
+
column = columns.find { |c| c.name == column }
|
|
103
|
+
next if column.null
|
|
104
|
+
next if column.type == :datetime
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
index.columns
|
|
108
|
+
rescue NoMethodError # The index.columns maybe isn't Array type
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def custom_unique_indexes(indexes)
|
|
114
|
+
indexes.each do |index|
|
|
115
|
+
index.each do |column|
|
|
116
|
+
column = columns.find { |c| c.name == column }
|
|
117
|
+
raise ArgumentError, "The #{name} table not found #{column.inspect} column" if column.nil?
|
|
118
|
+
|
|
119
|
+
if column.type == :datetime
|
|
120
|
+
raise ArgumentError, "The #{column.inspect} is datetime type, ArCache do't support datetime type"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArCache
|
|
4
|
+
class WhereClause
|
|
5
|
+
attr_reader :klass, :table, :predicates
|
|
6
|
+
|
|
7
|
+
def initialize(klass, predicates)
|
|
8
|
+
@klass = klass
|
|
9
|
+
@table = klass.ar_cache_table
|
|
10
|
+
@predicates = predicates
|
|
11
|
+
@missed_values = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cacheable?
|
|
15
|
+
return @cacheable if defined?(@cacheable)
|
|
16
|
+
|
|
17
|
+
@cacheable = predicates.any? && where_values_hash.length == predicates.length && hit_unique_index?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def hit_unique_index?
|
|
21
|
+
table.unique_indexes.each do |index|
|
|
22
|
+
@index = index
|
|
23
|
+
@multi_values_key = nil
|
|
24
|
+
count = 0
|
|
25
|
+
|
|
26
|
+
bool = index.all? do |column|
|
|
27
|
+
where_values_hash[column].tap do |value|
|
|
28
|
+
if value.is_a?(Array)
|
|
29
|
+
@multi_values_key = column
|
|
30
|
+
count += 1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return true if bool && count < 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def single?
|
|
42
|
+
@multi_values_key.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def primary_key_index?
|
|
46
|
+
(@multi_values_key || @index.first) == table.primary_key
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cache_hash
|
|
50
|
+
return @cache_hash if defined?(@cache_hash)
|
|
51
|
+
|
|
52
|
+
@cache_hash = {}
|
|
53
|
+
multi_values_key = @multi_values_key || @index.first
|
|
54
|
+
|
|
55
|
+
Array(where_values_hash[multi_values_key]).each do |v|
|
|
56
|
+
@cache_hash[table.cache_key(where_values_hash, @index, multi_values_key, v)] = v
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return @cache_hash if primary_key_index?
|
|
60
|
+
|
|
61
|
+
@original_cache_hash = @cache_hash
|
|
62
|
+
@cache_hash = ArCache::Store.read_multi(@cache_hash.keys)
|
|
63
|
+
@original_cache_hash.each { |k, v| @missed_values << v unless @cache_hash.key?(k) }
|
|
64
|
+
@cache_hash = @cache_hash.invert
|
|
65
|
+
|
|
66
|
+
@cache_hash
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cache_keys
|
|
70
|
+
keys = cache_hash.keys
|
|
71
|
+
keys += cache_hash.values unless primary_key_index?
|
|
72
|
+
|
|
73
|
+
keys
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def missed_hash
|
|
77
|
+
@missed_hash ||= @missed_values.empty? ? {} : { (@multi_values_key || @index.first) => @missed_values }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_missed_values(key)
|
|
81
|
+
if primary_key_index?
|
|
82
|
+
@missed_values << cache_hash[key]
|
|
83
|
+
else
|
|
84
|
+
@missed_values << @original_cache_hash[cache_hash[key]]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_invalid_keys(key)
|
|
89
|
+
@invalid_keys ||= []
|
|
90
|
+
@invalid_keys << key
|
|
91
|
+
@invalid_keys << cache_hash[key] unless primary_key_index?
|
|
92
|
+
@invalid_keys
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def delete_invalid_keys
|
|
96
|
+
ArCache::Store.delete_multi(@invalid_keys) if @invalid_keys
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# This module is based on ActiveRecord::Relation::WhereClause modified
|
|
100
|
+
module Raw
|
|
101
|
+
def where_values_hash
|
|
102
|
+
@where_values_hash ||= equalities(predicates).each_with_object({}) do |node, hash|
|
|
103
|
+
# Don't support Arel::Nodes::NamedFunction.
|
|
104
|
+
# But we don't judge it, because it will raise exception if it is Arel::Nodes::NamedFunction object.
|
|
105
|
+
next if table.name != node.left.relation.name
|
|
106
|
+
|
|
107
|
+
name = node.left.name.to_s
|
|
108
|
+
value = extract_node_value(node.right)
|
|
109
|
+
next if value.respond_to?(:size) && value.size > ArCache::Configuration.index_column_max_size
|
|
110
|
+
|
|
111
|
+
hash[name] = value
|
|
112
|
+
end
|
|
113
|
+
rescue NoMethodError, ActiveModel::RangeError
|
|
114
|
+
@where_values_hash = {}
|
|
115
|
+
end
|
|
116
|
+
alias to_h where_values_hash
|
|
117
|
+
|
|
118
|
+
private def equalities(predicates)
|
|
119
|
+
equalities = []
|
|
120
|
+
|
|
121
|
+
predicates.each do |node|
|
|
122
|
+
if equality_node?(node)
|
|
123
|
+
equalities << node
|
|
124
|
+
elsif node.is_a?(Arel::Nodes::And)
|
|
125
|
+
equalities.concat equalities(node.children)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
equalities
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def equality_node?(node)
|
|
133
|
+
!node.is_a?(String) && node.equality?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private def extract_node_value(node)
|
|
137
|
+
value = case node
|
|
138
|
+
when Array
|
|
139
|
+
node.map { |v| extract_node_value(v) }
|
|
140
|
+
when Arel::Nodes::BindParam
|
|
141
|
+
node.value.value_for_database # Maybe raise ActiveModel::RangeError
|
|
142
|
+
when Arel::Nodes::Casted, Arel::Nodes::Quoted
|
|
143
|
+
node.value_for_database # Maybe raise ActiveModel::RangeError
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
value.is_a?(Date) ? value.to_s : value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
include Raw
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
require 'rails/generators/active_record/migration'
|
|
6
|
+
|
|
7
|
+
module ArCache
|
|
8
|
+
module Generators
|
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
|
10
|
+
include Rails::Generators::Migration
|
|
11
|
+
include ::ActiveRecord::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
|
+
|
|
15
|
+
def copy_initializer_file
|
|
16
|
+
copy_file 'configuration.rb', 'config/initializers/ar_cache.rb'
|
|
17
|
+
|
|
18
|
+
migration_template 'migrate/create_ar_cache_records.rb', 'db/migrate/create_ar_cache_records.rb'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# For more information, please see: https://github.com/OuYangJinTing/ar_cache/README.md
|
|
4
|
+
ArCache.configure do |config|
|
|
5
|
+
# WARNING: The should uncomment only when your database default isolation level is "READ UNCOMMITTED"!
|
|
6
|
+
# config.read_uncommitted = true # defaul false
|
|
7
|
+
|
|
8
|
+
# config.cache_store = ActiveSupport::Cache::Store # default Rails.cache || ActiveSupport::Cache::MemoryStore.new
|
|
9
|
+
|
|
10
|
+
# Cache key automatic expiration time.
|
|
11
|
+
# config.expires_in = Numeric # default 1 week
|
|
12
|
+
|
|
13
|
+
# Serialize and deserialize cached data.
|
|
14
|
+
# config.coder = [YAML|JSON] # default YAML
|
|
15
|
+
|
|
16
|
+
# Support the maximum length of index column value.
|
|
17
|
+
# config.index_column_max_size = Integer # default 64
|
|
18
|
+
|
|
19
|
+
# ArCache switch.
|
|
20
|
+
# config.disabled = Boolean # default false
|
|
21
|
+
|
|
22
|
+
# Whether to support selecct columns query
|
|
23
|
+
# config.select_disabled = Boolean # default true
|
|
24
|
+
|
|
25
|
+
# config.tables_options = {
|
|
26
|
+
# table_name: {
|
|
27
|
+
# disabled: Boolean,
|
|
28
|
+
# select_disabled: Boolean,
|
|
29
|
+
# unique_indexes: Array # eg: [:id, [:name, :statue]], The default is the unique index column of the table.
|
|
30
|
+
# ignored_columns: Array # eg: [:created_at, :updated_at], defaule [].
|
|
31
|
+
# },
|
|
32
|
+
# ...
|
|
33
|
+
# }
|
|
34
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateArCacheMonitors < ActiveRecord::Migration[<%= ActiveRecord::VERSION::MAJOR %>.<%= ActiveRecord::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :ar_cache_records do |t|
|
|
6
|
+
t.string :table_name, null: false
|
|
7
|
+
t.string :table_md5, null: false, limit: 32, default: '0' * 32
|
|
8
|
+
t.integer :version, null: false, default: 0
|
|
9
|
+
t.integer :lock_version, null: false, default: 0
|
|
10
|
+
|
|
11
|
+
t.timestamps null: false
|
|
12
|
+
|
|
13
|
+
t.index :table_name, unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|