Sending email through resque: object treated as hash - ruby-on-rails

I'm sending emails out through resque. All emails send properly except this one, which sends fine in development locally but fails on staging server.
It seems to view the 'Admin' object as a hash instead of treating it as an admin object. Any ideas?
account.rb
class Account < ActiveRecord::Base
after_commit :send_welcome_email
def send_welcome_email
SubscriptionNotifier.welcome(self).deliver
end
end
subscription_notifier.rb
class SubscriptionNotifier < ActionMailer::Base
def welcome(account)
#subscriber = account
mail(:to => account.admin.email, :subject => "Welcome!")
end
end
Resque error
SubscriptionNotifier Arguments
"welcome"
{"account"=>{"address_line1"=>nil, "address_line2"=>nil, "city"=>nil, "created_at"=>"2012-02-08T10:56:22-08:00", "currency"=>"United States Dollar (USD)", "deleted_at"=>nil, "description"=>nil, "email"=>"test2#test.com", "full_domain"=>"www.test.net", "id"=>3, "initial_plan"=>nil, "latitude"=>nil, "longitude"=>nil, "name"=>"macs", "phone"=>nil, "setup_steps_complete"=>0, "state"=>nil, "time_zone"=>"Pacific Time (US & Canada)", "updated_at"=>"2012-02-08T10:56:22-08:00", "website"=>nil, "zip"=>nil}}
Exception
NoMethodError
Error
undefined method `admin' for #<Hash:0x0000000585aa70>

I think you should just pass in the account ID into the queue and have the worker fetch the Account object when it does its perform method. That should lessen your Hash woes.

This is an old question, but still relevant. The answers here give workarounds, but don't describe why the problem arises or how to design jobs to avoid it.
Basically for Resque to persist jobs in Redis, the arguments need to be serialized so they can be saved. You can't save a Ruby object in a database (for example), so the arguments are serialized to JSON (which can be persisted). In your case, it's calling account.to_json and stores that as an argument to your job.
The likely reason this is an issue in staging but not in development, is your development Redis is only storing the jobs in memory (and therefore they don't need to be serialized). Staging is persisting them to disk or a database for example, so they have to be serialized.
To avoid this problem, arguments to jobs should be strings, numbers, or data structures that can be converted to json.

You will need to load the environment by running 'rake environment resque:work QUEUE='*' RAILS_ENV=staging'
that is unless you have
task "resque:setup" => :environment
defined in a resque.rake file.

Try this:
in account.rb
def send_welcome_email
SubscriptionNotifier.welcome(self.admin.email).deliver
end
in subscription_notifier.rb
def welcome(account_admin_email)
#subscriber = account
mail(:to => account_admin_email, :subject => "Welcome!")
end
I had the same error, apparently you are passing account as a hash and it has no email admin method. So just get the email in the send_welcome_email method and pass it as a parameter, instead to passing a hash and trying to access the email in the welcome method.
NOTE: for the #subscriber, you would need to pass the parameters, just like the email, you use in the email template for example self.admin.name in the model and #name = account_admin_name in the welcome method
Hope this helps.

Related

Sending email as a job on ruby on rails

Good day everyone,
I created a mailer to send an email to my client. As of right now im still testing it, but I couldn't make it to work. I've read redis, sidekiq, rails_mailer and still nothing. I can see that the mail is in the queue of sidekiq UI but I cant receive the email.
Here's the flow of my code.
User will check the text box on the view if they wanted to send an email to a client.
I a method will be triggered on the controller. Heres my code.
def send_workorder_message
if params.has_key?(:to_send_email)
WorkorderMessage::WorkorderMessageJob.perform_in(10.seconds, #curr_user, params[:message])
end
endv
then a workorder job is created. heres the code.
class WorkorderMessage::WorkorderMessageJob
# include SuckerPunch::Job
include Sidekiq::Worker
sidekiq_options queue: 'mailers'
def perform(user, message)
Spree::WorkorderMailer.workorder_send_to_email(user, message).deliver_now
# ActiveRecord::Base.connection_pool.with_connection do
# end
end
end
after that it will trigger the WorkorderMailer heres the code.
class WorkorderMailer < BaseMailer
def workorder_send_to_email(to_user, message)
ActiveRecord::Base.connection_pool.with_connection do
subject = "sample message mailer"
#message = message
#user = to_user
mail(
to: #user.email,
# 'reply-to': Spree::Store.current.support_address,
from: Spree::Store.current.support_address,
subject: subject
)
end
end
end
when I use the preview mailer I can see the UI working fine.
Also I've noticed that on sidekiq view I see this User Obj. I that normal?
According to the Sidekiq documentation, the arguments you pass must be primitives that cleanly serialize to JSON, and not full Ruby objects, like the user you are passing here:
Complex Ruby objects do not convert to JSON, by default it will
convert with to_s and look like #<Quote:0x0000000006e57288>. Even if
they did serialize correctly, what happens if your queue backs up and
that quote object changes in the meantime? Don't save state to
Sidekiq, save simple identifiers. Look up the objects once you
actually need them in your perform method.
The arguments you pass to perform_async must be composed of simple
JSON datatypes: string, integer, float, boolean, null(nil), array and
hash. This means you must not use ruby symbols as arguments. The
Sidekiq client API uses JSON.dump to send the data to Redis. The
Sidekiq server pulls that JSON data from Redis and uses JSON.load to
convert the data back into Ruby types to pass to your perform method.
Don't pass symbols, named parameters or complex Ruby objects (like
Date or Time!) as those will not survive the dump/load round trip
correctly.
I would suggest you change it to lookup the User by ID within the job, and only pass the ID instead of the entire user object.
# pass #curr_user.id instead of #curr_user
WorkorderMessage::WorkorderMessageJob.perform_in(10.seconds, #curr_user.id, params[:message])
# accept the ID instead of user here
def perform(user_id, message)
# get the user object here
user = User.find(user_id)
# send the mail
mail(
to: user.email,
#...
end

send object as parameter in resque

sorry for my newbie question.
I'm sending current_user helper that have User object type as resque job runner parameter like this
JobRunner.run LikerCommenterAnalyzerJob, current_user, session['basic_instagram_data']['access_token'], 505
but when I try to get current_user parameter in perform method of resque it changed to Hash type
def self.perform (current_user, access_token, archive_id)
account = current_user.InstagramAccount.where('access_token', access_token).first
end
and my error is
undefined method `InstagramAccount' for #<Hash:0x007f9e39c27458>
Basically, when you push a task to resque, it will store somewhere to run later (Redis for example). So it can't store your object, it just stores the description of your passed parameters, the Hash above is the description.
Then Resque pulls down the description to run it.
So the solution will be, put the information that you use to retrieve the object instead (this is best practice)
When pushing the task:
JobRunner.run LikerCommenterAnalyzerJob, current_user.id, session['basic_instagram_data']['access_token'], 505
When runing the task
def self.perform (user_id, access_token, archive_id)
user = User.find(user_id)
account = user.InstagramAccount.where('access_token', access_token).first
end

How do you return out of an ActionMailer::Base function?

I'm trying to implement an ActionMailer function that will send out a newsletter to a specific user. I want to make sure that the newsletter is only sent to subscribed users. I tried implementing it like so:
class UserMailer < ActionMailer::Base
def newsletter(user)
return unless user.subscribed # This still renders my mailer view
mail(:to => user.email, :subject => "Newsletter")
end
end
The problem is that the return unless user.subscribed line still appears to be rendering the mailer view and is still sent by the calling code (from a cron job):
task :cron => :environment do
User.where(:subscribed => true).each do |user|
UserMailer.newsletter(user).deliver
end
end
Note that I do have that subscription logic in my cron job as well for performance reasons (shouldn't have to iterate over ALL users, only those that are subscribed). However, it feels like the UserMailer class is the right place for this logic to exist (otherwise any other location that calls the newsletter method will need to check the subscribed flag as well.
The Mailer, IMHO, is the wrong place for this logic. The mailer should do nothing but format and send messages. The logic to decide whether or not to send should be within the calling block of code. It's not the right way, but something as simple as:
UserMailer.newsletter(user).deliver if user.subscribed?
Alternately, as you mentioned, you shouldn't have to iterate over all users, just the subscribed. So with a scope in the User model called subscribed:
User.subscribed.each do |user|
UserMailer.newsletter(user).deliver
end
This way you don't need to test on a per-user basis; only the subscribed users are included, and the logic is in the calling block, not in the mailer.

after_create callback not working in test but works in console

Testing in Rails has always been something of a mystery that I avoid if possible but I'm putting a production application together that people will pay for so I really need to test. This problem is driving me mad because the test fails but when I perform the same commands in the console (in test and development mode) it works fine.
user_test.rb
test "should update holidays booked after create"
user = users(:robin)
assert_equal user.holidays_booked_this_year, 4 # this passes
absence = user.absences.create(:from => "2011-12-02", :to => "2011-12-03", :category_id => 1, :employee_notes => "Secret") # this works
assert_equal user.holidays_booked_this_year, 5 # fails
end
absence.rb
after_create :update_holidays_booked
def update_holidays_booked
user = self.user
user.holidays_booked_this_year += self.days_used # the days used attribute is calculated using a before_create callback on the absence
user.save
end
My only thoughts are that it's something to do with updating the User model through a callback on the Absence model but, as I say, this works in the console.
Any advice would be appreciated.
Thanks
Robin
What are you using for your factory?
If you are using a database backed test then you need to reload the user in the test (because the user instance is not updated, the absence's user is updated and saved to the database), reloading the user would look like:
assert_equal user.reload.holidays_booked_this_year, 5
I would also guess that an absence needs to have a user, so you should use build instead of create so the foreign key for user is part of the "created" instance:
user.absences.build
First guess would be that in the console you are operating on a real user in the database whereas the test is a fixture. Have you tried this is in test?:
raise user.inspect
Look at the output and determine which user you are actually working with and what the holidays_booked_this_year attributes is.
(your test block also needs a "do" after the description)

How do I debug this 'no method' error in ruby on rails?

I get the following error:
Delayed::Job SomeMailJob# (NoMethodError) "undefined method `subject' for #<YAML::Object:0x2b0a191f4c78>"
This comes from the following code which references the SombMailJob above:
class SomeMailJob < Struct.new(:contact, :contact_email)
def perform
OutboundMailer.deliver_campaign_email(contact,contact_email)
end
end
Here is the mailer:
class OutboundMailer < Postage::Mailer
def campaign_email(contact,email)
subject email.subject
recipients contact.email
from 'me.com>'
sent_on Date.today
body :email => email
end
This is the cron task that invokes the mailer:
Contact.all.each do |contact|
email = contact.email_today #email_today is a contact method returning email object if <= today
unless contact.email_today == "none" || email.nil?
puts "contact info inside cron job"
puts contact.first_name
puts email.days
puts contact.date_entered
puts contact.colleagues
puts "substituted subject:"
puts email.substituted_subject(contact,contact.colleagues)
# create the Contact Email object that gets created and sent
contact_email = ContactEmail.new
contact_email.contact_id = contact.id
contact_email.email_id = email.id
contact_email.subject = email.substituted_subject(contact,contact.colleagues)
puts contact_email.subject
contact_email.date_sent = Date.today
contact_email.date_created = Date.today
contact_email.body = email.substituted_message(contact, contact.colleagues)
contact_email.status = "sent"
#Delayed::Job.enqueue OutboundMailer.deliver_campaign_email(contact,contact_email)
Delayed::Job.enqueue SomeMailJob.new(contact,contact_email)
contact_email.save #now save the record
Question: why am I getting this error? I don't even know what the object is because it is coming up with the code, so I can't really drill-down further to debug.
This worked for me. Seems to be the same issue you're having.
Rails Delayed Job & Library Class
When you enqueue a delayed job, it serializes the things involved (the class, the name of hte method you're calling on it, the arguments) into YAML so it can pull them out later when running the job and work with them.
It looks like in your case the email argument is not getting deserialized properly from YAML before .subject is called on it.
I've found that delayed_job tends to have trouble serializing/deserializing anything that's not a simple stored ActiveRecord object or primitive type (integer, string). I always try to set things up so my Job objects only take record IDs (integers), then in the perform method I find for the objects there and work with them. This would definitely avoid the trouble you're seeing.

Resources