• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 1022
  • Last Modified:

Daemon for background email job - Rails

Trying to automate sending email subscription out to my users with a daemon. Daemon seems to start and stop ok, but nothing is being delivered.

#daemon mailer.rb
#!/usr/bin/env ruby

# You might want to change this
ENV["RAILS_ENV"] ||= "production"

require File.dirname(__FILE__) + "/../../config/environment"

$running = true
Signal.trap("TERM") do
  $running = false
end

while($running) do
  subscription = Subscription.next_for_delivery
  if subscription
    subscription.deliver
    else
      sleep 30
    end
end

#subscription.rb
class Subscription < ActiveRecord::Base
  has_and_belongs_to_many :users
 
  def self.next_for_delivery
    Subscription.first(:conditions => ["delivered_at IS NULL AND schedule_delivery <= ?", Time.now.utc], :order => "schedule_delivery")
  end
 
def deliver(recipients, subscription, user, subject, lotto_numbers)
  UserMailer.deliver_lotto_saturday_subscription(recipients, subscription, user, subject, lotto_numbers)
  update_attribute(:delivered_at, Time.now)
end

end

#subscriptions_controller
class SubscriptionsController < ApplicationController
 
  def deliver
    @subscription = Subscription.find(params[:id])
    @users = @subscription.users
    @subject = @subscription.subject
    for user in @users
      @lotto_numbers = (1..49).to_a.sort{rand() - 0.5 } [0..5]
      @recipients = user.email
      @subscription.deliver(@recipients, @subscription, @user, @subject, @lotto_numbers)
    end
    flash[:notice] = "Delivered Subscription"
    redirect_to subscriptions_url
  end
0
depassion
Asked:
depassion
  • 6
  • 4
  • 3
2 Solutions
 
cminearCommented:
Based on what you've given here, you are already delivering the e-mail in the SubscriptionsController's deliver method.  For each user for the subscription, you perform '@subscription.deliver', which in turn sets the 'delivered_at' attribute.  So even if you were to set the 'schedule_delivery' attribute somewhere (which I don't see in the code you give), 'delivered_at' would not be NULL, so the daemon wouldn't find the subscription to send e-mail for anyways.

If you want to go down this road, you should replace the "for user in @users ... end" code in the SubscriptionsController's deliver method to just update the 'schedule_delivery' attribute.  However, you would need to fix the 'subscription.deliver' call in the daemon, as the 'deliver' method for the Subscription model class currently requires 5 arguments, and you are passing none.

(The follow-on question is, why not use the "deliver immediately" model you currently seem to have?  What problem are you having that this new approach would solve?)
0
 
depassionAuthor Commented:
Hi cminear, thanks for the comment.

Yes i am currently able to send via a "deliver" link, but the plan is to automate the task but still have the ability to change the delivery date fairly easily.

I'll change the conditions to deliver if delivered at is before scheduled delivery:
["delivered_at <= schedule_delivery AND schedule_delivery <= ?", Time.now.utc]

i have commented out the deliver call, but i need to keep the other code in the block there because i need to generate unique random numbers for every user.
def deliver
    @subscription = Subscription.find(params[:id])
    @users = @subscription.users
    @subject = @subscription.subject
    for user in @users
      @lotto_numbers = (1..49).to_a.sort{rand() - 0.5 } [0..5]
      @recipients = user.email
      #@subscription.deliver(@recipients, @subscription, @user, @subject, @lotto_numbers)  
    end
    flash[:notice] = "Delivered Subscription"
    redirect_to subscriptions_url
  end

i have changed the daemon to pass the arguments:
while($running) do
  subscription = Subscription.next_for_delivery
  if subscription
    subscription.deliver(recipients, subscription, user, subject, lotto_numbers)
    else
      sleep 30
    end
0
 
cminearCommented:
Some questions and statements:
- Where is 'schedule_delivery' being set?  If that is never set, then 'Subscription.next_for_delivery' will still never find any entries.  (Any comparison with a NULL is false.)

- You say "i need to keep the other code in the block there because i need to generate unique random numbers for every user."  However, in the 'deliver' method as you present it, I am not seeing that @lotto_numbers would be saved to the database.  You are setting variables which are then effectively dropped on the floor.

- You are now passing arguments to 'subscription.deliver' in your daemon.  But you never assign values to 4 of the variables you use: recipients, user, subject, and lotto_numbers.  So these would be nil and thus the e-mail (if it is sent) would likely not have what you were expecting in it.  And you don't need to pass the subscription as an argument to the 'deliver' method of a subscription, unless is it always different than the subscription being used to call it.  A given 'subscription' instance object will already known everything about itself, so it's unnecessary to also supply it as an argument.

The lotto numbers can be saved, assuming your schema can handle it.  (I am curious what that is, if you currently have that set up.)

0
Cloud Class® Course: Microsoft Azure 2017

Azure has a changed a lot since it was originally introduce by adding new services and features. Do you know everything you need to about Azure? This course will teach you about the Azure App Service, monitoring and application insights, DevOps, and Team Services.

 
depassionAuthor Commented:
The application is a lottery reminder service where users sign-up to receive certain random numbers at certain date times. So lets say a user signs up for the "lotto subscription", every Saturday morning they will receive 6 random numbers.

@lotto_numbers is not saved to the database, i don't need to save it, i just need the deliver action to generate the random numbers and email those numbers to every user, i don't need a record. The variable @lotto_numbers is recalled in the email body template:

#lotto_saturday_subscription.rb
Your numbers for the National Lottery UK Lotto Saturday Draw are <%= "#{@lotto_numbers.sort.join(' ')}" %>
Sent on <%= Time.now %

Schedule_delivery is an attribute of the Subscription model, it is set by the admin.
ActiveRecord::Schema.define(:version => 20091223174753) do

#schema.rb
  create_table "subscriptions", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "subject"
    t.text     "content"
    t.datetime "schedule_delivery"
    t.datetime "delivered_at"
  end

  create_table "subscriptions_users", :id => false, :force => true do |t|
    t.integer  "subscription_id"
    t.integer  "user_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "users", :force => true do |t|
    t.string   "name"
    t.string   "email"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

I am successfully sending the emails with the correct content via a link, but like i said i just want to automate the deliver action to take place when specified by the schedule_delivery attribute.

Your right i don't assign anything to @user and @subscription but the others are used in the email:

#user_mailer.rb
class UserMailer < ActionMailer::Base
def lotto_saturday_subscription(to_addresses, subject, lotto_numbers)
  recipients   to_addresses
  from         "no-reply@lottomail.net"
  subject      subject
  body         :lotto_numbers => lotto_numbers
end  
end


0
 
depassionAuthor Commented:
Actually the schedule_delivery should not be a datetime it should be a recurring date and time. So lets say i want to sent the lotto subscription out every saturday at 6am UTC.

do you know how to call the action on a day and time recurring every week?
#subscriptions_controller
def deliver
    @subscription = Subscription.find(params[:id])
    @users = @subscription.users
    @subject = @subscription.subject
    for user in @users
      @lotto_numbers = (1..49).to_a.sort{rand() - 0.5 } [0..5]
      @recipients = user.email
      #call ths from the deamon
      #@subscription.deliver(@recipients, @subject, @lotto_numbers)   
    end
    flash[:notice] = "Delivered Subscription"
    redirect_to subscriptions_url
  end

#subscription.rb
def deliver(recipients, subject, lotto_numbers)
  UserMailer.deliver_lotto_saturday_subscription(recipients, subject, lotto_numbers)
  update_attribute(:delivered_at, Time.now)
end

Open in new window

0
 
cminearCommented:
If you are running on a Linux/Unix system, the best option is to configure a cron job.  Create a script (similar to your daemon) whose sole purpose is to send the e-mails at the time you want them sent.  Then configure a cron entry which runs the script when you want the script run.

(See this solution for more details on cron: http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/Q_24211076.html?sfQueryTermInfo=1+cron+window#a23832368
)

The script is run outside of the SubscriptionsController, so you can't rely on what is set in that deliver method unless it is saved to the database.  So if the lotto numbers are generated when needed, I'd suggest putting them into the subscription 'deliver' method.  Also, you would either want to change the 'next_for_delivery' method so that it returns all subscriptions which should be delivered, create an alternate method to perform that action, or modify the script code (assuming a modification of the daemon script) so that it iteratively runs 'next_for_delivery' to get all of them.
0
 
Andrew DoadesIT TechnicianCommented:
Have you both looked at Rufus Scheduler?

http://rufus.rubyforge.org/rufus-scheduler/

I think this will do what you want, and its so easy to setup and configure, all my rails apps use it.

Andrew
0
 
depassionAuthor Commented:
ok, i installed the rufus-scheduler gem and created the task_scheduler.rb file in config/initalizers. Any idea as how to call the deliver action properly?
#task_scheduler.rb 
require 'rubygems'
 require 'rufus/scheduler'
 
 scheduler = Rufus::Scheduler.start_new   
 
 scheduler.every("10s") do  
    Subscription.deliver(@recipients, @subject, @lotto_numbers)
 end

#subscription.rb
def deliver(recipients, subject, lotto_numbers)
  @subscription = Self.first
  @users = @subscription.users
  @subject = @subscription.subject
  for user in @users
    @lotto_numbers = (1..49).to_a.sort{rand() - 0.5 } [0..5]
    @recipients = user.email
    UserMailer.deliver_lotto_saturday_subscription(recipients, subject, lotto_numbers)
  end
  update_attribute(:delivered_at, Time.now)
end

Open in new window

0
 
Andrew DoadesIT TechnicianCommented:
OK.. what I usually do is create the file in libs.. this is an example from one of my projects that sends an email out using rufus.

What I have done is the emails.rb file is basically my ruby mailer model
then in rufus I basically construct the controller action
#lib/emails.rb

require 'action_mailer'

ActionMailer::Base.smtp_settings =
{
  :address => 'localhost',
  :domain  => 'DOMAIN'}

class MyMailer < ActionMailer::Base

 def alert(hardware)

   recipients  'me@domain.com'
   from        "you@domain.com"
   subject     'Contract Alert'
   body        "The hardware #{hardware.system_description}'s contract will expire in one month, #{hardware.contract_expiration}"
 end

 end


#RUFUS
require 'rubygems'
require 'rufus/scheduler'

rails_root = File.expand_path(File.join(File.dirname(__FILE__)))
RAILS_HOME = rails_root
RAILS_ENV="production"

require RAILS_HOME + '/config/environment'
require 'emails' #the lib file

scheduler = Rufus::Scheduler.start_new
scheduler.start

scheduler.schedule("30 4 * * *") do
        #...popping up every 1 day at 04:30...
        puts Time.now.to_s + "...sending the emails..."
        @hardware = Hardware.find(:all, :conditions => ['contract_expiration =?', Date.today - 90.days])
        @hardware.each do |hardware|
        MyMailer.deliver_alert(hardware)
        end
end

scheduler.join

Open in new window

0
 
Andrew DoadesIT TechnicianCommented:
I think you're 98% there.. hopefully if you take a look at how mine works above it will help you see how to construct it.

Andrew
0
 
Andrew DoadesIT TechnicianCommented:
I think you're 98% there.. hopefully if you take a look at how mine works above it will help you see how to construct it.

Andrew
0
 
depassionAuthor Commented:
SOLUTION!!

Summary: the task was to deliver an email to the subscribed users through a background job internal to the rails app.  I went with the rufus-sheduler gem http://rufus.rubyforge.org/rufus-scheduler/ really easy to set-up. I moved all the original controller logic into the task_scheduler.rb file.

 
#config/initializers/task_scheduler.rb

 require 'rubygems'
 require 'rufus/scheduler'
 require 'user_mailer'
 
 scheduler = Rufus::Scheduler.start_new   
 
 scheduler.every("30s") do  
  @subscription = Subscription.first
  @users = @subscription.users
  @subject = @subscription.subject
    for user in @users
      @lotto_numbers = (1..49).to_a.sort{rand() - 0.5 } [0..5]
      @recipients = user.email
      UserMailer.deliver_lotto_saturday_subscription(@recipients, @subject, @lotto_numbers)
      @subscription.update_attribute(:delivered_at, Time.now)
    end
end

Open in new window

0
 
depassionAuthor Commented:
Thanks guys.
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

Join & Write a Comment

Featured Post

Cloud Class® Course: Amazon Web Services - Basic

Are you thinking about creating an Amazon Web Services account for your business? Not sure where to start? In this course you’ll get an overview of the history of AWS and take a tour of their user interface.

  • 6
  • 4
  • 3
Tackle projects and never again get stuck behind a technical roadblock.
Join Now