dom
dom

Reputation: 444

How can update_all fail without raising an exception?

I have problem with storing data into Postgres using update_all. To explain the problem, we have 2 classes, Meter and Readings. Every meter has many readings. Meter has attributes unit, like energy unit kWh, MWh, ..., and multiplier, number that multiplies readings state to get final value. When user wants to update Meter params (unit, multiplier), we use Interactors to firstly update Readings states and then save Meter itself. All these operations happen in single transaction, so if one fails, then all fail. But we came in situation, when meter is saved and readings are not updated or vice versa. I checked, that when meter does not save correctly, it causes context.fail!. Readings uses update_all without any check of success, but I read, that update_all goes directly to the DB and when it fails on constraints, it fails with an exception.

I did not find any way, how to replicate it.

// update readings
class Meters::ChangeUnit
   // includes

   def call
      coefficient = 1.0
      coefficient *= unit_change if context.meter.energy_unit_changed?
      coefficient *= multiplier_change if context.meter.multiplier_changed?

      return if coefficient == 1.0

      // this probably fails:
      context.meter.readings.update_all "state = state * #{coefficient}"
   end

   // ...

end

// save meter
class Meters::Save
    include Meters::BaseInteractor

    def call
        context.fail! meter_errors: context.meter.errors unless context.meter.save
    end
end

My idea is to use something like this into Meters::ChangeUnit call:

 // ...
 cnt = context.meter.readings.count
 updated = context.meter.readings.update_all "state = state * #{coefficient}"
 unless cnt == updated
   context.fail! updated_meter_readings: "#{updated}/#{cnt}"
 end
 // ...

but I have no idea, how to prove it.

EDIT1:

 // usage in cotroller
 context = UpdateMeter.call(meter: @meter, bonds_definition: params[:meters_ids])


 // UpdateMeter
 class UpdateMeter
    include Interactor::Organizer

    organize Meters::Update, ProcessAfterCommitQueue
 end




// Meters::Update
class Meters::Update
  include Interactor::Organizer
  include Interactor::InTransaction

  organize Meters::ValidateActive,
           // ...
           Meters::ChangeUnit,
           // ...
           Meters::Save,
           // ...
end



// Interactor::InTransaction
module Interactor::InTransaction
   extend ActiveSupport::Concern

   included do
      around do |interactor|
         ActiveRecord::Base.transaction { interactor.call }
      end
   end
end

Upvotes: 0

Views: 709

Answers (2)

dom
dom

Reputation: 444

The problem was all about threads. We found out, that this problem occurred just several times within last few years. So it was really edge case.

There are two operations. One is mentioned meter params update by user, and the other is automatic import of readings from actual devices (physical meters). When import starts, it selects the unit and multiplier (action 1), modifies readings and saves them into the DB (action 2). But those 2 actions are not in the same transaction. So if user saved meter between those 2 actions, we would save wrong data, coz of updated unit or multiplier.

We resolved that using meter.reload in transaction with readings.save. We compare meter before reload and after reload, and if it has changed, we have to recalculate readings.

Upvotes: 0

kwerle
kwerle

Reputation: 2401

(comment can't format, so here it is in an answer)

I don't see a transaction. Without unwinding your code, I don't understand why this isn't just

Meter.transaction do
  context.meter.save
  interactor.call
end

Upvotes: 1

Related Questions