activerecord-batch_touching 1.0.pre.beta4 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
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: