say_when 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/Rakefile +1 -0
  4. data/generators/say_when_migration/say_when_migration_generator.rb +11 -0
  5. data/generators/say_when_migration/templates/migration.rb +48 -0
  6. data/lib/generators/.DS_Store +0 -0
  7. data/lib/generators/say_when/migration/migration_generator.rb +13 -0
  8. data/lib/generators/say_when/migration/templates/migration.rb +47 -0
  9. data/lib/say_when/base_job.rb +96 -0
  10. data/lib/say_when/cron_expression.rb +621 -0
  11. data/lib/say_when/processor/active_messaging.rb +22 -0
  12. data/lib/say_when/processor/base.rb +17 -0
  13. data/lib/say_when/processor/simple.rb +15 -0
  14. data/lib/say_when/scheduler.rb +129 -0
  15. data/lib/say_when/storage/active_record/acts.rb +85 -0
  16. data/lib/say_when/storage/active_record/job.rb +85 -0
  17. data/lib/say_when/storage/active_record/job_execution.rb +17 -0
  18. data/lib/say_when/storage/memory/base.rb +34 -0
  19. data/lib/say_when/storage/memory/job.rb +48 -0
  20. data/lib/say_when/storage/mongoid/job.rb +15 -0
  21. data/lib/say_when/tasks.rb +22 -0
  22. data/lib/say_when/triggers/base.rb +11 -0
  23. data/lib/say_when/triggers/cron_strategy.rb +22 -0
  24. data/lib/say_when/triggers/once_strategy.rb +30 -0
  25. data/lib/say_when/version.rb +3 -0
  26. data/lib/say_when.rb +28 -0
  27. data/lib/tasks/say_when.rake +2 -0
  28. data/say_when.gemspec +26 -0
  29. data/spec/active_record_spec_helper.rb +11 -0
  30. data/spec/db/schema.rb +36 -0
  31. data/spec/db/test.db +0 -0
  32. data/spec/mongoid_spec_helper.rb +7 -0
  33. data/spec/say_when/cron_expression_spec.rb +72 -0
  34. data/spec/say_when/scheduler_spec.rb +76 -0
  35. data/spec/say_when/storage/active_record/job_spec.rb +84 -0
  36. data/spec/say_when/storage/memory/job_spec.rb +31 -0
  37. data/spec/say_when/storage/memory/trigger_spec.rb +54 -0
  38. data/spec/say_when/storage/mongoid/trigger_spec.rb +57 -0
  39. data/spec/spec.opts +4 -0
  40. data/spec/spec_helper.rb +46 -0
  41. data/spec/support/models.rb +31 -0
  42. metadata +224 -0
@@ -0,0 +1,129 @@
1
+ module SayWhen
2
+
3
+ class Scheduler
4
+
5
+ DEFAULT_PROCESSOR_CLASS = SayWhen::Processor::Simple
6
+ DEFAULT_STORAGE_STRATEGY = :memory
7
+
8
+ @@scheduler = nil
9
+ @@lock = nil
10
+
11
+ attr_accessor :storage_strategy, :processor_class, :tick_length
12
+
13
+ attr_accessor :running
14
+
15
+ # support for a singleton scheduler, but you are not restricted to this
16
+ class << self
17
+
18
+ def scheduler
19
+ self.lock.synchronize {
20
+ if @@scheduler.nil?
21
+ @@scheduler = self.new
22
+ end
23
+ }
24
+ @@scheduler
25
+ end
26
+
27
+ def configure
28
+ yield self.scheduler
29
+ self.scheduler
30
+ end
31
+
32
+ def lock
33
+ @@lock ||= Mutex.new
34
+ end
35
+
36
+ def schedule(job)
37
+ self.scheduler.schedule(job)
38
+ end
39
+
40
+ def start
41
+ self.scheduler.start
42
+ end
43
+
44
+ end
45
+
46
+ def initialize
47
+ self.tick_length = 1
48
+ end
49
+
50
+ def processor
51
+ if @processor.nil?
52
+ @processor_class ||= DEFAULT_PROCESSOR_CLASS
53
+ @processor = @processor_class.new(self)
54
+ end
55
+ @processor
56
+ end
57
+
58
+ def start
59
+ logger.info "SayWhen::Scheduler starting"
60
+
61
+ [$stdout, $stderr].each{|s| s.sync = true; s.flush}
62
+ trap("TERM", "EXIT")
63
+ trap("QUIT") { stop }
64
+
65
+ begin
66
+ self.running = true
67
+
68
+ logger.info "SayWhen::Scheduler running"
69
+ job = nil
70
+ while running
71
+ begin
72
+ time_now = Time.now
73
+ logger.debug "SayWhen:: Looking for job that should be ready to fire before #{time_now}"
74
+ job = job_class.acquire_next(time_now)
75
+ if job.nil?
76
+ logger.debug "SayWhen:: no jobs to acquire, sleep"
77
+ sleep(tick_length)
78
+ else
79
+ logger.debug "SayWhen:: got a job: #{job.inspect}"
80
+ # delegate processing the trigger to the processor
81
+ self.processor.process(job)
82
+ logger.debug "SayWhen:: job processed"
83
+
84
+ # this should update next fire at, and put back in list of scheduled jobs
85
+ job.fired
86
+ logger.debug "SayWhen:: job fired complete"
87
+
88
+ end
89
+ rescue Object=>ex
90
+ begin
91
+ job_msg = job && " job:'#{job.inspect}'"
92
+ logger.error "SayWhen:: Failure to process#{job_msg}: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
93
+ job.release if job
94
+ rescue
95
+ puts ex
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ logger.info "SayWhen::Scheduler stopped"
102
+ end
103
+
104
+ def stop
105
+ logger.info "SayWhen::Scheduler stopping..."
106
+ self.running = false
107
+ end
108
+
109
+ def job_class
110
+ @job_class ||= load_job_class
111
+ end
112
+
113
+ def load_job_class
114
+ strategy = @storage_strategy || DEFAULT_STORAGE_STRATEGY
115
+ require "say_when/storage/#{strategy}/job"
116
+ job_class_name = "SayWhen::Storage::#{strategy.to_s.camelize}::Job"
117
+ job_class_name.constantize
118
+ end
119
+
120
+ def schedule(job)
121
+ job_class.create(job)
122
+ end
123
+
124
+ def logger
125
+ SayWhen::logger
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,85 @@
1
+ module SayWhen #:nodoc:
2
+ module Storage #:nodoc:
3
+ module ActiveRecord #:nodoc:
4
+ module Acts #:nodoc:
5
+
6
+ def self.included(base) # :nodoc:
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_scheduled
12
+ include SayWhen::Storage::ActiveRecord::Acts::InstanceMethods
13
+
14
+ has_many :scheduled_jobs, :as=>:scheduled, :class_name=>'SayWhen::Storage::ActiveRecord::Job'
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ def schedule_cron(expression, time_zone, job={})
21
+ options = job_options(job)
22
+ options[:trigger_strategy] = :cron
23
+ options[:trigger_options] = {:expression => expression, :time_zone => time_zone}
24
+ Scheduler.schedule(options)
25
+ end
26
+
27
+ def schedule_once(time, job={})
28
+ options = job_options(job)
29
+ options[:trigger_strategy] = :once
30
+ options[:trigger_options] = time
31
+ Scheduler.schedule(options)
32
+ end
33
+
34
+ def schedule_in(after, job={})
35
+ options = job_options(job)
36
+ options[:trigger_strategy] = :once
37
+ options[:trigger_options] = Time.now + after
38
+ Scheduler.schedule(options)
39
+ end
40
+
41
+ # helpers
42
+
43
+ def job_options(job)
44
+ { :scheduled => self,
45
+ :job_class => extract_job_class(job),
46
+ :job_method => extract_job_method(job),
47
+ :data => extract_data(job) }
48
+ end
49
+
50
+ def extract_job_class(job)
51
+ if job.is_a?(Hash)
52
+ job[:class]
53
+ elsif job.is_a?(Class)
54
+ job.name
55
+ elsif job.is_a?(String)
56
+ job
57
+ else
58
+ raise "Could not identify job class from: #{job}"
59
+ end
60
+ end
61
+
62
+ def extract_job_method(job)
63
+ if job.is_a?(Hash)
64
+ job[:method]
65
+ else
66
+ 'execute'
67
+ end
68
+ end
69
+
70
+ def extract_data(job)
71
+ if job.is_a?(Hash)
72
+ job[:data]
73
+ else
74
+ nil
75
+ end
76
+ end
77
+
78
+ end # InstanceMethods
79
+
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ ActiveRecord::Base.send(:include, SayWhen::Storage::ActiveRecord::Acts) unless ActiveRecord::Base.include?(SayWhen::Storage::ActiveRecord::Acts)
@@ -0,0 +1,85 @@
1
+ require 'active_record'
2
+ require 'say_when/base_job'
3
+ require 'say_when/storage/active_record/job_execution'
4
+ require 'say_when/storage/active_record/acts'
5
+
6
+ module SayWhen
7
+ module Storage
8
+ module ActiveRecord
9
+
10
+ class Job < ::ActiveRecord::Base
11
+
12
+ include SayWhen::BaseJob
13
+
14
+ self.table_name = "say_when_jobs"
15
+
16
+
17
+ serialize :trigger_options
18
+ serialize :data
19
+ belongs_to :scheduled, :polymorphic => true
20
+ has_many :job_executions, :class_name=>'SayWhen::Storage::ActiveRecord::JobExecution'
21
+
22
+ def self.acquire_next(no_later_than)
23
+ SayWhen::Storage::ActiveRecord::Job.transaction do
24
+ # select and lock the next job that needs executin' (status waiting, and after no_later_than)
25
+ next_job = find(:first,
26
+ :lock => true,
27
+ :order => 'next_fire_at ASC',
28
+ :conditions => ['status = ? and ? >= next_fire_at',
29
+ STATE_WAITING,
30
+ no_later_than.in_time_zone('UTC')])
31
+
32
+ # make sure there is a job ready to run
33
+ return nil if next_job.nil?
34
+
35
+ # set status to acquired to take it out of rotation
36
+ next_job.update_attribute(:status, STATE_ACQUIRED)
37
+
38
+ return next_job
39
+ end
40
+ end
41
+
42
+ def before_create
43
+ self.status = STATE_WAITING
44
+ self.next_fire_at = self.trigger.next_fire_at(Time.now)
45
+ end
46
+
47
+ def fired
48
+ Job.transaction {
49
+ super
50
+ self.save!
51
+ }
52
+ end
53
+
54
+ def release
55
+ Job.transaction {
56
+ super
57
+ self.save!
58
+ }
59
+ end
60
+
61
+
62
+ # default impl with some error handling and result recording
63
+ def execute
64
+ result = nil
65
+ execution = SayWhen::Storage::ActiveRecord::JobExecution.create(:job=>self, :status=>'executing', :start_at=>Time.now)
66
+
67
+ begin
68
+ result = self.execute_job(data)
69
+ execution.result = result
70
+ execution.status = 'complete'
71
+ rescue Object=>ex
72
+ execution.result = "#{ex.class.name}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
73
+ execution.status = 'error'
74
+ end
75
+
76
+ execution.end_at = Time.now
77
+ execution.save!
78
+ result
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_record'
2
+
3
+ module SayWhen
4
+ module Storage
5
+ module ActiveRecord
6
+
7
+ class JobExecution < ::ActiveRecord::Base
8
+
9
+ self.table_name = "say_when_job_executions"
10
+
11
+ belongs_to :job, :class_name=>'SayWhen::Storage::ActiveRecord::Job'
12
+
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module SayWhen
2
+ module Storage
3
+ module Memory
4
+
5
+ module Base
6
+
7
+ attr_accessor :props
8
+
9
+ def has_properties(*args)
10
+ @props ||= []
11
+ args.each do |a|
12
+ unless @props.member?(a.to_s)
13
+ @props << a.to_s
14
+ class_eval { attr_accessor(a.to_sym) }
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.included(base)
20
+ base.extend self
21
+ end
22
+
23
+ def initialize(args={})
24
+ args.each do |k,v|
25
+ if self.class.props.member?(k.to_s)
26
+ self.send("#{k}=", v)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ require 'say_when/storage/memory/base'
2
+
3
+ module SayWhen
4
+ module Store
5
+ module Memory
6
+
7
+ # define a trigger class
8
+ class Job
9
+
10
+ cattr_accessor :jobs
11
+ @@jobs = SortedSet.new
12
+
13
+ include SayWhen::Storage::Memory::Base
14
+ include SayWhen::BaseJob
15
+
16
+ has_properties :group, :name, :status, :start_at, :end_at
17
+ has_properties :trigger_strategy, :trigger_options, :last_fire_at, :next_fire_at
18
+ has_properties :job_class, :job_method, :data
19
+ has_properties :scheduled
20
+
21
+ def self.acquire_next(no_later_than)
22
+ self.lock.synchronize {
23
+
24
+ next_job = jobs.detect(nil) do |j|
25
+ (j.status == STATE_WAITING) && (j.next_fire_at.to_i <= no_later_than.to_i)
26
+ end
27
+
28
+ next_job.status = STATE_ACQUIRED if next_job
29
+ next_job
30
+ }
31
+ end
32
+
33
+ def initialize(options={})
34
+ super
35
+ self.status = STATE_WAITING unless self.status
36
+ self.next_fire_at = self.trigger.next_fire_at(Time.now)
37
+ self.class.jobs << self
38
+ end
39
+
40
+ def <=>(job)
41
+ self.next_fire_at.to_i <=> job.next_fire_at.to_i
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ require 'mongoid'
2
+
3
+ module SayWhen
4
+ module Store
5
+ module Mongoid
6
+
7
+ class Job
8
+
9
+ include Mongoid::Document
10
+
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ namespace :say_when do
2
+ task :setup
3
+
4
+ desc "Start the SayWhen Scheduler"
5
+ task :start => [ :preload ] do
6
+ require 'say_when'
7
+ SayWhen::Scheduler.start
8
+ end
9
+
10
+ # Preload app files if this is Rails
11
+ # thanks resque
12
+ task :preload => :setup do
13
+ if defined?(Rails) && Rails.respond_to?(:application)
14
+ # Rails 3
15
+ Rails.application.eager_load!
16
+ elsif defined?(Rails::Initializer)
17
+ # Rails 2.3
18
+ $rails_rake_task = false
19
+ Rails::Initializer.run :load_application_classes
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module SayWhen
2
+ module Triggers
3
+ module Base
4
+
5
+ def next_fire_at(time=Time.now)
6
+ raise NotImplementedError.new('You need to implement next_fire_at in your strategy')
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ require 'say_when/triggers/base'
2
+ require 'say_when/cron_expression'
3
+
4
+ module SayWhen
5
+ module Triggers
6
+ class CronStrategy
7
+
8
+ include SayWhen::Triggers::Base
9
+
10
+ attr_accessor :cron_expression
11
+
12
+ def initialize(options={})
13
+ @cron_expression = SayWhen::CronExpression.new(options)
14
+ end
15
+
16
+ def next_fire_at(time=Time.now)
17
+ cron_expression.next_fire_at(time)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ require 'say_when/triggers/base'
2
+
3
+ module SayWhen
4
+ module Triggers
5
+ class OnceStrategy
6
+
7
+ include SayWhen::Triggers::Base
8
+
9
+ attr_accessor :once_at
10
+
11
+ def initialize(options=nil)
12
+ options ||= Time.now
13
+ # if it's a hash, pull out the time
14
+ @once_at = if options.is_a?(Time) || options.acts_like_time?
15
+ options
16
+ elsif options.is_a?(Hash) && options[:at]
17
+ options[:at]
18
+ else
19
+ Time.now
20
+ end
21
+
22
+ end
23
+
24
+ def next_fire_at(time=Time.now)
25
+ once_at if once_at >= time
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module SayWhen
2
+ VERSION = "0.1.0"
3
+ end
data/lib/say_when.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'active_support'
2
+
3
+ require "say_when/version"
4
+ require 'say_when/base_job'
5
+ require 'say_when/cron_expression'
6
+ require 'say_when/processor/base'
7
+ require 'say_when/processor/simple'
8
+ require 'say_when/scheduler'
9
+
10
+ require 'say_when/processor/active_messaging' if defined?(ActiveMessaging)
11
+
12
+ require 'say_when/storage/active_record/job' if defined?(ActiveRecord)
13
+
14
+ module SayWhen
15
+
16
+ def SayWhen.logger=(logger)
17
+ @@logger = logger
18
+ end
19
+
20
+ def SayWhen.logger
21
+ unless defined?(@@logger)
22
+ @@logger = Rails.logger if defined?(Rails.logger) && Rails.logger
23
+ @@logger = Logger.new(STDOUT) unless defined?(@@logger)
24
+ end
25
+ @@logger
26
+ end
27
+
28
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
2
+ require 'say_when/tasks'
data/say_when.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "say_when/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "say_when"
7
+ s.version = SayWhen::VERSION
8
+ s.authors = ["Andrew Kuklewicz"]
9
+ s.email = ["andrew@prx.org"]
10
+ s.homepage = "http://labs.prx.org"
11
+ s.summary = %q{Scheduling system for programmatically defined and stored jobs.}
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_development_dependency "activemessaging", '~> 0.9.0'
19
+ s.add_development_dependency "activesupport", '~> 2.3.14'
20
+ s.add_development_dependency "activerecord", '~> 2.3.14'
21
+ s.add_development_dependency "mongoid", '~> 1.9.5'
22
+ s.add_development_dependency 'rspec', "~> 1.3"
23
+ s.add_development_dependency 'sqlite3'
24
+ s.add_development_dependency 'rake'
25
+
26
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_record'
2
+ require 'sqlite3'
3
+
4
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
5
+
6
+ ActiveRecord::Base.establish_connection(
7
+ :adapter => "sqlite3",
8
+ :database => (File.dirname(__FILE__) + "/db/test.db")
9
+ )
10
+
11
+ require (File.dirname(__FILE__) + "/db/schema.rb")
data/spec/db/schema.rb ADDED
@@ -0,0 +1,36 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+
3
+ create_table :say_when_jobs, :force => true do |t|
4
+
5
+ t.string :status
6
+
7
+ t.string :trigger_strategy
8
+ t.text :trigger_options
9
+ t.string :time_zone
10
+
11
+ t.timestamp :last_fire_at
12
+ t.timestamp :next_fire_at
13
+
14
+ t.timestamp :start_at
15
+ t.timestamp :end_at
16
+
17
+ t.string :job_class
18
+ t.string :job_method
19
+ t.text :data
20
+
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :say_when_job_executions, :force => true do |t|
25
+ t.integer :job_id
26
+ t.string :status
27
+ t.text :result
28
+ t.datetime :start_at
29
+ t.datetime :end_at
30
+ end
31
+
32
+ add_index :say_when_jobs, :status
33
+ add_index :say_when_jobs, :next_fire_at
34
+
35
+
36
+ end
data/spec/db/test.db ADDED
Binary file
@@ -0,0 +1,7 @@
1
+ require 'mongoid'
2
+
3
+ Mongoid.configure do |config|
4
+ config.master = Mongo::Connection.new.db("say_when_test")
5
+ end
6
+
7
+ # Mongoid.logger = Logger.new($stdout)