Transaction unable to Rollback beyond certain Model - ruby-on-rails

I have to process a very long form with multiple models.
def registerCandidate
action_redirect = ""
id = ""
ActiveRecord::Base.transaction do
begin
#entity = Entity.new( name: params[:entity][:name], description: params[:entity][:description], logo: params[:entity][:logo])
#access = Access.new( username: params[:access][:username], password: params[:access][:password], remember_me: params[:access][:rememberme], password_confirmation: params[:access][:password_confirmation])
#access.entity = #entity
#access.save!
#biodata = Biodatum.new(
date_of_birth: params[:biodatum][:birthday],
height: params[:biodatum][:height],
family_members: params[:biodatum][:family_members],
gender: params[:biodatum][:gender],
complexion: params[:biodatum][:complexion],
marital_status: params[:biodatum][:marital_status],
blood_type: params[:biodatum][:blood_type],
religion: params[:biodatum][:religion],
education: params[:biodatum][:education],
career_experience: params[:biodatum][:career_experience],
notable_accomplishments: params[:biodatum][:notable_accomplishments],
emergency_contact: params[:biodatum][:emergency_contact],
languages_spoken: params[:biodatum][:languages_spoken]
)
#biodata.entity = #entity
#biodata.save!
#employee = Employee.new()
#employee.entity = #entity
#employee.save!
action_redirect = "success_candidate_registration"
id = #access.id
#action_redirect = "success_candidate_registration?id=" + #access.id
#Error Processing
rescue StandardError => e
flash[:collective_errors] = "An error of type #{e.class} happened, message is #{e.message}"
action_redirect = "candidate_registration"
end
end
redirect_to action: action_redirect, access_id: id
end
If I raise any error beyond #access.save! it does the entire transaction without rolling back. How do you modify in order for any error related to all models rollback everything?

Since you are rescuing StandardError - which most errors derive from you could very well be swallowing errors which could cause a rollback.
This is commonly known as Pokemon exception handling (Gotta catch em' all) and is an anti-pattern.
Instead listen for more specific errors such as ActiveRecord::RecordInvalid - and raise a raise ActiveRecord::Rollback on error to cause a rollback.
If you haven't already read the Active Record Transactions docs, there is some very good information there about what will cause a rollback.
Also if you want to be a good Ruby citizen and follow the principle of least surprise use snakecase (register_candidate) not camelCase (registerCandidate) when naming methods.
Added
Instead of copying every value from params to your models you should use strong parameters and pass the model a hash.
This reduces code duplication between controller actions and keeps your controllers nice and skinny.
class Biodatum < ActiveRecord::Base
alias_attribute :date_of_birth, :birthday # allows you to take the :birthday param
end
# in your controller:
def register_candidate
# ...
#biodata = Biodatum.new(biodatum_params)
# ...
end
private
def biodatum_params
params.require(:biodatum).allow(
:birthday, :height, :family_members :family_members
) # #todo whitelist the rest of the parameters.
end

Related

How to cause manually ActiveRecord RecordInvalid in Rails

Entry: I have attribute class that derive from one of the ActiveRecord types and implement method cast(value) which transforms provided value to derived Active Record type. In our case we perform transformation only when the provided value is a String otherwise the default integer casting is performed. Private method to_minutes converts formatted time to an integer representing spent minutes. I assumed that 1d = 8h = 480m. E.g. result of to_minutes('1d 1h 1m') = 541. (I used this resource Custom Attributes in Ruby on Rails 5 ActiveRecord
If the string that came has no numbers, I need to return a validation error, and set this error to the #card.errors. How should I do it? I try:
if time.scan(/\d/).empty?
raise ActiveRecord::RecordInvalid.new(InvalidRecord.new)
end
But it does not work, I get an error
NameError in CardsController#update
uninitialized constant CardDuration::Type::InvalidRecord
Extracted source (around line #15):
I create my own attribute for integer type:
class CardDuration
class Type < ActiveRecord::Type::Value
def cast(value)
if value.is_a?(String)
to_seconds(value)
else
super
end
end
private
def to_seconds(time)
if time.scan(/\d/).empty?
return raise ActiveRecord::RecordInvalid.new, { errors: {message: 'Duration is too short (minimum is 1 number)'} }
end
time_sum = 0
time.split(' ').each do |time_part|
value = time_part.to_i
type = time_part[-1,1]
case type
when 'm'
value
when 'h'
value *= 60
when 'd'
value *= 8*60
else
value
end
time_sum += value
end
time_sum
end
end
end
and inside model:
class Card < ApplicationRecord
validates :duration, length: { within: 0..14880 }
attribute :duration, CardDuration::Type.new
end
Also validation doesn't work, and I do not understand why.
Thanks)
Inside controller this field can only be updated, so I need set the error to the #card.errors:
class CardsController < ApplicationController
def update
if #card.update(card_params)
flash[:success] = "Card was successfully updated."
else
flash[:error] = #card.errors.full_messages.join("\n")
render status: 422
end
rescue ActiveRecord::RecordInvalid => e
return e.record
end
end
in ActiveRecord::RecordInvalid.new(...) you need to pass structure, which have method 'errors' documentation.
try to raise ActiveRecord::RecordInvalid.new(self.new)
or write or own class, with method errors, which will be handle your exeptions
The only problem that I can see in your code is the way you're raising the invalid record error.
The correct way to do that would be-
raise ActiveRecord::RecordInvalid

Handle exception in ruby on rails

I called a method #txt.watch inside model from worker and Inside watch() there is an array of parameters(parameters = self.parameters). Each parameter have unique reference id.
I want to rescue each exception error for each parameter from inside worker.
class TextWorker
def perform(id)
#txt = WriteTxt.find(id)
begin
#txt.watch
total_complete_watch = if #txt.job_type == 'movie'
#txt.total_count
else
#txt.tracks.where(status:'complete').size
end
#txt.completed = total_completed_games
#txt.complete = (total_complete_games == #txt.total_count)
#txt.completed_at = Time.zone.now if #txt.complete
#txt.zipper if #txt.complete
#txt.save
FileUtils.rm_rf #txt.base_dir if #txt.complete
rescue StandardError => e
#How to find errors for each reference_id here
raise e
end
end
end
Is there any way to do. Thanks u very much.
I assume self.parameters are in your Model class instance. In that case, do as follows and you can reference them.
begin
#txt.watch
rescue StandardError
p #parameters # => self.parameters in the Model context
raise
end
Note:
As a rule of thumb, it is recommended to limit the scope of rescue as narrow as possible. Do not include statements which should not raise Exceptions in your main clause (such as, #txt.save and FileUtils.rm_rf in your case). Also, it is far better to limit the class of an exception; for example, rescue Encoding::CompatibilityError instead of EncodingError, or EncodingError instaed of StandardError, and so on. Or, an even better way is to define your own Exception class and raise it deliberately.

How to DRY up Webhook handlers in Rails

I've been developing Stripe Webhook handler to create/update records depending the values.
It's not really hard, if it's a simple like this below;
StripeEvent.configure do |events|
events.subscribe 'charge.succeeded' do |event|
charge = event.data.object
StripeMailer.receipt(charge).deliver
StripeMailer.admin_charge_succeeded(charge).deliver
end
end
However If I need to store the data conditionally, it could be little messier.
In here I extracted the each Webhook handler and defined something like stripe_handlers/blahblah_handler.rb.
class InvoicePaymentFailed
def call(event)
invoice_obj = event.data.object
charge_obj = retrieve_charge_obj_of(invoice_obj)
invoice = Invoice.find_by(stripe_invoice_id: charge_obj[:invoice])
# common execution for subscription
invoice.account.subscription.renew_billing_period(start_at: invoice_obj[:period_start], end_at: invoice_obj[:period_end])
case invoice.state
when 'pending'
invoice.fail!(:processing,
amount_due: invoice[:amount_due],
error: {
code: charge_obj[:failure_code],
message: charge_obj[:failure_message]
})
when 'past_due'
invoice.failed_final_attempt!
end
invoice.next_attempt_at = Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i)
invoice.attempt_count = invoice_obj[:attempt_count].to_i
invoice.save
end
private
def retrieve_charge_obj_of(invoice)
charge_obj = Stripe::Charge.retrieve(id: invoice.charge)
return charge_obj
rescue Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::APIConnectionError, Stripe::StripeError => e
logger.error e
logger.error e.backtrace.join("\n")
end
end
end
I just wonder how I can DRY up this Webhook handler.
Is there some best practice to approach this or any ideas?
I suggest re-raising the exception in retrieve_charge_obj_of, since you'll just get a nil reference exception later on, which is misleading. (As is, you might as well let the exception bubble up, and let a dedicated error handling system rescue, log, and return a meaningful 500 error.)
a. If you don't want to return a 500, then you have a bug b/c retrieve_charge_obj_of will return nil after the exception is rescued. And if charge_obj is nil, then this service will raise a NPE, resulting in a 500.
if invoice_obj[:next_payment_attempt] can be !present? (blank?), then what is Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i) supposed to mean?
a. If it was nil, false, or '', #to_i returns 0 -- is that intended? ([]/{} is also blank? but would raise)
Conceptually, this handler needs to issue a state transition on an Invoice, so a chunk of this logic can go in the model instead:
class Invoice < ApplicationRecord
# this method is "internal" to your application, so incoming params should be already "clean"
def mark_payment_failed!(err_code, err_msg, attempt_count, next_payment_at)
transaction do # payment processing usually needs to be transactional
case self.state
when 'pending'
err = { code: err_code, message: err_msg }
self.fail!(:processing, amount_due: self.amount_due, error: err)
when 'past_due'
self.failed_final_attempt!
else
ex_msg = "some useful data #{state} #{err_code}"
raise InvalidStateTransition, ex_msg
end
self.next_attempt_at = next_payment_at
self.attempt_count = attempt_count
self.save
end
end
class InvalidStateTransition < StandardError; end
end
Note: I recommend a formal state machine implementation (e.g. state_machine) before states & transitions get out of hand.
Data extraction, validation, and conversion should happen in the handler (that's what "handlers" are for), and they should happen before flowing deeper in your application. Errors are best caught early and execution stopped early, before any action has been taken.
There are still some other edge cases that I see that aren't really handled.

Rails - 'can't dump hash with default proc' during custom validation

I have 2 models. User and Want. A User has_many: Wants.
The Want model has a single property besides user_id, that's name.
I have written a custom validation in the Want model so that a user cannot submit to create 2 wants with the same name:
validate :existing_want
private
def existing_want
return unless errors.blank?
errors.add(:existing_want, "you already want that") if user.already_wants? name
end
The already_wants? method is in the User model:
def already_wants? want_name
does_want_already = false
self.wants.each { |w| does_want_already = true if w.name == want_name }
does_want_already
end
The validation specs pass in my model tests, but my feature tests fail when i try and submit a duplicate to the create action in the WantsController:
def create
#want = current_user.wants.build(params[:want])
if #want.save
flash[:success] = "success!"
redirect_to user_account_path current_user.username
else
flash[:validation] = #want.errors
redirect_to user_account_path current_user.username
end
end
The error I get: can't dump hash with default proc
No stack trace that leads to my code.
I have narrowed the issue down to this line:
self.wants.each { |w| does_want_already = true if w.name == want_name }
if I just return true regardless the error shows in my view as I would like.
I don't understand? What's wrong? and why is it so cryptic?
Thanks.
Without a stack trace (does it lead anywhere, or does it just not appear?) it is difficult to know what exactly is happening, but here's how you can reproduce this error in a clean environment:
# initialize a new hash using a block, so it has a default proc
h = Hash.new {|h,k| h[k] = k }
# attempt to serialize it:
Marshal.dump(h)
#=> TypeError: can't dump hash with default proc
Ruby can't serialize procs, so it wouldn't be able to properly reconstitute that serialized hash, hence the error.
If you're reasonably sure that line is the source of your trouble, try refactoring it to see if that solves the problem.
def already_wants? want_name
wants.any? {|want| want_name == want.name }
end
or
def already_wants? want_name
wants.where(name: want_name).count > 0
end

deserialize database object

I have a statistic model
class Statistic < ActiveRecord::Base
serialize :value
end
The model contains a value attribute containing a Goals object. I want to deserialize the goals object
when I do
goals = Statistic.all
goals.each do |goal|
test = goal.value
end
I get an error
value was supposed to be a Goals, but was a String
In the debugger I see that goal.value is of type String and contains the goal data
--- !ruby/object:Goals \ngoals: {}\n\ngoals_against: 1\ngoals_for: 0\nversion: 1\n
When I add serialize :value, Goals I get following error when deserialzing
ActiveRecord::SerializationTypeMismatch in ClubsController#new
value was supposed to be a Goals, but was a String
The Goals class
class Goals
attr_accessor :goals
attr_accessor :goals_for
attr_accessor :goals_against
attr_accessor :goals_own
attr_accessor :penalty_for
attr_accessor :penalty_against
def initialize(goals = nil, goals_against = nil, goals_own = nil, penalty_for = nil, penalty_against = nil)
#version = 1
if goals.nil?
#goals = {}
else
#goals = goals
end
#goals_against = goals_against.to_i
#goals_own = goals_own.to_i unless goals_own.nil?
unless penalty_for.nil?
#penalty_for = penalty_for.to_i
#penalty_against = penalty_against.to_i
end
set_goals_for()
end
private
def set_goals_for
#goals_for = 0
#goals.each_value {|value| #goals_for += value.to_i }
#goals_for += #goals_own unless #goals_own.nil?
end
end
Someone knows how I can make rails know that its an goals object and not a string?
Thanks
Most of my experience with serialization problems comes from Rails 1 era, but I recall two usual reasons of serialization failures:
silently ignored exceptions
class reloading
Looking at the file ./activerecord/lib/active_record/base.rb (tag v3.0.7 from git) I see that there is a 'rescue' clause:
def object_from_yaml(string)
return string unless string.is_a?(String) && string =~ /^---/
YAML::load(string) rescue string
end
You may try to investigate what exception is thrown by YAML::load. I usually change this method into something like this:
begin
YAML::load(string)
rescue => e
Rails.logger.warn "YAML exception (ignored): #{e.inspect}"
string
end
About the class reloading, is your problem also visible in test mode? I was registering my classes in YAML, and noticed that the class definition was gone in each second request, since the class object was recreated, and the registered one was taken away from the class chains. I don't think this is your problem, but I am signalling it anyway - maybe this will be helpful?

Resources