activerecord-batch_touching 1.0.pre.beta4 → 1.1

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: 8e3ae09116c725e745e467afa1d834b6b2a28bcae4f01d5346e598235bb7d0a8
4
- data.tar.gz: 842acced5ccc6df3149f4ae60b2b7f574f1bf04a553a67b54d6832c1501b94ee
3
+ metadata.gz: f994be54523c59f7878cbcf822ca03552bb3a365215cb3de7f23c05541381c75
4
+ data.tar.gz: aeab5dbe2bebca4b0b88d2a77ce033b7a288969fe6bddb18c74e332ec729a6bf
5
5
  SHA512:
6
- metadata.gz: 5339c3f783b5b4e8f667a47437fa26e507353d648fd99f637ea73925c28d07d626c7eeb9a8850552ae4182ebb1d36ae9acb5acbe0e2d42e7b0d9f1664a0d86d7
7
- data.tar.gz: bf5025309b66d19d7571ef34a3701eb23d9d8c054b68619bb6ad1f881439b37eb99e2a6fdd67e53dbadccf585ea41ae2703ee09fe10d3a81dd9e422675aa8b56
6
+ metadata.gz: '0808dfbb8f5ea2fedbd51feaf8c453f9224699017d5c33c54a8c08ff02396b52524b67691937ba9b48b838f84257abf0eab3280500bb976945b7eec7302fc89a'
7
+ data.tar.gz: 4237add83e7f90d3c6852f3221daa31e798eedc2a2b780193e2eab71ae8d17c831c8d2076d899bfed1e84cf830285d501d820fd34ca942223e0f7b53dc5876f2
@@ -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,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Activerecord
2
4
  module BatchTouching
3
- VERSION = "1.0"
5
+ VERSION = "1.1"
4
6
  end
5
7
  end
@@ -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
 
@@ -20,9 +22,17 @@ module ActiveRecord
20
22
  # Person.first.touch
21
23
  # end
22
24
  def transaction(requires_new: nil, isolation: nil, joinable: true, &block)
23
- super(requires_new: requires_new, isolation: isolation, joinable: joinable) do
25
+ result = super(requires_new: requires_new, isolation: isolation, joinable: joinable) do
24
26
  BatchTouching.start(requires_new: requires_new, &block)
25
27
  end
28
+
29
+ if BatchTouching.should_apply_touches?
30
+ super() do
31
+ BatchTouching.apply_touches
32
+ end
33
+ end
34
+
35
+ result
26
36
  end
27
37
  end
28
38
 
@@ -67,7 +77,7 @@ module ActiveRecord
67
77
 
68
78
  # Disable batch touching for a block
69
79
  def disable
70
- Thread.current[:batch_touching_disabled] = false
80
+ Thread.current[:batch_touching_disabled] = true
71
81
  yield
72
82
  ensure
73
83
  Thread.current[:batch_touching_disabled] = false
@@ -77,12 +87,18 @@ module ActiveRecord
77
87
  Thread.current[:batch_touching_disabled] || @disabled
78
88
  end
79
89
 
80
- def states
81
- Thread.current[:batch_touching_states] ||= []
82
- end
90
+ # Start batching all touches. When done, apply them. (Unless nested.)
91
+ def start(requires_new:)
92
+ states.push State.new
93
+ yield.tap do
94
+ gather_touches if states.length == 1
95
+ end
96
+ ensure
97
+ merge_transactions unless $! && requires_new
83
98
 
84
- def current_state
85
- states.last
99
+ # Decrement nesting even if +gather_touches+ raised an error. To ensure the stack of States
100
+ # is empty after the top-level transaction exits.
101
+ states.pop
86
102
  end
87
103
 
88
104
  delegate :add_record, to: :current_state
@@ -91,18 +107,40 @@ module ActiveRecord
91
107
  states.present? && !disabled?
92
108
  end
93
109
 
94
- # Start batching all touches. When done, apply them. (Unless nested.)
95
- def start(options = {})
96
- states.push State.new
97
- yield.tap do
98
- apply_touches if states.length == 1
110
+ def should_apply_touches?
111
+ batched_touches.present?
112
+ end
113
+
114
+ def apply_touches
115
+ batched_touches&.each do |(klass, columns), records|
116
+ records.reject!(&:destroyed?)
117
+ touch_records klass, columns, records, batched_touches_time
99
118
  end
100
- ensure
101
- merge_transactions unless $! && options[:requires_new]
102
119
 
103
- # Decrement nesting even if +apply_touches+ raised an error. To ensure the stack of States
104
- # is empty after the top-level transaction exits.
105
- states.pop
120
+ reset!
121
+ end
122
+
123
+ private
124
+
125
+ def reset!
126
+ Thread.current[:batch_touching_batch] = nil
127
+ Thread.current[:batch_touching_time] = nil
128
+ end
129
+
130
+ def batched_touches
131
+ Thread.current[:batch_touching_batch]
132
+ end
133
+
134
+ def batched_touches_time
135
+ Thread.current[:batch_touching_time]
136
+ end
137
+
138
+ def states
139
+ Thread.current[:batch_touching_states] ||= []
140
+ end
141
+
142
+ def current_state
143
+ states.last
106
144
  end
107
145
 
108
146
  # When exiting a nested transaction, merge the nested transaction's
@@ -111,56 +149,60 @@ module ActiveRecord
111
149
  states[-2].merge!(current_state) if states.length > 1
112
150
  end
113
151
 
114
- # Apply the touches that were batched. We're in a transaction already so there's no need to open one.
115
- def apply_touches
152
+ # Gather all the touches that were batched.
153
+ def gather_touches
116
154
  current_time = ActiveRecord::Base.current_time_from_proper_timezone
117
- callbacks_run = Set.new
155
+ already_touched = Set.new
118
156
  all_states = State.new
119
157
  while current_state.more_records?
120
158
  all_states.merge!(current_state)
121
159
  state_records = current_state.records
122
160
  current_state.clear_records!
123
161
  state_records.each do |(_klass, columns), records|
124
- soft_touch_records(columns, records, current_time, callbacks_run)
162
+ soft_touch_records(columns, records, current_time, already_touched)
125
163
  end
126
164
  end
127
165
 
128
166
  # Sort by class name. Having a consistent order can help mitigate deadlocks.
129
- sorted_records = all_states.records.keys.sort_by { |k| k.first.name }.map { |k| [k, all_states.records[k]] }.to_h
130
- sorted_records.each do |(klass, columns), records|
131
- records.reject!(&:destroyed?)
132
- touch_records klass, columns, records, current_time if records.present?
133
- end
167
+ sorted_records = all_states.records.keys.sort_by { |k| k.first.name }.to_h { |k| [k, all_states.records[k]] }
168
+
169
+ # Save results to a thread so that we can reference these later in their own transaction.
170
+ Thread.current[:batch_touching_batch] = sorted_records
171
+ Thread.current[:batch_touching_time] = current_time
172
+ end
173
+
174
+ # Perform DB update to touch records
175
+ def touch_records(klass, columns, records, time)
176
+ return if columns.blank? || records.blank?
177
+
178
+ sql = columns.map { |column| "#{klass.connection.quote_column_name(column)} = :time" }.join(", ")
179
+ sql += ", #{klass.locking_column} = #{klass.locking_column} + 1" if klass.locking_enabled?
180
+
181
+ klass.unscoped.where(klass.primary_key => records.to_a).update_all([sql, time: time])
134
182
  end
135
183
 
136
- # Only set new timestamp in memory.
137
- # Running callbacks also allows us to collect more touches (i.e. touch: true for associations).
138
- def soft_touch_records(columns, records, time, callbacks_run)
184
+ # Set new timestamp in memory, without updating the DB just yet.
185
+ def soft_touch_records(columns, records, time, already_touched)
186
+ return if columns.blank? || records.blank?
187
+
139
188
  records.each do |record|
140
- record.instance_eval do
141
- unless destroyed?
142
- columns.each { |column| write_attribute column, time }
143
- if locking_enabled?
144
- self[self.class.locking_column] += 1
145
- clear_attribute_change(self.class.locking_column)
146
- end
147
- clear_attribute_changes(columns)
148
- end
149
- unless callbacks_run.include?(record)
150
- record._run_touch_callbacks
151
- callbacks_run.add(record)
152
- end
153
- end
189
+ next if record.destroyed? || already_touched.include?(record)
190
+
191
+ soft_touch_record(columns, record, time)
192
+
193
+ # Running callbacks also allows us to collect more touches (i.e. touch: true for associations).
194
+ record._run_touch_callbacks
195
+ already_touched.add(record)
154
196
  end
155
197
  end
156
198
 
157
- # Touch the specified records--non-empty set of instances of the same class.
158
- def touch_records(klass, columns, records, time)
159
- if columns.present?
160
- sql = columns.map { |column| "#{klass.connection.quote_column_name(column)} = :time" }.join(", ")
161
- sql += ", #{klass.locking_column} = #{klass.locking_column} + 1" if klass.locking_enabled?
162
-
163
- klass.unscoped.where(klass.primary_key => records.to_a).update_all([sql, time: time])
199
+ def soft_touch_record(columns, record, time)
200
+ columns.each { |column| record.write_attribute column, time }
201
+ if record.locking_enabled?
202
+ record[record.class.locking_column] += 1
203
+ record.clear_attribute_changes(columns + [record.class.locking_column])
204
+ else
205
+ record.clear_attribute_changes(columns)
164
206
  end
165
207
  end
166
208
  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.beta4
4
+ version: '1.1'
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-07 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/ProductPlan/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,12 +205,12 @@ 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
+ - - ">="
155
212
  - !ruby/object:Gem::Version
156
- version: 1.3.1
213
+ version: '0'
157
214
  requirements: []
158
215
  rubygems_version: 3.3.7
159
216
  signing_key: