Module: RaceGuard::DBLockAuditor::ReadModifyWrite::ReadModWriteImpl
- Defined in:
- lib/race_guard/db_lock_auditor/read_modify_write.rb
Overview
Internal: keeps Patches and singleton API small (RuboCop). rubocop:disable Metrics/ModuleLength – one cohesive implementation unit for 4.1
Constant Summary collapse
- WRITE_DEPTH_KEY =
:__race_guard_rmw_in_save_depth
Class Method Summary collapse
- .after_relation_update_all(payload) ⇒ Object
- .after_save(record, success, write_label) ⇒ Object
- .around_with_lock_user_block(record) ⇒ Object
- .atomic_sql_update_all?(updates) ⇒ Boolean
- .before_relation_update_all(relation, updates) ⇒ Object
- .capture_read!(record, attr) ⇒ Object
- .check_after_persisted_save!(record, write_label) ⇒ Object
- .each_rmw_matching_change(record, changes, write_label, record_pk) ⇒ Object
- .enter_persisted_write! ⇒ Object
- .forget_reads_for_relation_records!(model_class, ids) ⇒ Object
- .inside_persisted_write? ⇒ Boolean
- .leave_persisted_write! ⇒ Object
- .normalize_attr_name_for_read(record, attr) ⇒ Object
- .note_read_from__read_attribute(record, raw_attr, _val) ⇒ Object
- .note_read_from_read_attribute(record, raw_attr, _val) ⇒ Object
- .primary_key_value_for_rmw(record) ⇒ Object
- .record_pessimistic_lock_for_record!(record) ⇒ Object
- .relation_target_record_ids(relation) ⇒ Object
-
.report_read_modify_write!(record, attr_name, read_age_ms, write_label, record_pk = nil) ⇒ Object
rubocop:disable Metrics/MethodLength – one linear report; keeps context fields explicit.
- .should_track_read?(record) ⇒ Boolean
- .should_track_write?(record) ⇒ Boolean
- .should_track_write_class?(klass) ⇒ Boolean
- .skip_rmw_due_to_pessimistic_lock?(record, record_pk) ⇒ Boolean
Class Method Details
.after_relation_update_all(payload) ⇒ Object
234 235 236 237 238 239 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 234 def after_relation_update_all(payload) return unless payload forget_reads_for_relation_records!(payload[:model_class], payload[:ids]) nil end |
.after_save(record, success, write_label) ⇒ Object
155 156 157 158 159 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 155 def after_save(record, success, write_label) return unless success check_after_persisted_save!(record, write_label) end |
.around_with_lock_user_block(record) ⇒ Object
198 199 200 201 202 203 204 205 206 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 198 def around_with_lock_user_block(record) pk = primary_key_value_for_rmw(record) begin RaceGuard.context.rmw_with_lock_block_enter!(record.class, pk) if pk yield ensure RaceGuard.context.rmw_with_lock_block_leave!(record.class, pk) if pk end end |
.atomic_sql_update_all?(updates) ⇒ Boolean
241 242 243 244 245 246 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 241 def atomic_sql_update_all?(updates) return true if updates.is_a?(String) return true if defined?(Arel::Nodes::SqlLiteral) && updates.is_a?(Arel::Nodes::SqlLiteral) false end |
.before_relation_update_all(relation, updates) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 222 def before_relation_update_all(relation, updates) return nil unless atomic_sql_update_all?(updates) return nil unless should_track_write_class?(relation.klass) ids = relation_target_record_ids(relation) return nil if ids.empty? { model_class: relation.klass, ids: ids } rescue StandardError nil end |
.capture_read!(record, attr) ⇒ Object
145 146 147 148 149 150 151 152 153 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 145 def capture_read!(record, attr) return if inside_persisted_write? return unless should_track_read?(record) pk = primary_key_value_for_rmw(record) return unless pk RaceGuard.context.rmw_read_record!(record.class, pk, attr) end |
.check_after_persisted_save!(record, write_label) ⇒ Object
161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 161 def check_after_persisted_save!(record, write_label) return unless should_track_write?(record) return unless record.persisted? pk = primary_key_value_for_rmw(record) return if pk.nil? changes = record.saved_changes return if changes.nil? || changes.empty? each_rmw_matching_change(record, changes, write_label, pk) end |
.each_rmw_matching_change(record, changes, write_label, record_pk) ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 174 def each_rmw_matching_change(record, changes, write_label, record_pk) changes.each_key do |attr_name| age_ms = RaceGuard.context.rmw_read_age_ms_for(record.class, record_pk, attr_name) next unless age_ms if skip_rmw_due_to_pessimistic_lock?(record, record_pk) RaceGuard.context.rmw_read_forget!(record.class, record_pk, attr_name) next end report_read_modify_write!(record, attr_name, age_ms, write_label, record_pk) RaceGuard.context.rmw_read_forget!(record.class, record_pk, attr_name) end end |
.enter_persisted_write! ⇒ Object
91 92 93 94 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 91 def enter_persisted_write! d = Thread.current[WRITE_DEPTH_KEY].to_i + 1 Thread.current[WRITE_DEPTH_KEY] = d end |
.forget_reads_for_relation_records!(model_class, ids) ⇒ Object
258 259 260 261 262 263 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 258 def forget_reads_for_relation_records!(model_class, ids) ids.each do |id| RaceGuard.context.rmw_read_forget_record!(model_class, id) end nil end |
.inside_persisted_write? ⇒ Boolean
101 102 103 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 101 def inside_persisted_write? Thread.current[WRITE_DEPTH_KEY].to_i.positive? end |
.leave_persisted_write! ⇒ Object
96 97 98 99 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 96 def leave_persisted_write! d = Thread.current[WRITE_DEPTH_KEY].to_i - 1 Thread.current[WRITE_DEPTH_KEY] = d <= 0 ? nil : d end |
.normalize_attr_name_for_read(record, attr) ⇒ Object
119 120 121 122 123 124 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 119 def normalize_attr_name_for_read(record, attr) s = attr.to_s return s unless record.class.respond_to?(:attribute_aliases) record.class.attribute_aliases[s] || s end |
.note_read_from__read_attribute(record, raw_attr, _val) ⇒ Object
126 127 128 129 130 131 132 133 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 126 def note_read_from__read_attribute(record, raw_attr, _val) return if raw_attr.nil? || inside_persisted_write? n = normalize_attr_name_for_read(record, raw_attr) capture_read!(record, n) rescue StandardError nil end |
.note_read_from_read_attribute(record, raw_attr, _val) ⇒ Object
135 136 137 138 139 140 141 142 143 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 135 def note_read_from_read_attribute(record, raw_attr, _val) return if raw_attr.nil? || inside_persisted_write? n = raw_attr.to_s n = record.class.attribute_aliases[n] || n if record.class.respond_to?(:attribute_aliases) capture_read!(record, n) rescue StandardError nil end |
.primary_key_value_for_rmw(record) ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 105 def primary_key_value_for_rmw(record) return nil unless record.is_a?(::ActiveRecord::Base) return nil if record.new_record? pk = record.class.primary_key name = (pk.is_a?(Array) ? pk.first : pk).to_s set = record.instance_variable_get(:@attributes) return nil unless set set.fetch_value(name) rescue StandardError nil end |
.record_pessimistic_lock_for_record!(record) ⇒ Object
208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 208 def record_pessimistic_lock_for_record!(record) return unless should_track_write?(record) return unless record.is_a?(::ActiveRecord::Base) return if record.new_record? pk = primary_key_value_for_rmw(record) return if pk.nil? RaceGuard.context.rmw_pessimistic_lock_register!(record.class, pk) RaceGuard.context.rmw_read_forget_record!(record.class, pk) rescue StandardError nil end |
.relation_target_record_ids(relation) ⇒ Object
248 249 250 251 252 253 254 255 256 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 248 def relation_target_record_ids(relation) klass = relation.klass pk = klass.primary_key return [] if pk.nil? || pk.is_a?(Array) relation.except(:select).pluck(pk).compact.uniq rescue StandardError [] end |
.report_read_modify_write!(record, attr_name, read_age_ms, write_label, record_pk = nil) ⇒ Object
rubocop:disable Metrics/MethodLength – one linear report; keeps context fields explicit
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 294 def report_read_modify_write!(record, attr_name, read_age_ms, write_label, record_pk = nil) cfg = RaceGuard.configuration msg = "Read-modify-write on #{record.class.name}##{attr_name} " \ '(read then persisted save; consider atomic SQL, locking, or idempotency)' snapshot = RaceGuard.context.current sev = cfg.severity_for(:'db_lock_auditor:read_modify_write') rid = record_pk || primary_key_value_for_rmw(record) ctx = { 'model' => record.class.name, 'record_id' => rid, 'attribute' => attr_name.to_s, 'write_method' => write_label, 'in_transaction' => snapshot.in_transaction?, 'read_age_ms' => read_age_ms.round } RaceGuard.report( detector: ReadModifyWrite::DETECTOR, message: msg, severity: sev, context: ctx ) end |
.should_track_read?(record) ⇒ Boolean
265 266 267 268 269 270 271 272 273 274 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 265 def should_track_read?(record) cfg = RaceGuard.configuration return false unless cfg.active? return false unless record.is_a?(::ActiveRecord::Base) return false if record.new_record? return false if primary_key_value_for_rmw(record).nil? return false if cfg.db_lock_read_modify_write_models.empty? cfg.db_lock_read_modify_write_tracks?(record.class) end |
.should_track_write?(record) ⇒ Boolean
276 277 278 279 280 281 282 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 276 def should_track_write?(record) cfg = RaceGuard.configuration return false unless cfg.active? return false if cfg.db_lock_read_modify_write_models.empty? cfg.db_lock_read_modify_write_tracks?(record.class) end |
.should_track_write_class?(klass) ⇒ Boolean
284 285 286 287 288 289 290 291 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 284 def should_track_write_class?(klass) cfg = RaceGuard.configuration return false unless cfg.active? return false unless klass.is_a?(Class) return false if cfg.db_lock_read_modify_write_models.empty? cfg.db_lock_read_modify_write_tracks?(klass) end |
.skip_rmw_due_to_pessimistic_lock?(record, record_pk) ⇒ Boolean
189 190 191 192 193 194 195 196 |
# File 'lib/race_guard/db_lock_auditor/read_modify_write.rb', line 189 def skip_rmw_due_to_pessimistic_lock?(record, record_pk) return true if RaceGuard.context.rmw_pessimistic_lock_active?(record.class, record_pk) depth = RaceGuard.context.rmw_with_lock_block_depth_for(record.class, record_pk) return true if depth.positive? false end |