?
Solved

Daemon for background email job - Rails

Posted on 2009-12-23
13
Medium Priority
?
1,008 Views
Last Modified: 2013-11-13
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
Comment
Question by:depassion
  • 6
  • 4
  • 3
13 Comments
 
LVL 12

Expert Comment

by:cminear
ID: 26114739
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
 

Author Comment

by:depassion
ID: 26114919
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
 
LVL 12

Expert Comment

by:cminear
ID: 26115788
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
What does it mean to be "Always On"?

Is your cloud always on? With an Always On cloud you won't have to worry about downtime for maintenance or software application code updates, ensuring that your bottom line isn't affected.

 

Author Comment

by:depassion
ID: 26118233
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
 

Author Comment

by:depassion
ID: 26118709
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
 
LVL 12

Accepted Solution

by:
cminear earned 1000 total points
ID: 26123488
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
 
LVL 10

Expert Comment

by:Andrew Doades
ID: 26124014
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
 

Author Comment

by:depassion
ID: 26127915
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
 
LVL 10

Assisted Solution

by:Andrew Doades
Andrew Doades earned 1000 total points
ID: 26128081
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
 
LVL 10

Expert Comment

by:Andrew Doades
ID: 26128085
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
 
LVL 10

Expert Comment

by:Andrew Doades
ID: 26128086
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
 

Author Comment

by:depassion
ID: 26133338
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
 

Author Closing Comment

by:depassion
ID: 31669541
Thanks guys.
0

Featured Post

Keep up with what's happening at Experts Exchange!

Sign up to receive Decoded, a new monthly digest with product updates, feature release info, continuing education opportunities, and more.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Article by: narshlob
If you've ever programmed in Ruby and have come across either a proc or a lambda, you might have been wondering what the difference is between the two and when you would use one over the other. This article will try to explain the difference between…
Recently I spent hours debugging an issue in a Rails project where ActiveRecord was causing MySQL errors trying to create a User object of a class at the top level of a Single Table Inheritance model structure.  It turns out `.create` behaves differ…
Exchange organizations may use the Journaling Agent of the Transport Service to archive messages going through Exchange. However, if the Transport Service is integrated with some email content management application (such as an anti-spam), the admin…
Despite its rising prevalence in the business world, "the cloud" is still misunderstood. Some companies still believe common misconceptions about lack of security in cloud solutions and many misuses of cloud storage options still occur every day. …
Suggested Courses
Course of the Month14 days, 23 hours left to enroll

840 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question