i want to save this message five time from active jobs not from controller.
is there anyway to that ?
here message.save just returning true and its not saving the message in databas.
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
for i in 0..5
message.save!
ActionCable.server.broadcast 'chat', {message: render_message(message)}
end
end
private
def render_message(message)
MessagesController.render(
partial: 'message',
locals: {
message: message
}
)
end
end
this code is from model.
class Message < ApplicationRecord
belongs_to :user
after_create_commit {
MessageBroadcastJob.perform_later(self)
}
end
You are not calling your job in code anywhere. You are saving that after_create_commit mean after saving message to active record you want to run this job. But you are not doing it.
So you have to save at least one time, to run this job. That mean in your controller or anywhere in code at least save once message, this job will run when you have saved message onces, than it will save message. If you still have issue please check job is running properly? do you have any configuration in redis
I smell from your codes you are using cable, that mean you have to put following in your conversation channel file.
Message.create(message_params)
As soon as above code run than your job will also run. If above all is good than you have also issue in your job, you are saying that same message should be saved, so it won't save 5 message, you have to take parameters in it do following
m = Message.new
m.body = message
m.user_id = message.user_id
# setup other parameters required
m.save
ActionCable.server.broadcast 'chat', {message: render_message(message)}
Related
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
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
I'm using Rails 4 with Oracle 12c and I need to update the status of an User, and then use the new status in a validation for another model I also need to update:
class User
has_many :posts
def custom_update!(new_status)
relevant_posts = user.posts.active_or_something
ActiveRecord::Base.transaction do
update!(status: new_status)
relevant_posts.each { |post| post.update_stuff! }
end
end
end
class Post
belongs_to :user
validate :pesky_validation
def update_stuff!
# I can call this from other places, so I also need a transaction here
ActiveRecord::Base.transaction do
update!(some_stuff: 'Some Value')
end
end
def pesky_validation
if user.status == OLD_STATUS
errors.add(:base, 'Nope')
end
end
end
However, this is failing and I receive the validation error from pesky_validation, because the user inside Post doesn't have the updated status.
The problem is, when I first update the user, the already instantiated users inside the relevant_posts variable are not yet updated, and normally all I'd need to fix this was to call reload, however, maybe because I'm inside a transaction, this is not working, and pesky_validation is failing.
relevant_users.first.user.reload, for example, reloads the user to the same old status it had before the update, and I'm assuming it's because the transaction is not yet committed. How can I solve this and update all references to the new status?
I have a Post model (below) which has a callback method to modify the body attribute via a delayed job. If I remove "delay." and just execute #shorten_urls! instantly, it works fine. However, from the context of a delayed job, it does NOT save the updated body.
class Post < ActiveRecord::Base
after_create :shorten_urls
def shorten_urls
delay.shorten_urls!
end
def shorten_urls!
# this task might take a long time,
# but for this example i'll just change the body to something else
self.body = 'updated body'
save!
end
end
Strangely, the job is processed without any problems:
[Worker(host:dereks-imac.home pid:76666)] Post#shorten_urls! completed after 0.0021
[Worker(host:dereks-imac.home pid:76666)] 1 jobs processed at 161.7611 j/s, 0 failed ...
Yet, the body is not updated. Anyone know what I'm doing wrong?
-- EDIT --
Per Alex's suggestion, I've updated the code to look like this (but to no avail):
class Post < ActiveRecord::Base
after_create :shorten_urls
def self.shorten_urls!(post_id=nil)
post = Post.find(post_id)
post.body = 'it worked'
post.save!
end
def shorten_urls
Post.delay.shorten_urls!(self.id)
end
end
One of the reasons might be that self is not serialized correctly when you pass method to delay. Try making shorten_urls! a class method that takes record id and fetches it from DB.
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.