-
-
Save ginjo/3688965 to your computer and use it in GitHub Desktop.
# # # # # scheduled_job.rb - recurring schedules for delayed_job.rb # # # # # | |
# | |
# This file is version controlled at https://gist.github.com/ginjo/3688965 | |
# | |
# Forked from https://gist.github.com/kares/1024726 | |
# | |
# This is an enhanced version of the original scheduled_job.rb | |
# It was born out of the need to schedule a whole bunch of simple jobs. | |
# I started with the sample below and quickly found that I was repeating | |
# a lot of code. So I created the Delayed::Task pseudo-class that allows | |
# simple creation of class-wrappers surrounding the code to be | |
# scheduled. This gives you two easy ways to schedule any block of code. | |
# | |
# 1. Delayed::Task.schedule("MyTask", 30.minutes, Time.now){code_to_be_scheduled_starting_right_now} | |
# => Wraps the code in a class MyTask, to be run every 30 minutes, starting now. | |
# | |
# 2. MyTask = Delayed::Task.new(6.hours){code_to_be_scheduled} | |
# => Wraps the code in a class MyTask, to be run every 3.hours. | |
# MyTask.schedule(5.minutes.from_now) | |
# => start the schedule 5 minutes from now. | |
# | |
# MyTask.jobs # => show scheduled jobs | |
# MyTask.unschedule # => stop scheduled jobs | |
# | |
# | |
# I also added a couple of minor enhancements to the original ScheduledJob | |
# module. | |
# | |
# | |
# Instructions from the original Gist. | |
# | |
# Setup Your job the "plain-old" DJ (perform) way, include this module | |
# and Your handler will re-schedule itself every time it succeeds. | |
# | |
# Sample : | |
# | |
# class MyTask | |
# include Delayed::ScheduledJob | |
# | |
# run_every 1.day | |
# | |
# def display_name | |
# "MyTask" | |
# end | |
# | |
# def perform | |
# # code to run ... | |
# end | |
# | |
# end | |
# | |
# inspired by http://rifkifauzi.wordpress.com/2010/07/29/8/ | |
# | |
# Use: MyTask.schedule; MyTask.jobs | |
# | |
module Delayed | |
module ScheduledJob | |
def self.included(base) | |
base.extend(ClassMethods) | |
base.class_eval do | |
@@logger = Delayed::Worker.logger | |
cattr_reader :logger | |
end | |
end | |
def perform_with_schedule | |
perform_without_schedule | |
schedule! # only schedule if job did not raise | |
end | |
# Schedule this "repeating" job | |
def schedule!(run_at = nil) | |
run_at ||= self.class.run_at | |
if Gem.loaded_specs['delayed_job'].version.to_s.first.to_i < 3 | |
Delayed::Job.enqueue self, 0, run_at | |
else | |
Delayed::Job.enqueue self, :priority=>0, :run_at=>run_at | |
end | |
end | |
# Re-schedule this job instance | |
def reschedule! | |
schedule! Time.now | |
end | |
module ClassMethods | |
def method_added(name) | |
if name.to_sym == :perform && | |
! instance_methods(false).map(&:to_sym).include?(:perform_without_schedule) | |
alias_method_chain :perform, :schedule | |
end | |
end | |
def run_at | |
run_interval.from_now | |
end | |
def run_interval | |
@run_interval ||= 1.hour | |
end | |
def run_every(time) | |
@run_interval = time | |
end | |
# | |
# Show all jobs for this schedule | |
def jobs | |
if Rails::VERSION::MAJOR > 2 | |
Delayed::Job.where("handler LIKE ?", "%#{name}%") | |
else | |
Delayed::Job.find(:all, :conditions=>["handler LIKE ?", "%#{name}%"]) | |
end | |
end | |
# Remove all jobs for this schedule (Stop the schedule) | |
def unschedule | |
jobs.each{|j| j.destroy} | |
end | |
# Main interface to start this schedule (adds it to the jobs table). | |
# Pass in a time to run the first job (nil runs the first job at run_interval from now). | |
def schedule(run_at = nil) | |
schedule!(run_at) unless scheduled? | |
end | |
def schedule!(run_at = nil) | |
new.schedule!(run_at) | |
end | |
def scheduled? | |
jobs.count > 0 | |
end | |
end # ClassMethods | |
end # ScheduledJob | |
# Task is a pseudo-class for creating named classes that represent any block of code to be scheduled. | |
# | |
# MyTask = Delayed::Task.new(5.minutes){do-something-here-every-5-minutes} | |
# => creates a class MyTask that can be used to control the schedule of the encapsulated block. | |
# | |
# MyTask.schedule Time.now | |
# => adds MyTask to the jobs table, and run the first job at Time.now. | |
# | |
# MyTask = Delayed::Task.new(2.hours){|*args_for_manual_run| puts args[0].to_s} | |
# MyTask.run | |
# => "" | |
# | |
# MyTask.run "something" | |
# => "something" | |
# | |
module Task | |
# Creates a new class wrapper around a block of code to be scheduled. | |
def self.new(*args, &bloc) | |
duration = args[0] || 1.day | |
name = args[1] | |
start_at = args[2] | |
klas = Class.new | |
klas.class_eval do | |
include Delayed::ScheduledJob | |
@in_duration = duration | |
@in_bloc = bloc | |
def self.in_duration; @in_duration; end | |
def self.in_bloc; @in_bloc; end | |
def self.run(*args); @in_bloc.call(*args); end | |
def display_name | |
self.class.name | |
end | |
def perform | |
self.class.in_bloc.call | |
end | |
run_every duration | |
end | |
Object.const_set(name, klas) if name | |
klas.schedule(start_at) if start_at and name | |
return klas | |
end | |
# Schedule a block of code on-the-fly. | |
# This is a friendly wrapper for using Task.new without an explicit constant assignment. | |
# Delayed::Task.schedule('MyNewTask', 10.minutes, 1.minute.from_now){do_some_stuff_here} | |
def self.schedule(*args, &bloc) | |
self.new(args[1], args[0], args[2], &bloc) | |
end | |
end # Task | |
end # Delayed | |
# # # # # Control delayed_job workers with Upstart # # # # # | |
# # | |
# # Upstart config for Rails delayed_job worker. | |
# # This gives a simple way to start/stop/restart daemons on Linux. | |
# # This config defines an upstart control for a delayed_job worker (headless Rails instance). | |
# # | |
# # Place this config in a file in your project, | |
# # then symlink this file in /etc/init/ as myprojectworker.conf. | |
# # After installing new upstart script, run "initctl reload-configuration" | |
# # (shouldn't need it, but seems to need it when using symlinks). | |
# # Use: | |
# # sudo start/stop/restart myprojectworker | |
# | |
# description "delayed_job worker for myproject" | |
# author "[email protected]" | |
# | |
# start on (net-device-up | |
# and local-filesystems | |
# and started mysql | |
# and runlevel [2345]) | |
# stop on runlevel [016] | |
# | |
# respawn | |
# | |
# # Give up if restart occurs 10 times in 90 seconds. | |
# respawn limit 10 90 | |
# | |
# #env RAILS_RELATIVE_URL_ROOT=/dev | |
# #env RAILS_ENV=development | |
# env RAILS_ENV=production | |
# umask 007 | |
# | |
# # Default is 5 seconds | |
# kill timeout 60 | |
# | |
# chdir /home/admin/sites/myproject | |
# | |
# exec su admin -c '/usr/bin/env script/delayed_job run' | |
# # # # # Control delayed_job workers with Launchd # # # # # | |
# | |
# # Place this plist in ~/Library/LaunchDaemons/ as com.myproject.jobs.plist | |
# # It should automatically start your delayed_job worker and keep it running. | |
# # Manually start/stop with: | |
# # launchctl <load|unload> -w ~/Library/LaunchDaemons/com.myproject.jobs.plist | |
# | |
# <?xml version="1.0" encoding="UTF-8"?> | |
# <!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN | |
# http://www.apple.com/DTDs/PropertyList-1.0.dtd > | |
# <plist version="1.0"> | |
# <dict> | |
# <key>Label</key> | |
# <string>com.myproject.jobs</string> | |
# <key>WorkingDirectory</key> | |
# <string>/Users/admin/sites/myproject</string> | |
# <key>UserName</key> | |
# <string>admin</string> | |
# <key>ProgramArguments</key> | |
# <array> | |
# <string>/bin/bash</string> | |
# <string>-c</string> | |
# <string>script/delayed_job run</string> | |
# </array> | |
# <key>RunAtLoad</key> | |
# <true/> | |
# </dict> | |
# </plist> | |
# # # # # More ScheduledJob Examples # # # # # | |
# | |
# # You can load these lines along with (but after) the above modules. | |
# # | |
# # Schedule a proc. | |
# task1 = proc{`Date >> log/mindless_task.log`} | |
# Delayed::Task.schedule('MyTask1', 10.seconds, &task1) | |
# | |
# # Define a scheduled task and start the job, all in one line. | |
# Delayed::Task.schedule('MyTask2', 10.seconds, Time.now){`Date >> log/mindless_task.log`} | |
# | |
# # Define a scheduled task that you can load into your Rails apps, without actually starting the schdules. | |
# MyTask3 = Delayed::Task.new(10.seconds){` echo "MyTask3" >> log/mindless_task.log; Date >> log/mindless_task.log`} | |
# MyTask4 = Delayed::Task.new(10.seconds){` echo "MyTask4" >> log/mindless_task.log; Date >> log/mindless_task.log`} | |
# | |
# # Only start the schedules when the worker daemon loads. | |
# if $0[/delayed_job/i] | |
# MyTask3.schedule | |
# MyTask4.schedule(5.minutes.from_now) | |
# end | |
Thanks for the comment. Do you think there is a problem with this code that could cause a stack level too deep error, or was there something else going on with delayed_job?
To follow up my original gist, I've been using this code for several weeks now to manage a dozen or so scheduled jobs. The whole mess has been very stable and appears to be superior to my old cron/rake solutions.
Also, I've had great results using Ubuntu's upstart init daemon to manage delayed_job workers. Really easy to setup too: Put an upstart conf file somewhere in your rails project and symlink to it from the /etc/init/ directory. Then you can do this at the linux prompt: sudo start/stop/restart myjobworker. I'll replace the helpers in this gist with an example of an upstart conf file. The equivalent tool on Mac is launchd, but I haven't used that for delayed_job workers yet.
@ginjo It was a configuration problem in development. I wanted to have the jobs execute right away and so I used
Delayed::Worker.delay_jobs = Rails.env.production?
however the processes were trying to schedule/run immediately and caused an infinite loop. It just took me a while to figure out why it was happening.
@ginjo Thanks for your work on this! I've borrowed your code, extended it a bit, and packaged it into a gem. Check it out if you're interested: https://github.com/amitree/delayed_job_recurring
@shawnpyle Among other things, the gem addresses the infinite loop issue.
@afn Cool, I was just poking around to see the latest activity on delayed_job scheduling, thinking I might gem-ify this if no one else had yet. Thanks for picking it up (and fixing the loop issue), I'll check it out.
Also, just to comment on control of delayed_job workers and scheduled jobs, I've been using launchd on OSX and upstart on Ubuntu for a couple of years now, both with great success.
Thanks for posting this! Works great however I spent quite a bit of time trying to track down a "Stack too deep" error in my development environment. Come to find out, my delayed_job initializer contained the following to run delayed jobs right away:
Delayed::Worker.delay_jobs = Rails.env.production?
Re-enabling delayed jobs for my development environment resolved that.