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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f994be54523c59f7878cbcf822ca03552bb3a365215cb3de7f23c05541381c75
|
4
|
+
data.tar.gz: aeab5dbe2bebca4b0b88d2a77ce033b7a288969fe6bddb18c74e332ec729a6bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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] =
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
-
#
|
115
|
-
def
|
152
|
+
# Gather all the touches that were batched.
|
153
|
+
def gather_touches
|
116
154
|
current_time = ActiveRecord::Base.current_time_from_proper_timezone
|
117
|
-
|
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,
|
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 }.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
#
|
137
|
-
|
138
|
-
|
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.
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
158
|
-
|
159
|
-
if
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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.
|
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-
|
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:
|
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:
|
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:
|
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:
|
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:
|
213
|
+
version: '0'
|
157
214
|
requirements: []
|
158
215
|
rubygems_version: 3.3.7
|
159
216
|
signing_key:
|