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

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: 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
  - - ">"