Permalink
Browse files

Initial import

  • Loading branch information...
0 parents commit 0bd9db72585f19f5e4e8c2e89960c23324a98090 @betamatt betamatt committed May 25, 2011
@@ -0,0 +1,2 @@
+Gemfile.lock
+.DS_Store
@@ -0,0 +1,2 @@
+source :rubygems
+gemspec
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2005 Tobias Luetke
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND
+NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,14 @@
+# DelayedJob ActiveRecord Backend
+
+## Installation
+
+Add the gems to your Gemfile:
+
+ gem 'delayed_job'
+ gem 'delayed_job_active_record'
+
+Run `bundle install`
+
+If you're using Rails, run the generator to create the migration for the delayed_job table: `rails g delayed_job:active_record`
+
+That's it. Use [delayed_job as normal](http://github.com/collectiveidea/delayed_job).
@@ -0,0 +1,12 @@
+# -*- encoding: utf-8 -*-
+require 'rubygems'
+require 'bundler/setup'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core/rake_task'
+desc 'Run the specs'
+RSpec::Core::RakeTask.new do |r|
+ r.verbose = false
+end
+
+task :default => :spec
@@ -0,0 +1,24 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = 'delayed_job_active_record'
+ s.version = '0.2.0'
+ s.authors = ["Matt Griffin"]
+ s.summary = 'ActiveRecord backend for DelayedJob'
+ s.description = 'ActiveRecord backend for DelayedJob, originally authored by Tobias Luetke'
+ s.email = ['matt@griffinonline.org']
+ s.extra_rdoc_files = 'README.md'
+ s.files = Dir.glob('{contrib,lib,recipes,spec}/**/*') +
+ %w(LICENSE README.md)
+ s.homepage = 'http://github.com/betamatt/delayed_job_active_record'
+ s.rdoc_options = ["--main", "README.md", "--inline-source", "--line-numbers"]
+ s.require_paths = ["lib"]
+ s.test_files = Dir.glob('spec/**/*')
+
+ s.add_runtime_dependency 'delayed_job'
+ s.add_runtime_dependency 'activerecord', '~> 3.0'
+
+ s.add_development_dependency 'rspec', '~> 2.0'
+ s.add_development_dependency 'rake', '~> 0.8'
+ s.add_development_dependency 'sqlite3'
+end
@@ -0,0 +1,80 @@
+module Delayed
+ module Backend
+ module ActiveRecord
+ # A job object that is persisted to the database.
+ # Contains the work object as a YAML field.
+ class Job < ::ActiveRecord::Base
+ include Delayed::Backend::Base
+ set_table_name :delayed_jobs
+
+ before_save :set_default_run_at
+
+ scope :ready_to_run, lambda {|worker_name, max_run_time|
+ where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
+ }
+ scope :by_priority, order('priority ASC, run_at ASC')
+
+ def self.before_fork
+ ::ActiveRecord::Base.clear_all_connections!
+ end
+
+ def self.after_fork
+ ::ActiveRecord::Base.establish_connection
+ end
+
+ # When a worker is exiting, make sure we don't have any locked jobs.
+ def self.clear_locks!(worker_name)
+ update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
+ end
+
+ # Find a few candidate jobs to run (in case some immediately get locked by others).
+ def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
+ scope = self.ready_to_run(worker_name, max_run_time)
+ scope = scope.scoped(:conditions => ['priority >= ?', Worker.min_priority]) if Worker.min_priority
+ scope = scope.scoped(:conditions => ['priority <= ?', Worker.max_priority]) if Worker.max_priority
+
+ ::ActiveRecord::Base.silence do
+ scope.by_priority.all(:limit => limit)
+ end
+ end
+
+ # Lock this job for this worker.
+ # Returns true if we have the lock, false otherwise.
+ def lock_exclusively!(max_run_time, worker)
+ now = self.class.db_time_now
+ affected_rows = if locked_by != worker
+ # We don't own this job so we will update the locked_by name and the locked_at
+ self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and (locked_at is null or locked_at < ?) and (run_at <= ?)", id, (now - max_run_time.to_i), now])
+ else
+ # We already own this job, this may happen if the job queue crashes.
+ # Simply resume and update the locked_at
+ self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
+ end
+ if affected_rows == 1
+ self.locked_at = now
+ self.locked_by = worker
+ self.locked_at_will_change!
+ self.locked_by_will_change!
+ return true
+ else
+ return false
+ end
+ end
+
+ # Get the current time (GMT or local depending on DB)
+ # Note: This does not ping the DB to get the time, so all your clients
+ # must have syncronized clocks.
+ def self.db_time_now
+ if Time.zone
+ Time.zone.now
+ elsif ::ActiveRecord::Base.default_timezone == :utc
+ Time.now.utc
+ else
+ Time.now
+ end
+ end
+
+ end
+ end
+ end
+end
@@ -0,0 +1,13 @@
+class ActiveRecord::Base
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
+
+ def self.yaml_new(klass, tag, val)
+ klass.unscoped.find(val['attributes'][klass.primary_key])
+ rescue ActiveRecord::RecordNotFound
+ raise Delayed::DeserializationError
+ end
+
+ def to_yaml_properties
+ ['@attributes']
+ end
+end
@@ -0,0 +1,7 @@
+require 'delayed_job'
+require 'active_record'
+
+require 'delayed/backend/active_record'
+require 'delayed/serialization/active_record'
+
+Delayed::Worker.backend = :active_record
@@ -0,0 +1,17 @@
+require 'generators/delayed_job/delayed_job_generator'
+require 'rails/generators/migration'
+require 'rails/generators/active_record/migration'
+
+# Extend the DelayedJobGenerator so that it creates an AR migration
+module DelayedJob
+ class ActiveRecordGenerator < ::DelayedJobGenerator
+ include Rails::Generators::Migration
+ extend ActiveRecord::Generators::Migration
+
+ self.source_paths << File.join(File.dirname(__FILE__), 'templates')
+
+ def create_migration_file
+ migration_template 'migration.rb', 'db/migrate/create_delayed_jobs.rb'
+ end
+ end
+end
@@ -0,0 +1,21 @@
+class CreateDelayedJobs < ActiveRecord::Migration
+ def self.up
+ create_table :delayed_jobs, :force => true do |table|
+ table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue
+ table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
+ table.text :handler # YAML-encoded string of the object that will do work
+ table.text :last_error # reason for last failure (See Note below)
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
+ table.datetime :locked_at # Set when a client is working on this object
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
+ table.string :locked_by # Who is working on this object (if locked)
+ table.timestamps
+ end
+
+ add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
+ end
+
+ def self.down
+ drop_table :delayed_jobs
+ end
+end
@@ -0,0 +1,8 @@
+sqlite:
+ adapter: sqlite3
+ database: ":memory:"
+
+mysql:
+ adapter: mysql
+ database: delayed_job
+ uesrname: root
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'delayed/backend/active_record'
+
+describe Delayed::Backend::ActiveRecord::Job do
+ after do
+ Time.zone = nil
+ end
+
+ it_should_behave_like 'a delayed_job backend'
+
+ context "db_time_now" do
+ it "should return time in current time zone if set" do
+ Time.zone = 'Eastern Time (US & Canada)'
+ %w(EST EDT).should include(Delayed::Job.db_time_now.zone)
+ end
+
+ it "should return UTC time if that is the AR default" do
+ Time.zone = nil
+ ActiveRecord::Base.default_timezone = :utc
+ Delayed::Backend::ActiveRecord::Job.db_time_now.zone.should == 'UTC'
+ end
+
+ it "should return local time if that is the AR default" do
+ Time.zone = 'Central Time (US & Canada)'
+ ActiveRecord::Base.default_timezone = :local
+ %w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
+ end
+ end
+
+ describe "after_fork" do
+ it "should call reconnect on the connection" do
+ ActiveRecord::Base.should_receive(:establish_connection)
+ Delayed::Backend::ActiveRecord::Job.after_fork
+ end
+ end
+
+ describe "enqueue" do
+ it "should allow enqueue hook to modify job at DB level" do
+ later = described_class.db_time_now + 20.minutes
+ job = described_class.enqueue :payload_object => EnqueueJobMod.new
+ Delayed::Backend::ActiveRecord::Job.find(job.id).run_at.should be_within(1).of(later)
+ end
+ end
+end
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ActiveRecord do
+ it 'should load classes with non-default primary key' do
+ lambda {
+ YAML.load(Story.create.to_yaml)
+ }.should_not raise_error
+ end
+
+ it 'should load classes even if not in default scope' do
+ lambda {
+ YAML.load(Story.create(:scoped => false).to_yaml)
+ }.should_not raise_error
+ end
+end
@@ -0,0 +1,54 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'rubygems'
+require 'bundler/setup'
+require 'rspec'
+require 'logger'
+
+require 'active_record'
+
+require 'delayed_job_active_record'
+require 'delayed/backend/shared_spec'
+
+Delayed::Worker.logger = Logger.new('/tmp/dj.log')
+ENV['RAILS_ENV'] = 'test'
+
+config = YAML.load(File.read('spec/database.yml'))
+# ActiveRecord::Base.configurations = {'test' => config['sqlite']}
+ActiveRecord::Base.establish_connection config['sqlite']
+ActiveRecord::Base.logger = Delayed::Worker.logger
+ActiveRecord::Migration.verbose = false
+
+ActiveRecord::Schema.define do
+ create_table :delayed_jobs, :force => true do |table|
+ table.integer :priority, :default => 0
+ table.integer :attempts, :default => 0
+ table.text :handler
+ table.text :last_error
+ table.datetime :run_at
+ table.datetime :locked_at
+ table.datetime :failed_at
+ table.string :locked_by
+ table.timestamps
+ end
+
+ add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
+
+ create_table :stories, :primary_key => :story_id, :force => true do |table|
+ table.string :text
+ table.boolean :scoped, :default => true
+ end
+end
+
+# Purely useful for test cases...
+class Story < ActiveRecord::Base
+ set_primary_key :story_id
+ def tell; text; end
+ def whatever(n, _); tell*n; end
+ default_scope where(:scoped => true)
+
+ handle_asynchronously :whatever
+end
+
+# Add this directory so the ActiveSupport autoloading works
+ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)

0 comments on commit 0bd9db7

Please sign in to comment.