activerecord-batch_touching 1.0.pre.beta2 → 1.0.pre.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd952639925581e1a9402a4e1542ca8be34e826ee23754742381daea8465f59d
4
- data.tar.gz: 061f01b40c568b24b9c383558c63f87bf29e27ded41192ae30d5afc0106294af
3
+ metadata.gz: 06aa6d889559fed96fed448ffb100324496a79d1e97e147607067f1a3275ab92
4
+ data.tar.gz: 5a737bed02b7812f0feba746bf2ea19bfece091959bdaa25a465dae634b1c38c
5
5
  SHA512:
6
- metadata.gz: 25f107a0f99f1e951a5ec5c83115aab53c90676ffbc9652e170566fbd933fa61a18e1649a0a885a29e935fd08aa27dcc26450632c86c69230792cc48f5e6c799
7
- data.tar.gz: a231c2bfc1aefd7d56b537c41d5a0f887660a4b4d842e1f066a6e65057402471fe8be1fc456eecb92c901f4e8b45bd13308a6c2a84c7c1f1b8343a7e14463108
6
+ metadata.gz: b0add569ae81b020e75b5168b00e550d4f69a67cf8e24e5adbb6f45808339ac0d126f69a59bbd0c8620dd073fc762a9cc98283362f9534f3e20197b95da9dc33
7
+ data.tar.gz: 2d23e326e341927fa8196c566b3101f8850f56632ab93bdb9bf6506e87a451f9c6141801e40c2c6f2743e7a9cfacc535c555161d06003467e1bd29fa59c0f673
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  # = Active Record Batch Touching
3
5
  module BatchTouching
4
-
5
6
  # Tracking of the touch state. This class has no class-level data, so you can
6
7
  # store per-thread instances in thread-local variables.
7
8
  class State # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Activerecord
2
4
  module BatchTouching
3
5
  VERSION = "1.0"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord/batch_touching/version"
2
4
  require "activerecord/batch_touching/state"
3
5
 
@@ -55,34 +57,58 @@ module ActiveRecord
55
57
  end
56
58
 
57
59
  class << self
58
- def states
59
- Thread.current[:batch_touching_states] ||= []
60
+ # Disable batch touching globally
61
+ def disable!
62
+ @disabled = true
60
63
  end
61
64
 
62
- def current_state
63
- states.last
65
+ # Enable batch touching globally
66
+ def enable!
67
+ @disabled = false
64
68
  end
65
69
 
66
- delegate :add_record, to: :current_state
70
+ # Disable batch touching for a block
71
+ def disable
72
+ Thread.current[:batch_touching_disabled] = true
73
+ yield
74
+ ensure
75
+ Thread.current[:batch_touching_disabled] = false
76
+ end
67
77
 
68
- def batch_touching?
69
- states.present?
78
+ def disabled?
79
+ Thread.current[:batch_touching_disabled] || @disabled
70
80
  end
71
81
 
72
82
  # Start batching all touches. When done, apply them. (Unless nested.)
73
- def start(options = {})
83
+ def start(requires_new:)
74
84
  states.push State.new
75
85
  yield.tap do
76
86
  apply_touches if states.length == 1
77
87
  end
78
88
  ensure
79
- merge_transactions unless $! && options[:requires_new]
89
+ merge_transactions unless $! && requires_new
80
90
 
81
91
  # Decrement nesting even if +apply_touches+ raised an error. To ensure the stack of States
82
92
  # is empty after the top-level transaction exits.
83
93
  states.pop
84
94
  end
85
95
 
96
+ delegate :add_record, to: :current_state
97
+
98
+ def batch_touching?
99
+ states.present? && !disabled?
100
+ end
101
+
102
+ private
103
+
104
+ def states
105
+ Thread.current[:batch_touching_states] ||= []
106
+ end
107
+
108
+ def current_state
109
+ states.last
110
+ end
111
+
86
112
  # When exiting a nested transaction, merge the nested transaction's
87
113
  # touched records with the outer transaction's touched records.
88
114
  def merge_transactions
@@ -91,50 +117,58 @@ module ActiveRecord
91
117
 
92
118
  # Apply the touches that were batched. We're in a transaction already so there's no need to open one.
93
119
  def apply_touches
94
- callbacks_run = Set.new
120
+ current_time = ActiveRecord::Base.current_time_from_proper_timezone
121
+ already_touched = Set.new
95
122
  all_states = State.new
96
123
  while current_state.more_records?
97
124
  all_states.merge!(current_state)
98
125
  state_records = current_state.records
99
126
  current_state.clear_records!
100
- state_records.each do |_, records|
101
- # Run callbacks to collect more touches (i.e. touch: true for associations)
102
- records.each do |record|
103
- unless callbacks_run.include?(record)
104
- record._run_touch_callbacks
105
- callbacks_run.add(record)
106
- end
107
- end
127
+ state_records.each do |(_klass, columns), records|
128
+ soft_touch_records(columns, records, current_time, already_touched)
108
129
  end
109
130
  end
110
131
 
111
132
  # Sort by class name. Having a consistent order can help mitigate deadlocks.
112
- sorted_records = all_states.records.keys.sort_by { |k| k.first.name }.map { |k| [k, all_states.records[k]] }.to_h
133
+ sorted_records = all_states.records.keys.sort_by { |k| k.first.name }.to_h { |k| [k, all_states.records[k]] }
113
134
  sorted_records.each do |(klass, columns), records|
114
- touch_records klass, columns, records
135
+ records.reject!(&:destroyed?)
136
+ touch_records klass, columns, records, current_time
115
137
  end
116
138
  end
117
139
 
118
- # Touch the specified records--non-empty set of instances of the same class.
119
- def touch_records(klass, columns, records)
120
- if columns.present?
121
- current_time = records.first.send(:current_time_from_proper_timezone)
122
-
123
- records.each do |record|
124
- record.instance_eval do
125
- columns.each { |column| write_attribute column, current_time }
126
- if locking_enabled?
127
- self[self.class.locking_column] += 1
128
- clear_attribute_change(self.class.locking_column)
129
- end
130
- clear_attribute_changes(columns)
131
- end
132
- end
140
+ # Perform DB update to touch records
141
+ def touch_records(klass, columns, records, time)
142
+ return if columns.blank? || records.blank?
133
143
 
134
- sql = columns.map { |column| "#{klass.connection.quote_column_name(column)} = :current_time" }.join(", ")
135
- sql += ", #{klass.locking_column} = #{klass.locking_column} + 1" if klass.locking_enabled?
144
+ sql = columns.map { |column| "#{klass.connection.quote_column_name(column)} = :time" }.join(", ")
145
+ sql += ", #{klass.locking_column} = #{klass.locking_column} + 1" if klass.locking_enabled?
146
+
147
+ klass.unscoped.where(klass.primary_key => records.to_a).update_all([sql, time: time])
148
+ end
149
+
150
+ # Set new timestamp in memory, without updating the DB just yet.
151
+ def soft_touch_records(columns, records, time, already_touched)
152
+ return if columns.blank? || records.blank?
153
+
154
+ records.each do |record|
155
+ next if record.destroyed? || already_touched.include?(record)
156
+
157
+ soft_touch_record(columns, record, time)
158
+
159
+ # Running callbacks also allows us to collect more touches (i.e. touch: true for associations).
160
+ record._run_touch_callbacks
161
+ already_touched.add(record)
162
+ end
163
+ end
136
164
 
137
- klass.unscoped.where(klass.primary_key => records.to_a).update_all([sql, current_time: current_time])
165
+ def soft_touch_record(columns, record, time)
166
+ columns.each { |column| record.write_attribute column, time }
167
+ if record.locking_enabled?
168
+ record[record.class.locking_column] += 1
169
+ record.clear_attribute_changes(columns + [record.class.locking_column])
170
+ else
171
+ record.clear_attribute_changes(columns)
138
172
  end
139
173
  end
140
174
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-batch_touching
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.pre.beta2
4
+ version: 1.0.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Morearty
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-07-01 00:00:00.000000000 Z
12
+ date: 2022-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -54,7 +54,7 @@ dependencies:
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: sqlite3
57
+ name: rspec-rails
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
@@ -68,7 +68,7 @@ dependencies:
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
- name: timecop
71
+ name: rubocop
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - ">="
@@ -82,7 +82,35 @@ dependencies:
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
- name: rspec-rails
85
+ name: rubocop-performance
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rubocop-rake
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: rubocop-rspec
86
114
  requirement: !ruby/object:Gem::Requirement
87
115
  requirements:
88
116
  - - ">="
@@ -123,6 +151,34 @@ dependencies:
123
151
  - - ">="
124
152
  - !ruby/object:Gem::Version
125
153
  version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: sqlite3
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: timecop
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
126
182
  description: Batch up your ActiveRecord "touch" operations for better performance.
127
183
  All accumulated "touch" calls will be consolidated into as few database round trips
128
184
  as possible.
@@ -136,10 +192,11 @@ files:
136
192
  - lib/activerecord/batch_touching.rb
137
193
  - lib/activerecord/batch_touching/state.rb
138
194
  - lib/activerecord/batch_touching/version.rb
139
- homepage: ''
195
+ homepage: https://github.com/irphilli/activerecord-batch_touching
140
196
  licenses:
141
197
  - MIT
142
- metadata: {}
198
+ metadata:
199
+ rubygems_mfa_required: 'true'
143
200
  post_install_message:
144
201
  rdoc_options: []
145
202
  require_paths:
@@ -148,7 +205,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
205
  requirements:
149
206
  - - ">="
150
207
  - !ruby/object:Gem::Version
151
- version: '0'
208
+ version: 2.7.0
152
209
  required_rubygems_version: !ruby/object:Gem::Requirement
153
210
  requirements:
154
211
  - - ">"