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.
data/ar_cache.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  spec.add_runtime_dependency 'activerecord', '>= 6.1', '< 7'
31
+ spec.add_runtime_dependency 'oj', '>= 3', '< 4'
31
32
 
32
33
  # For more information and examples about making a new gem, checkout our
33
34
  # guide at: https://bundler.io/guides/creating_gem.html
data/lib/ar_cache.rb CHANGED
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'active_support/cache'
4
4
  require 'active_record'
5
+ require 'oj'
5
6
 
6
7
  require 'ar_cache/version'
7
8
  require 'ar_cache/configuration'
8
- require 'ar_cache/record'
9
- require 'ar_cache/store'
10
9
  require 'ar_cache/marshal'
11
10
  require 'ar_cache/table'
12
11
  require 'ar_cache/mock_table'
@@ -18,31 +17,65 @@ require 'ar_cache/active_record'
18
17
  require_relative './generators/ar_cache/install_generator' if defined?(Rails)
19
18
 
20
19
  module ArCache
21
- PRELOADER = ::ActiveRecord::Associations::Preloader.new
20
+ PLACEHOLDER = ''
21
+
22
+ @cache_reflection = {}
22
23
 
23
24
  class << self
24
- delegate :configure, to: Configuration
25
+ delegate :configure, :memcached?, :redis?, :cache_store, to: ArCache::Configuration
26
+ delegate :read, :read_multi, :write, :write_multi, :delete, :delete_multi, :exist?, :clear, to: :cache_store
27
+
28
+ def skip_cache?
29
+ Thread.current[:ar_cache_skip_cache]
30
+ end
31
+
32
+ def skip_cache
33
+ return yield if skip_cache?
34
+
35
+ begin
36
+ Thread.current[:ar_cache_skip_cache] = true
37
+ yield
38
+ ensure
39
+ Thread.current[:ar_cache_skip_cache] = false
40
+ end
41
+ end
25
42
 
26
- def skip?
27
- Thread.current[:ar_cache_skip]
43
+ def skip_expire?
44
+ Thread.current[:ar_cache_skip_expire]
28
45
  end
29
46
 
30
- def skip
31
- Thread.current[:ar_cache_skip] = true
32
- yield
33
- ensure
34
- Thread.current[:ar_cache_skip] = false
47
+ def skip_expire
48
+ return yield if skip_expire?
49
+
50
+ begin
51
+ Thread.current[:ar_cache_skip_expire] = true
52
+ yield
53
+ ensure
54
+ Thread.current[:ar_cache_skip_expire] = false
55
+ end
56
+ end
57
+
58
+ def cache_reflection?(reflection)
59
+ @cache_reflection.fetch(reflection) do
60
+ Thread.current[:ar_cache_reflection] = true
61
+ @cache_reflection[reflection] = yield
62
+ ensure
63
+ Thread.current[:ar_cache_reflection] = false
64
+ end
35
65
  end
36
66
 
37
- def expire?
38
- Thread.current[:ar_cache_expire]
67
+ def dump_attributes(attributes)
68
+ memcached? || redis? ? Oj.dump(attributes) : attributes
39
69
  end
40
70
 
41
- def expire
42
- Thread.current[:ar_cache_expire] = true
43
- yield
44
- ensure
45
- Thread.current[:ar_cache_expire] = false
71
+ def load_attributes(attributes)
72
+ memcached? || redis? ? Oj.load(attributes) : attributes
73
+ end
74
+
75
+ def lock_key(key)
76
+ ArCache.write(key, PLACEHOLDER, raw: true, expires_in: 1.hour)
46
77
  end
47
78
  end
48
79
  end
80
+
81
+ require_relative './generators/ar_cache/templates/configuration'
@@ -5,7 +5,7 @@ module ArCache
5
5
  module Associations
6
6
  module Association
7
7
  def reload(...)
8
- loaded? ? ArCache.skip { super } : super
8
+ loaded? ? ArCache.skip_cache { super } : super
9
9
  end
10
10
  end
11
11
  end
@@ -4,19 +4,21 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Associations
6
6
  module HasOneThroughAssociation
7
- private def find_target # rubocop:disable Metrics/CyclomaticComplexity
8
- return super if ArCache.skip?
9
- return super if reflection.klass.ar_cache_table.disabled?
10
- return super if reflection.through_reflection.klass.ar_cache_table.disabled?
7
+ PRELOADER = ::ActiveRecord::Associations::Preloader.new
8
+
9
+ private def find_target
10
+ return super if ArCache.skip_cache?
11
+ return super unless ArCache.cache_reflection?(reflection) do
12
+ ArCache::Query.new(owner.association(through_reflection.name).scope).exec_queries_cacheable? &&
13
+ ArCache::Query.new(source_reflection.active_record.new.association(source_reflection.name).scope).exec_queries_cacheable? # rubocop:disable Layout/LineLength
14
+ end
11
15
 
12
16
  if (owner.strict_loading? || reflection.strict_loading?) && owner.validation_context.nil?
13
- Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
17
+ ::ActiveRecord::Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
14
18
  end
15
19
 
16
- ArCache::PRELOADER.preload(owner, reflection.name)
17
- owner.send(reflection.name)
18
- rescue StandardError
19
- super
20
+ PRELOADER.preload(owner, reflection.name)
21
+ target
20
22
  end
21
23
  end
22
24
  end
@@ -5,12 +5,8 @@ module ArCache
5
5
  module Associations
6
6
  module SingularAssociation
7
7
  private def skip_statement_cache?(...)
8
- return super if ArCache.skip?
9
-
10
- # Polymorphic associations do not support computing the class, so can't judge ArCache status.
11
- # But SingularAssociation query usually can hit the unique index, so here return true directly.
12
- return true if is_a?(::ActiveRecord::Associations::BelongsToPolymorphicAssociation)
13
- return true unless reflection.klass.ar_cache_table.disabled?
8
+ return super if ArCache.skip_cache?
9
+ return true if ArCache.cache_reflection?(reflection) { ArCache::Query.new(scope).exec_queries_cacheable? }
14
10
 
15
11
  super
16
12
  end
@@ -4,18 +4,23 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
6
  module DatabaseStatements
7
+ def select_all(arel, ...)
8
+ result = super
9
+ klass, select_values = arel.try(:klass_and_select_values)
10
+ return result if klass.nil?
11
+
12
+ klass.ar_cache_table.write(result.to_a)
13
+
14
+ if select_values
15
+ result.to_a.each { |r| r.slice!(*select_values) }
16
+ elsif klass.ignored_columns.any?
17
+ result.to_a.each { |r| r.except!(*klass.ignored_columns) }
18
+ end
19
+
20
+ result
21
+ end
22
+
7
23
  # def insert(arel, ...)
8
- # super.tap do
9
- # if arel.is_a?(String)
10
- # sql = arel.downcase
11
- # ArCache::Table.all.each do |table|
12
- # current_transaction.add_changed_table(table.name) if sql.include?(table.name)
13
- # end
14
- # else # is Arel::InsertManager
15
- # klass = arel.ast.relation.instance_variable_get(:@klass)
16
- # current_transaction.add_changed_table(klass.table_name)
17
- # end
18
- # end
19
24
  # end
20
25
  # alias create insert
21
26
 
@@ -46,24 +51,24 @@ module ArCache
46
51
  end
47
52
 
48
53
  private def update_ar_cache_by_arel(arel)
49
- return if ArCache.expire?
54
+ return if ArCache.skip_expire?
50
55
 
51
56
  arel_table = arel.ast.relation.is_a?(Arel::Table) ? arel.ast.relation : arel.ast.relation.left
52
57
  klass = arel_table.instance_variable_get(:@klass)
53
- current_transaction.update_ar_cache_table(klass.ar_cache_table) unless klass.ar_cache_table.disabled?
58
+ current_transaction.update_ar_cache_table(klass.ar_cache_table)
54
59
  end
55
60
 
56
61
  private def update_ar_cache_by_sql(sql)
57
62
  sql = sql.downcase
58
63
 
59
64
  ArCache::Table.all.each do |table|
60
- current_transaction.update_ar_cache_table(table) if !table.disabled? && sql.include?(table.name)
65
+ current_transaction.update_ar_cache_table(table) if sql.include?(table.name)
61
66
  end
62
67
  end
63
68
 
64
69
  private def update_ar_cache_by_table(table_name)
65
70
  ArCache::Table.all.each do |table|
66
- current_transaction.update_ar_cache_table(table) if !table.disabled? && table_name.casecmp?(table.name)
71
+ break current_transaction.update_ar_cache_table(table) if table_name == table.name
67
72
  end
68
73
  end
69
74
  end
@@ -4,75 +4,80 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
6
  module NullTransaction
7
- def delete_ar_cache_keys(keys, delay: false) # rubocop:disable Lint/UnusedMethodArgument
8
- ArCache::Store.delete_multi(keys)
7
+ def delete_ar_cache_primary_keys(keys, table)
8
+ handle_ar_cache_primary_keys(keys) unless table.disabled?
9
9
  end
10
10
 
11
- def update_ar_cache_table(table, delay: false) # rubocop:disable Lint/UnusedMethodArgument
12
- table.update_version
11
+ def update_ar_cache_table(table)
12
+ table.update_cache
13
13
  end
14
14
 
15
- def add_changed_table(...); end
15
+ def handle_ar_cache_primary_keys(keys)
16
+ if ArCache::Configuration.cache_lock?
17
+ keys.each { |k| ArCache.lock_key(k) }
18
+ else
19
+ ArCache.delete_multi(keys)
20
+ end
21
+ end
16
22
  end
17
23
 
18
24
  module Transaction
19
25
  include NullTransaction
20
26
 
27
+ attr_reader :ar_cache_primary_keys, :ar_cache_tables
28
+
21
29
  def initialize(...)
22
30
  super
23
- @ar_cache_keys = []
31
+ @ar_cache_primary_keys = []
24
32
  @ar_cache_tables = []
25
33
  end
26
34
 
27
- def delete_ar_cache_keys(keys, delay: false)
28
- super if !delay && read_uncommitted?
29
- @ar_cache_keys.push(*keys)
30
- end
31
-
32
- def update_ar_cache_table(table, delay: false)
33
- add_changed_table(table.name) unless delay
35
+ def delete_ar_cache_primary_keys(keys, table)
36
+ connection.transaction_manager.add_transaction_table(table.name)
37
+ return if table.disabled?
34
38
 
35
- super if !delay && read_uncommitted?
36
- @ar_cache_tables.push(table)
39
+ super if read_uncommitted?
40
+ ar_cache_primary_keys.push(*keys)
37
41
  end
38
42
 
39
- def add_changed_table(table_name)
40
- connection.transaction_manager.add_changed_table(table_name)
43
+ def update_ar_cache_table(table)
44
+ connection.transaction_manager.add_transaction_table(table.name)
45
+ return if table.disabled?
46
+
47
+ super if read_uncommitted?
48
+ ar_cache_tables.push(table)
41
49
  end
42
50
 
43
- # FIXME: Cache update and transaction commit may cause dirty reads during this period!
51
+ # FIXME: The cache is removed after transaction commited, so dirty read may occur.
44
52
  def commit
45
53
  super
46
54
  ensure
47
55
  if @run_commit_callbacks
48
- @ar_cache_tables.uniq(&:name).each(&:update_version) if @ar_cache_tables.any?
49
- ArCache::Store.delete_multi(@ar_cache_keys.uniq) if @ar_cache_keys.any?
56
+ handle_ar_cache_primary_keys(ar_cache_primary_keys.uniq) if ar_cache_primary_keys.any?
57
+ ar_cache_tables.uniq(&:name).each(&:update_cache) if ar_cache_tables.any?
50
58
  else
51
- transaction = connection.current_transaction
52
- @ar_cache_tables.each { |table| transaction.update_ar_cache_table(table, delay: true) }
53
- transaction.delete_ar_cache_keys(@ar_cache_keys, delay: true)
59
+ connection.current_transaction.ar_cache_tables.push(*ar_cache_tables)
60
+ connection.current_transaction.ar_cache_primary_keys.push(*ar_cache_primary_keys)
54
61
  end
55
62
  end
56
63
 
57
64
  def read_uncommitted?
58
- ArCache::Configuration.read_uncommitted ||
59
- isolation_level == :read_uncommitted ||
60
- !connection.transaction_manager.fully_joinable?
65
+ isolation_level == :read_uncommitted || !connection.transaction_manager.fully_joinable?
61
66
  end
62
67
  end
63
68
 
64
69
  module TransactionManager
65
70
  def initialize(...)
66
71
  super
67
- @changed_tables = {}
72
+ @transaction_tables = {}
68
73
  end
69
74
 
70
- def add_changed_table(table_name)
71
- @changed_tables[table_name] = true if fully_joinable?
75
+ def add_transaction_table(table_name)
76
+ @transaction_tables[table_name] = true if fully_joinable?
72
77
  end
73
78
 
74
- def changed_table?(table_name)
75
- @changed_tables.key?(table_name)
79
+ def transaction_table?(table_name)
80
+ @transaction_tables.key?(table_name)
76
81
  end
77
82
 
78
83
  def fully_joinable?
@@ -82,7 +87,7 @@ module ArCache
82
87
  def within_new_transaction(...)
83
88
  super
84
89
  ensure
85
- @changed_tables = {} if @stack.count(&:joinable?).zero?
90
+ @transaction_tables.clear if @stack.count(&:joinable?).zero?
86
91
  end
87
92
  end
88
93
  end
@@ -4,14 +4,15 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Core
6
6
  module ClassMethods
7
- # The #find use statement cache execute querying first, so need force skip.
7
+ # The #find and #find_by use ActiveRecord::StatementCache to execute querying first.
8
+ # For ArCache, we need force skip ActiveRecord::StatementCache.
9
+
8
10
  def find(...)
9
- ArCache.skip? || ar_cache_table.disabled? ? super : all.find(...)
11
+ ArCache.skip_cache? || ar_cache_table.disabled? ? super : all.find(...)
10
12
  end
11
13
 
12
- # The #find_by use statement cache execute querying first, so need force skip.
13
14
  def find_by(...)
14
- ArCache.skip? || ar_cache_table.disabled? ? super : all.find_by(...)
15
+ ArCache.skip_cache? || ar_cache_table.disabled? ? super : all.find_by(...)
15
16
  end
16
17
  end
17
18
  end
@@ -5,10 +5,7 @@ module ArCache
5
5
  module InsertAll
6
6
  def execute
7
7
  super.tap do
8
- if on_duplicate == :update
9
- connection.current_transaction.update_ar_cache_table(model.ar_cache_table)
10
- connection.current_transaction.add_changed_table(model.table_name)
11
- end
8
+ connection.current_transaction.update_ar_cache_table(model.ar_cache_table) if on_duplicate == :update
12
9
  end
13
10
  end
14
11
  end
@@ -9,13 +9,7 @@ module ArCache
9
9
  end
10
10
 
11
11
  def ar_cache_table
12
- @ar_cache_table ||= begin
13
- if abstract_class? || self == ArCache::Record
14
- ArCache::MockTable
15
- else
16
- ArCache::Table.new(table_name)
17
- end
18
- end
12
+ @ar_cache_table ||= abstract_class? ? ArCache::MockTable : ArCache::Table.new(table_name)
19
13
  end
20
14
  end
21
15
 
@@ -5,14 +5,14 @@ module ArCache
5
5
  module Persistence
6
6
  module ClassMethods
7
7
  def _update_record(_, constraints)
8
- ArCache.expire do
8
+ ArCache.skip_expire do
9
9
  delete_ar_cache_key(constraints[@primary_key])
10
10
  super
11
11
  end
12
12
  end
13
13
 
14
14
  def _delete_record(constraints)
15
- ArCache.expire do
15
+ ArCache.skip_expire do
16
16
  delete_ar_cache_key(constraints[@primary_key])
17
17
  super
18
18
  end
@@ -20,13 +20,12 @@ module ArCache
20
20
 
21
21
  private def delete_ar_cache_key(id)
22
22
  key = ar_cache_table.primary_cache_key(id)
23
- connection.current_transaction.delete_ar_cache_keys([key])
24
- connection.current_transaction.add_changed_table(table_name)
23
+ connection.current_transaction.delete_ar_cache_primary_keys([key], ar_cache_table)
25
24
  end
26
25
  end
27
26
 
28
27
  def reload(...)
29
- ArCache.skip { super }
28
+ ArCache.skip_cache { super }
30
29
  end
31
30
  end
32
31
  end
@@ -4,22 +4,22 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Relation
6
6
  def reload
7
- loaded? ? ArCache.skip { super } : super
7
+ loaded? ? ArCache.skip_cache { super } : super
8
8
  end
9
9
 
10
10
  def explain
11
- ArCache.skip { super }
11
+ ArCache.skip_cache { super }
12
12
  end
13
13
 
14
14
  def update_all(...)
15
- ArCache.expire { delete_ar_cache_keys ? super : 0 }
15
+ ArCache.skip_expire { delete_ar_cache_primary_keys ? super : 0 }
16
16
  end
17
17
 
18
18
  def delete_all
19
- ArCache.expire { delete_ar_cache_keys ? super : 0 }
19
+ ArCache.skip_expire { delete_ar_cache_primary_keys ? super : 0 }
20
20
  end
21
21
 
22
- private def delete_ar_cache_keys
22
+ private def delete_ar_cache_primary_keys
23
23
  return true if klass.ar_cache_table.disabled?
24
24
 
25
25
  where_clause = ArCache::WhereClause.new(klass, arel.constraints)
@@ -31,13 +31,12 @@ module ArCache
31
31
 
32
32
  return false if keys.empty?
33
33
 
34
- @klass.connection.current_transaction.delete_ar_cache_keys(keys)
35
- @klass.connection.current_transaction.add_changed_table(@klass.table_name)
34
+ @klass.connection.current_transaction.delete_ar_cache_primary_keys(keys, @klass.ar_cache_table)
36
35
  true
37
36
  end
38
37
 
39
38
  private def exec_queries(&block)
40
- ArCache.skip? ? super : ArCache::Query.new(self).exec_queries(&block).freeze
39
+ ArCache.skip_cache? ? super : ArCache::Query.new(self).exec_queries(&block).freeze
41
40
  end
42
41
  end
43
42
  end