Model fails in validation but is created - ruby-on-rails

I'm facing an issue for a long time now. The code I have is the following:
class BrokenModel < ActiveRecord::Base
validates_with BrokenValidator
has_many :association_name
end
class BrokenValidator < ActiveModel::Validator
def validate record
#record = record
check_alerted
end
private
def check_alerted
return if #record.association_name.to_a.empty?
alerted = <test for alerted>
if alerted
#record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{#record.errors[:base]}"
end
end
worker.rb
[...]
BrokenModel.create(association_name: [model1, model2])
[...]
In my logs for the last print is shows that the validation passed only once, but I have actually multiple entries created for this model with association_name present.
My environment is running this in multiple threads and multiple cores, but as the entries are created minutes away from each other, it is not a concurrency issue, unless an exception in a separated thread is affecting the model creation.
Just for curiosity sake, this is running in a Sidekiq worker.
Edit
So I noticed in my logs, that it might be a concurrency issue. So here is what is happening:
instance 1 validation: alerted recently: failed (It was alerted recently)
instance 2 validation: alerted recently: passed
instance 2 validation: other validation: failed (Other validation)
instance 2 creation errors: It was alerted recently + Other validation
instance 1 creation errors: None
Any clue if there is any kind of thread unsafety in ActiveModel::Validator or the #record might be overwritten/shared by other threads?

Adding errors to a record does NOT make it invalid. In fact when the model is validated before save, all previous errors including the one you're adding in your code are erased.
Do this validation in the model... Not in the worker.
validate :check_alerted
def check_alerted
return if association_name.to_a.empty?
alerted = test
if <test for alerted>
errors.add(:base, "It was alerted recently")
end
end

Related

Rails Has-Many Relationship: Create not saving records into the database?

If I were to assume the models:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
When I'm trying to run User.first.posts.create [attributes], the model gets created, but its id is nil and is not saved in the database. Could anyone explain why? I thought this sort of behaviour was expected from #new, not from #create.
Your expectations are wrong.
Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is returned whether the object was saved successfully to the database or not.
The implementaton is actually dead simple:
def create(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
object = new(attributes, &block)
object.save
object
end
end
So for example when you do:
post = Post.create(title: 'Not a valid post')
This will always return an instance of post. If you then inspect the object you can see that its not persisted and the errors object will tell you why:
post.valid? # false
post.persisted? # false
post.errors.full_messages # ["User can't be blank."]
If you create the record off an association you're also mutating it:
user = User.first
post = user.posts.create(attributes)
user.posts.to_a.include?(post) # true
This might seem like undesirable behavior but is needed for nested attributes to work properly.
Use create! (which raises an error if the record is invalid) instead in non-interactive contexts (like seed files) where creating a record should not be expected to fail. Don't use it your controllers where is invalid input is not an exceptional event - and where its just a crutch akin to throwing a bunch of log or puts statements all over the code. Write tests that cover invalid and valid input instead.
If you really need to use a debugger like byebug or pry to step into the controller action and inspect the model instance.

Best practices for sending many emails at the same time or in a loop in Rails 6?

I have a rake task that loops through bookings and sends an email for each one using .deliver method (which I got from here (which I'm conscious is now 7 years old).
The problem is, sometimes some of the emails don't get sent. Here is my code
# Select bookings starting soon
bookings = Booking.where('start_time < ?', 24.hours.since)
# Email a reminder
bookings.each do |booking|
customer = booking.customer
CustomerMailer.reminder_24h(customer, booking).deliver
end
Since the loop is in a rake task, I don't think there's any value in calling .deliver_later, so I just use .deliver like in the rails cast
I am curious to know if there are best practices that can help using Action Mailer, for example should there be a sleep 2 between each email? Or should I always use .deliver_later to relieve the load on the server? Are there any other rails-related reasons that my code may not work (or, worse, I am using any anti patterns that I should refactor?)
TL;DR why would emails sent in a loop like in the code above occasionally fail to send
No an answer, but some advice from another forum.
Sending emails is a process that is filled with potential failures. It is always a good idea to do it in a background job that can be re-tried in case of intermittent errors like networks etc. and also skipped due to faulty addresses.
Here is a sketch of what may work:
# Reminder process rake task
namespace :bookings do
desc "Deliver reminders to upcoming bookings"
task remind_upcoming: :environment do
EnqueueUpcomingBookingReminders.call(UpcomingBookingRemindersQuery.call)
end
end
class EnqueueUpcomingBookingReminders
def self.call(bookings_scope)
booking_communication_attrs =
bookings_scope
.pluck(:id)
.map { |id| {booking_id: id, type: "reminder"} }
communications_result =
BookingCommunication.insert_all(booking_communication_attrs, unique_by: %i[booking_id type])
# Email a reminder
communications_result.rows.flatten.each do |communication_id|
DeliverBookingCommunicationJob.perform_later(communication_id)
end
end
end
class UpcomingBookingRemindersQuery
def self.call(scope: Booking)
Booking
.upcoming_this_day
.left_outer_joins(:communications)
.merge(BookingCommunication.reminder)
.where(communications: {id: nil})
end
end
class Booking
has_many :communications, class_name: "BookingCommunication"
def self.upcoming_this_day
where(starts_at:, (Time.current..24.hours.from_now))
end
end
class BookingCommunication
belongs_to :booking
enum step: {confirmation: "confirmation", reminder: "reminder"} # combination of this and the booking id should be unique
enum status: {pending: "pending", delivered: "delivered", canceled: "canceled", failed: "failed"} # should default to pending at database layer
end
class DeliverBookingCommunicationJob < ApplicationJob
def perform(communication_id)
communication = BookingCommunication.find_by(communication_id)
# Guard against state that invalidates us running this job
return unless communication
return unless communication.pending?
return communication.canceled! if communication.booking.canceled? # This should probably live in the cancel booking process
booking = communication.booking
mailer = CustomerMailer.with(customer: booking.customer, booking: booking)
case communication.step
when "reminder"
mailer.reminder_24h.deliver_now
else
# log unknown communication step, send to error tracking but dont raise since we do not want job to run again
end
communication.delivered!
rescue SomeEmailRelatedError => err
communication.failed!
# deliver err to error tracking service
end
end

Sidekiq not finding records for Rails Active Job

Jobs are queued on after a user is created like so in the model
user.rb
after_create_commit :profile_photo_job
def profile_photo_job
message = "Add a profile photo"
ReminderJob.set(wait: 1800).perform_later(self.id.to_s, message)
end
reminder_job.rb
class ReminderJob < ApplicationJob
queue_as :default
def perform(user_id, message)
if user_id
user = User.find(user_id)
end
##sending message notification here
end
end
However, it often throws the following error inside my sidekiq console
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=7749
Processor: User-MacBook-Pro.local:*****
This error happens in production.
I ran into this issue with workers returning the error:
ActiveRecord::RecordNotFound: Couldn't find User with 'id'
I tried a bunch of things and realised I had 2 separate Rails apps (with different databases) reading from the same queue. Silly error, but once I changed the queue name for one of the apps, I never saw this again.
Leaving this here if someone hasn't checked this 🤦‍♂️
In User.rb
after_commit :profile_photo_job, on: :create
def profile_photo_job
message = "Add a profile photo"
ReminderJob.set(wait: 1800).perform_later(self.id.to_s, message)
end

Rails methods not initialized in time for worker

Earlier, I had posted this question – and thought it was resolved:
Rails background worker always fails first time, works second
However, after continuing with tests and development, the error is back again, but in a slightly different way.
I'm using Sidekiq (with Rails 3.2.8, Ruby 1.9.3) to run background processes, after_save. Below is the code for my model, worker, and controller.
Model:
class Post < ActiveRecord::Base
attr_accessible :description,
:name,
:key
after_save :process
def process
ProcessWorker.perform_async(id, key) if key.present?
true
end
def secure_url
key.match(/(.*\/)+(.*$)/)[1]
end
def nonsecure_url
key.gsub('https', 'http')
end
end
Worker:
class ProcessWorker
include Sidekiq::Worker
def perform(id, key)
post = Post.find(id)
puts post.nonsecure_url
end
end
(Updated) Controller:
def create
#user = current_user
#post = #user.posts.create(params[:post])
render nothing: true
end
Whenever jobs are first dispatched, no matter the method, they fail initially:
undefined method `gsub' for nil:NilClass
Then, they always succeed on the first retry.
I've come across the following github issue, that appears to be resolved – relating to this same issue:
https://github.com/mperham/sidekiq/issues/331
Here, people are saying that if they create initializers to initialize the ActiveRecord methods on the model, that it resolves their issue.
To accomplish this, I've tried creating an initializer in lib/initializers called sidekiq.rb, with the following, simply to initialize the methods on the Post model:
Post.first
Now, the first job created completes successfully the first time. This is good. However, a second job created fails the first time – and completes upon retry... putting me right back to where I started.
This is really blowing my mind – has anyone had the same issue? Any help is appreciated.
Change your model callback from after_save to after_commit for the create action. Sometimes, sidekiq can initialize your worker before the model actually finishes saving to the database.
after_commit :process, :on => :create

How to save something to the database after failed ActiveRecord validations?

Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:
class MyModel < ActiveRecord::Base
validate :something
def something
# test
errors.add(:data, "bug!!")
end
end
I also have a model looking like this:
class MyModelLog < ActiveRecord::Base
def self.log_something
self.create(:log => "something happened")
end
end
In order to log I tried to :
Add MyModelLog.log_something in the something method of MyModel
Call MyModelLog.log_something on the after_validation callback of MyModel
In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.
What are my options?
Nested transactions do seem to work in MySQL.
Here is what I tried on a freshly generated rails (with MySQL) project:
./script/generate model Event title:string --skip-timestamps --skip-fixture
./script/generate model EventLog error_message:text --skip-fixture
class Event < ActiveRecord::Base
validates_presence_of :title
after_validation_on_create :log_errors
def log_errors
EventLog.log_error(self) if errors.on(:title).present?
end
end
class EventLog < ActiveRecord::Base
def self.log_error(event)
connection.execute('BEGIN') # If I do transaction do then it doesn't work.
create :error_message => event.errors.on(:title)
connection.execute('COMMIT')
end
end
# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []
Maybe I have over simplified it, or missing some point.
Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...
I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.
It looks something like this:
def something
error = nil
ActiveRecord::Base.transaction do
begin
# place codez here
rescue ActiveRecord::Rollback => e
error = e.message
raise ActiveRecord::Rollback
end
end
MyModelLog.log_something(error) unless error.nil?
end
By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.
I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.
I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).
What do you think?
MyModelLog.log_something should be done using a different connection.
You can make MyModelLog model always use a different connection by using establish_connection.
class MyModelLog < ActiveRecord::Base
establish_connection Rails.env # Use different connection
def self.log_something
self.create(:log => "something happened")
end
end
Not sure if this is the right way to do logging!!
You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.

Resources