Issues When Rescuing ActiveRecord::RecordNotUnique - sql

I'm attempting to build a table which will act as a queue of batched syncs to a third party service.
The following method should speak for itself; but to be clear, its intention is to add a new updatable object (a polymorphic relationship) with status: :queued to the delayed_syncs table.
There is a uniqueness constraint on the polymorphic relationship + status (updatable_id, updatable_type, status) which should cause updatable objects already in the queue with the :queued status to fail here and fall into the rescue block.
The issue I am seeing is that whenever the SELECT generated by find_by is fired, this entire method fails with a:
ActiveRecord::StatementInvalid
error.
Information I've found around this suggests a ROLLBACK or RELEASE SAVEPOINT after the failed INSERT, but I'm not sure how I would accomplish that here.
The aforementioned method:
def self.enqueue(updatable:, action:)
DelayedSync.create(updatable: updatable, status: :queued, action: action)
rescue ActiveRecord::RecordNotUnique
queued_update = DelayedSync.find_by(updatable: updatable, status: :queued, action: :sync_update)
if action == :sync_delete && queued_update.present?
queued_update.sync_delete!
else
Rails.logger.debug "#{updatable.class.name} #{updatable.id} already queued for sync, skipping."
end
end

Rather than rely on exception handling for logic, you can use ActiveRecord transactions to ensure all-or-nothing updates.
Like this:
ActiveRecord::Base.transaction do
DelayedSync.create!(updatable: updatable, status: :queued, action: action)
end
You can still safely utilize rescue to handle logging cleanup.
Docs that have much more detail about this can be found here.

After further digging, I uncovered the problem was due to how I was invoking this method from an after_save callback. Rails invokes after_save and after_destroy callbacks before the transaction has been closed. Rescuing the ActiveRecord::RecordNotUnique error invoked from this callback and attempting to execute more queries is impossible with Postgres, since it invalidates the entire transaction. My solution was to transition to the after_commit callback which provides the same control as after_save and after_destroy with the on: [:create, :destroy] parameter, with the benefit of being executed after the transaction (invalid or not) has closed.
This blog post is a bit dated, but the information near the bottom was immensely helpful and still holds true: http://markdaggett.com/blog/2011/12/01/transactions-in-rails/

Related

ActiveRecord transaction does not rollback

I'm new to ActiveRecord transactions. In the code below the first update_attributes causes a WARNING: Can't mass-assign protected attributes: account_type_cdx and that is ok. But I was surprised that the next line self.update_attributes!(:purchased => true) is executed and stored in the DB. I was expecting it to ROLLBACK because the first one failed.
I must be missing something...
Any hints?
def complete_purchase(current_user_id, plan_name)
Rails.logger.debug "COMPLETE PURCHASE"
user = User.find(current_user_id)
ActiveRecord::Base.transaction do
user.update_attributes!(:account_type_cdx => plan_name.to_sym)
self.update_attributes!(:purchased => true)
end
end
I followed the advices from this post: http://markdaggett.com/blog/2011/12/01/transactions-in-rails/
Thanks.
Rails ignores those records that are not explicitly listed in attr_accessible class call (hence the first update warning). It doesn't fail a transaction, that's why you'r reaching (and finishing) the second update_attributes! normally.

Track model changes on virtual attributes with previous_changes

I'm tracking changes on my model and I would like to track changes on a has_many relation. The model has:
define_attribute_method :tag_tokens
attr_reader :tag_tokens
def tag_tokens=(ids)
tag_tokens_will_change! unless ids == #tag_tokens
self.tag_ids = ids.split(",")
end
Combination of http://railscasts.com/episodes/258-token-fields and http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
The result is this exception which is caused on the define_attribute_method line. I believe this method is required to create the tag_tokens_will_change method.
ruby-1.9.3-p194#mm/gems/activerecord-3.2.8/lib/active_record/attribute_methods/time_zone_conversion.rb:59:in `create_time_zone_conversion_attribute?': undefined method `type' for nil:NilClass (NoMethodError)
How can I get this to work?
I received the same error, and it turns out the documentation we were referring to is outdated.
The current way to trigger this change is to call attribute_will_change! :attr_name
See the accepted answer here for more details.

Rails has_many clear method

According to the documentation Rails has_many association has clear method. Looks like it executes sql delete query immediately after it performs. Is there a canonical way to delete all the child objects and update association only at the moment of save method? For example:
#cart.container_items.delete_all_example # looks like `clear` execute sql at this line
if #cart.save
# do smth
else
#do smth
end
it is necessary because of many changes at the parent object and they must be committed all or none of them.
You don't want to delete_all, you want to destroy_all.
Calling delete_all executes a simple SQL delete, ignoring any callbacks and dependent records.
Using destroy_all invokes the destroy method on each object, allowing :dependent => :destroy to work as expected, cleaning up child records.
This does not destroy all objects at the point of save, and there is no canonical way to do that as you're not saving the record. Rails persists destroys at the point of the method call, not at a later save. If you need many destroys to be transactional, wrap them in a transaction:
Cart.begin do
#cart.container_items.delete_all_example
end
Try this:
Cart.transaction do
#cart.container_items.delete_all_example # looks like `clear` execute sql at this line
if #cart.save
# success
else
# error
raise ActiveRecord::Rollback
end
end
ActiveRecord::Rollback is not propagated outside the transaction block. It simply terminates the transaction.
Looks like i'm trying to do a transaction. Some articles to learn more about it:
Transations in Rails
Active record transactions

Delayed job with custom attributes

I'm using delayed job 3.0.2 with ActiveRecord and Rails 3.2.3. I have a User model which uses the has_secure_password mixin, so the password is only stored encrypted. Now I want to use delayed job to send the welcome email, which should contain a copy of the unencrypted password.
When creating the record, the plain-text password is in User#password. But delayed job seems to serialize/ deserialize the id of the record only and create a new instance of the model by doing User.find(X). This way my plain-text password is lost and the user gets an empty password in his email.
How can I tell delayed-job to serialize/ deserialize custom "virtual" attributes too, which are not stored in the database otherwise?
This is my monkey patch for delayed job 2.x, which worked fine.
class ActiveRecord::Base
def self.yaml_new(klass, tag, val)
klass.find(val['attributes']['id']).tap do |m|
val.except("attributes").each_pair{ |k, v| m.send("#{k}=", v) }
end
rescue ActiveRecord::RecordNotFound
raise Delayed::DeserializationError
end
end
It doesn't work with delayed job 3.x. I'm also not really interested in fixing my monkey patch as I hope there's a proper solution to this.
In delayed job 3.x, the best way to do this is to override a few methods on your ActiveRecord class, and then to force the Psych YAML deserializer to load the ActiveRecord object from the serialized data. By default, delayed job uses just the deserialized id, and then loads the ActiveRecord object from the DB. So, say I have an ActiveRecord class called ShipmentImport, and I want an attr_accessor named 'user_id' to work with delayed job serialization/deserialization. Here is what I would do.
In the ShipmentImport ActiveRecord class, add this:
def encode_with(coder)
super
coder['user_id'] = #user_id
end
def init_with(coder)
super
#user_id = coder['user_id']
self
end
In an initializer for your application, add this for your ActiveRecord class:
Psych.load_tags[['!ruby/ActiveRecord', ShipmentImport.name].join(':')] = ShipmentImport

Help debugging my session? Rails 3 ActionDispatch::Cookies::CookieOverflow

Even though I'm pretty sure I know why this error gets raised, I don't seem to know why or how my session is exceeding the 4KB limit...
My app was working fine, but once I deliberately started adding bugs to see if my transactions were rolling back I started getting this error.
To give some background, I'm busy coding a tournament application that (in this section) will create the tournament and then add some tournament legs based on the number of teams as well as populate the the tournament with some 'ghost fixtures' once the legs have been created.
The flash[:tournament] was working correctly before; using a tournament object, I have access to any AR validation errors as well as data that has been entered on the previous page to create the tournament.
TournamentController.rb
begin
<other code>
Tournament.transaction do
tournament.save!
Tournament.generate_legs tournament
Tournament.generate_ghost_fixtures tournament
end
flash[:notice] = "Tournament created!"
redirect_to :action => :index
rescue Exception => e
flash[:tournament] = tournament
redirect_to :action => :new, :notice => "There was an error!"
end
Tournament.rb
self.generate_ghost_fixtures(tournament)
<other code>
#Generate the ghost fixtures
#tournament_legs is a has_many association
tournament_legs_array = tournament.tournament_legs
tournament_legs_array.each do |leg|
number_of_fixtures = matches[leg.leg_code]
#For the first round of a 32 team tournament, this block will run 16 times to create the matches
number_of_fixtures.times do |n|
Fixture.creatse!(:tournament_leg_id => leg.id, :match_code => "#{leg.leg_code}-#{n+1}")
end
end
end
I can do nothing but speculate as to why my session variable is exceeding 4KB??
Is it possible that the tournament object I pass through the flash variable contains all the associations as well?
Here is the dump of my session once I get the error.
Hope this is enough info to help me out :)
Thanks
Session Dump
_csrf_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
flash: {:tournament=>#<Tournament id: nil, tournament_name: "asd", tournament_description: "asdasd", game_id: 1, number_of_teams: 16, start_date: "2011-04-30 00:00:00", tournament_style: "single elimination", tournament_status: "Drafting", active: true, created_at: "2011-04-30 10:07:28", updated_at: "2011-04-30 10:07:28">}
player_id: 1
session_id: "4e5119cbaee3d5d09111f49cf47aa8fa"
About dependencies, it is possible. Also save an ActiveRecord instance in the session is not a recommended aproach. You should save only the id. If you need it in all your requests use a before filter to retrieve it.
You can read more why is a bad idea at: http://asciicasts.com/episodes/13-dangers-of-model-in-session
The generally accepted and recommended approach is to not use a redirect on error, but a direct render instead. The standard "controller formula" is this:
def create
#tournament = Tournament.new(params[:tournament])
if #tournament.save
redirect ...
else
render 'new' # which will have access to the errors on the #tournament object and any other instance variable you may define
end
end
class Tournament < ActiveRecord::Base
before_create :set_up_legs
end
On successful saving, you can drop all instance variables (thereby wiping the in-memory state) and redirect to another page. On failure (or exception) you keep the object in memory and render a view template instead (typically the 'new' or 'edit' form page). If you're using standard Rails validation and error handling, then the object will have an errors array that you can just display.
I'd also recommend you use ActiveRecord associations which automatically give you transactions. If you push all this into the model, e.g. a "set_up_legs" method or something, then you can use ActiveRecord error handling. This is part of the "skinny controller, fat model" paradigm.
in session_store.rb, uncomment the last line with :active_record_store
Now restart the server
I would convert the exception to string before assigning it to flash[:tournament] with 'to_s'.
I had the same error and it seems assigning an exception object to a session variabla like flash means it takes the whole stack trace with it into the session. Try it, worked for me.