Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
afn committed Jun 6, 2014
1 parent 63f5425 commit 1b2d8ca
Show file tree
Hide file tree
Showing 13 changed files with 636 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -32,3 +32,7 @@ build/

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc

# ignore sqlite database
db/*.sqlite3
log/*
2 changes: 2 additions & 0 deletions Gemfile
@@ -0,0 +1,2 @@
source 'https://rubygems.org/'
gemspec
126 changes: 126 additions & 0 deletions Gemfile.lock
@@ -0,0 +1,126 @@
PATH
remote: .
specs:
delayed_job_recurring (0.1)
delayed_job (>= 4.0)
delayed_job_active_record (>= 4.0)

GEM
remote: https://rubygems.org/
specs:
actionmailer (4.1.1)
actionpack (= 4.1.1)
actionview (= 4.1.1)
mail (~> 2.5.4)
actionpack (4.1.1)
actionview (= 4.1.1)
activesupport (= 4.1.1)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionview (4.1.1)
activesupport (= 4.1.1)
builder (~> 3.1)
erubis (~> 2.7.0)
activemodel (4.1.1)
activesupport (= 4.1.1)
builder (~> 3.1)
activerecord (4.1.1)
activemodel (= 4.1.1)
activesupport (= 4.1.1)
arel (~> 5.0.0)
activesupport (4.1.1)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
arel (5.0.1.20140414130214)
builder (3.2.2)
database_cleaner (1.3.0)
delayed_job (4.0.1)
activesupport (>= 3.0, < 4.2)
delayed_job_active_record (4.0.1)
activerecord (>= 3.0, < 4.2)
delayed_job (>= 3.0, < 4.1)
diff-lcs (1.2.5)
erubis (2.7.0)
hike (1.2.3)
i18n (0.6.9)
json (1.8.1)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.25.1)
minitest (5.3.4)
multi_json (1.10.0)
polyglot (0.3.4)
rack (1.5.2)
rack-test (0.6.2)
rack (>= 1.0)
rails (4.1.1)
actionmailer (= 4.1.1)
actionpack (= 4.1.1)
actionview (= 4.1.1)
activemodel (= 4.1.1)
activerecord (= 4.1.1)
activesupport (= 4.1.1)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.1)
sprockets-rails (~> 2.0)
railties (4.1.1)
actionpack (= 4.1.1)
activesupport (= 4.1.1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (10.3.2)
rspec (3.0.0)
rspec-core (~> 3.0.0)
rspec-expectations (~> 3.0.0)
rspec-mocks (~> 3.0.0)
rspec-core (3.0.0)
rspec-support (~> 3.0.0)
rspec-expectations (3.0.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.0.0)
rspec-mocks (3.0.0)
rspec-support (~> 3.0.0)
rspec-rails (3.0.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.0.0)
rspec-expectations (~> 3.0.0)
rspec-mocks (~> 3.0.0)
rspec-support (~> 3.0.0)
rspec-support (3.0.0)
sprockets (2.11.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.1.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (~> 2.8)
sqlite3 (1.3.9)
thor (0.19.1)
thread_safe (0.3.4)
tilt (1.4.1)
timecop (0.7.1)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (1.2.1)
thread_safe (~> 0.1)

PLATFORMS
ruby

DEPENDENCIES
database_cleaner (~> 1.3)
delayed_job_recurring!
rails
rspec (= 3.0.0)
rspec-rails (= 3.0.1)
sqlite3 (~> 1.3)
timecop (~> 0.7.1)
11 changes: 11 additions & 0 deletions config/application.rb
@@ -0,0 +1,11 @@
require File.expand_path('../boot', __FILE__)

require 'rails/all'

Bundler.require(:default, Rails.env)

module DelayedJobRecurring
class Application < Rails::Application
config.eager_load = false
end
end
6 changes: 6 additions & 0 deletions config/boot.rb
@@ -0,0 +1,6 @@
require 'rubygems'

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5 changes: 5 additions & 0 deletions config/database.yml
@@ -0,0 +1,5 @@
test:
adapter: sqlite3
database: db/test.sqlite3
pool: 5
timeout: 5000
5 changes: 5 additions & 0 deletions config/environment.rb
@@ -0,0 +1,5 @@
# Load the rails application
require File.expand_path('../application', __FILE__)

# Initialize the rails application
DelayedJobRecurring::Application.initialize!
30 changes: 30 additions & 0 deletions db/schema.rb
@@ -0,0 +1,30 @@
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20140530220208) do
create_table "delayed_jobs", force: true do |t|
t.integer "priority", default: 0
t.integer "attempts", default: 0
t.text "handler"
t.text "last_error"
t.datetime "run_at"
t.datetime "locked_at"
t.datetime "failed_at"
t.string "locked_by"
t.string "queue"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
end
25 changes: 25 additions & 0 deletions delayed_job_recurring.gemspec
@@ -0,0 +1,25 @@
Gem::Specification.new do |s|
s.name = 'delayed_job_recurring'
s.version = '0.1'
s.date = '2014-06-03'
s.summary = "Recurring jobs for delayed_job"
s.description = "Extends delayed_job to support recurring jobs, including timezone support"
s.authors = ["Tony Novak"]
s.email = 'engineering@amitree.com'
s.files = Dir.glob("{bin,lib}/**/*") + %w(LICENSE README.md)

s.homepage = 'https://github.com/amitree/delayed_job_recurring'
s.license = 'MIT'

s.required_ruby_version = '~> 2.0'

s.add_development_dependency 'rails'
s.add_development_dependency 'rspec', '3.0.0'
s.add_development_dependency 'rspec-rails', '3.0.1'
s.add_development_dependency 'sqlite3', '~> 1.3'
s.add_development_dependency 'database_cleaner', '~> 1.3'
s.add_development_dependency 'timecop', '~> 0.7.1'

s.add_runtime_dependency 'delayed_job', '>= 4.0'
s.add_runtime_dependency 'delayed_job_active_record', '>= 4.0'
end
172 changes: 172 additions & 0 deletions lib/delayed/recurring_job.rb
@@ -0,0 +1,172 @@
#
# Delayed::RecurringJob
#
# Inspired by https://gist.github.com/ginjo/3688965
#
module Delayed
module RecurringJob
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
@@logger = Delayed::Worker.logger
cattr_reader :logger
end
end

def failure
schedule!
end

def success
schedule!
end

# Schedule this "repeating" job
def schedule! options = {}
@schedule_options = options.reverse_merge(@schedule_options || {}).reverse_merge(
run_at: self.class.run_at,
timezone: self.class.timezone,
run_interval: serialize_duration(self.class.run_every)
)

enqueue_opts = { priority: 0, run_at: next_run_time }

if Gem.loaded_specs['delayed_job'].version.to_s.first.to_i < 3
Delayed::Job.enqueue self, enqueue_opts[:priority], enqueue_opts[:run_at]
else
Delayed::Job.enqueue self, enqueue_opts
end
end

def next_run_time
times = @schedule_options[:run_at]
times = [times] unless times.is_a? Array
times = times.map{|time| time.in_time_zone @schedule_options[:timezone]} if @schedule_options[:timezone]

interval = deserialize_duration(@schedule_options[:run_interval])

until next_time = next_future_time(times)
times.map!{ |time| time + interval }
end

# Update @schedule_options to avoid growing number of calculations each time
@schedule_options[:run_at] = times

next_time
end

private
# We don't want the run_interval to be serialized as a number of seconds.
# 1.day is not the same as 86400 (not all days are 86400 seconds long!)
def serialize_duration(duration)
case duration
when ActiveSupport::Duration
{value: duration.value, parts: duration.parts}
else
duration
end
end

def deserialize_duration(serialized)
case serialized
when Hash
ActiveSupport::Duration.new(serialized[:value], serialized[:parts])
else
serialized
end
end

def next_future_time(times)
times.select{|time| time > Time.now}.min
end

module ClassMethods
def run_at(time = nil)
if time.nil?
@run_at || run_every.from_now
else
@run_at = time
end
end

def run_every(interval = nil)
if interval.nil?
@run_interval || 1.hour
else
@run_interval = interval
end
end

def timezone(zone = nil)
if zone.nil?
@tz
else
@tz = zone
end
end

# Show all jobs for this schedule
def jobs
::Delayed::Job.where("(handler LIKE ?) OR (handler LIKE ?)", "--- !ruby/object:#{name} %", "--- !ruby/object:#{name}\n%")
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(options = {})
schedule!(options) if Delayed::Worker.delay_jobs && !scheduled?
end

def schedule!(options = {})
new.schedule!(options)
end

def scheduled?
jobs.count > 0
end

end # ClassMethods
end # RecurringJob

module Task
# Creates a new class wrapper around a block of code to be scheduled.
def self.new(name, options, &block)
task_class = Class.new
task_class.class_eval do
include Delayed::RecurringJob

def display_name
self.class.name
end

def perform
block.call
end
end

Object.const_set(name, task_class) if name
task_class.schedule(options)
return task_class
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', run_every: 10.minutes, run_at: 1.minute.from_now){do_some_stuff_here}
# or
# Delayed::Task.schedule(run_every: 10.minutes, run_at: 1.minute.from_now){do_some_stuff_here}
def self.schedule(name_or_options={}, options={}, &block)
case name_or_options
when Hash
name, options = nil, name_or_options
else
name = name_or_options
end

self.new name, options, &block
end
end # Task
end # Delayed

0 comments on commit 1b2d8ca

Please sign in to comment.