say_when 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)