say_when 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -0
  4. data/Guardfile +50 -0
  5. data/README.md +135 -2
  6. data/Rakefile +1 -0
  7. data/lib/say_when.rb +33 -18
  8. data/lib/say_when/configuration.rb +16 -0
  9. data/lib/say_when/cron_expression.rb +19 -21
  10. data/lib/say_when/poller/base_poller.rb +108 -0
  11. data/lib/say_when/poller/celluloid_poller.rb +30 -0
  12. data/lib/say_when/poller/concurrent_poller.rb +31 -0
  13. data/lib/say_when/poller/simple_poller.rb +37 -0
  14. data/lib/say_when/processor/active_job_strategy.rb +35 -0
  15. data/lib/say_when/processor/simple_strategy.rb +13 -0
  16. data/lib/say_when/processor/test_strategy.rb +21 -0
  17. data/lib/say_when/scheduler.rb +67 -101
  18. data/lib/say_when/storage/active_record_strategy.rb +204 -0
  19. data/lib/say_when/storage/base_job.rb +96 -0
  20. data/lib/say_when/storage/memory_strategy.rb +140 -0
  21. data/lib/say_when/tasks.rb +15 -3
  22. data/lib/say_when/triggers/base.rb +3 -3
  23. data/lib/say_when/triggers/cron_strategy.rb +2 -3
  24. data/lib/say_when/triggers/instance_strategy.rb +3 -4
  25. data/lib/say_when/triggers/once_strategy.rb +3 -4
  26. data/lib/say_when/utils.rb +16 -0
  27. data/lib/say_when/version.rb +1 -1
  28. data/say_when.gemspec +10 -5
  29. data/test/minitest_helper.rb +45 -15
  30. data/test/say_when/configuration_test.rb +14 -0
  31. data/test/say_when/cron_expression_test.rb +140 -0
  32. data/test/say_when/poller/base_poller_test.rb +42 -0
  33. data/test/say_when/poller/celluloid_poller_test.rb +17 -0
  34. data/test/say_when/poller/concurrent_poller_test.rb +19 -0
  35. data/test/say_when/poller/simple_poller_test.rb +27 -0
  36. data/test/say_when/processor/active_job_strategy_test.rb +31 -0
  37. data/test/say_when/processor/simple_strategy_test.rb +15 -0
  38. data/test/say_when/scheduler_test.rb +41 -57
  39. data/test/say_when/storage/active_record_strategy_test.rb +134 -0
  40. data/test/say_when/storage/memory_strategy_test.rb +96 -0
  41. data/test/say_when/triggers/cron_strategy_test.rb +11 -0
  42. data/test/say_when/triggers/instance_strategy_test.rb +13 -0
  43. data/test/say_when/triggers/once_strategy_test.rb +2 -2
  44. data/test/say_when_test.rb +20 -0
  45. metadata +110 -36
  46. data/lib/say_when/base_job.rb +0 -96
  47. data/lib/say_when/processor/active_messaging.rb +0 -21
  48. data/lib/say_when/processor/base.rb +0 -19
  49. data/lib/say_when/processor/shoryuken.rb +0 -14
  50. data/lib/say_when/processor/simple.rb +0 -17
  51. data/lib/say_when/storage/active_record/acts.rb +0 -92
  52. data/lib/say_when/storage/active_record/job.rb +0 -100
  53. data/lib/say_when/storage/active_record/job_execution.rb +0 -14
  54. data/lib/say_when/storage/memory/base.rb +0 -36
  55. data/lib/say_when/storage/memory/job.rb +0 -53
  56. data/test/say_when/cron_expression_spec.rb +0 -74
  57. data/test/say_when/processor/active_messaging_test.rb +0 -41
  58. data/test/say_when/storage/active_record/job_test.rb +0 -90
  59. data/test/say_when/storage/memory/job_test.rb +0 -32
  60. data/test/say_when/storage/memory/trigger_test.rb +0 -54
  61. data/test/support/models.rb +0 -33
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'celluloid/current'
4
+ require 'logger'
5
+ require 'say_when/poller/base_poller'
6
+
7
+ module SayWhen
8
+ module Poller
9
+ class CelluloidPoller
10
+ include Celluloid
11
+ include SayWhen::Poller::BasePoller
12
+
13
+ def initialize(tick = nil)
14
+ @tick_length = tick.to_i if tick
15
+ start
16
+ end
17
+
18
+ def start
19
+ @tick_timer = every(tick_length) { process_jobs }
20
+ end
21
+
22
+ def stop
23
+ if @tick_timer
24
+ @tick_timer.cancel
25
+ @tick_timer = nil
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ require 'concurrent'
4
+ require 'logger'
5
+ require 'say_when/poller/base_poller'
6
+
7
+ module SayWhen
8
+ module Poller
9
+ class ConcurrentPoller
10
+ include SayWhen::Poller::BasePoller
11
+
12
+ def initialize(tick = nil)
13
+ @tick_length = tick.to_i if tick
14
+ start
15
+ end
16
+
17
+ def start
18
+ @tick_timer = Concurrent::TimerTask.new(execution_interval: tick_length) do
19
+ process_jobs
20
+ end.tap(&:execute)
21
+ end
22
+
23
+ def stop
24
+ if @tick_timer
25
+ @tick_timer.shutdown
26
+ @tick_timer = nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ require 'say_when/poller/base_poller'
4
+
5
+ module SayWhen
6
+ module Poller
7
+ class SimplePoller
8
+ include SayWhen::Poller::BasePoller
9
+
10
+ attr_accessor :running
11
+
12
+ def initialize(tick = nil)
13
+ self.tick_length = tick.to_i if tick
14
+ self.running = false
15
+ end
16
+
17
+ def running?
18
+ !!running
19
+ end
20
+
21
+ def start
22
+ self.running = true
23
+ logger.info "SayWhen::SimplePoller started"
24
+ while running
25
+ process_jobs
26
+ tick
27
+ end
28
+ logger.info "SayWhen::SimplePoller stopped"
29
+ end
30
+
31
+ def stop
32
+ logger.info "SayWhen::SimplePoller stopping..."
33
+ self.running = false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_job'
4
+
5
+ module SayWhen
6
+ module Processor
7
+ class ActiveJobStrategy
8
+ class << self
9
+ def process(job)
10
+ SayWhenJob.perform_later(job_to_arg(job))
11
+ end
12
+
13
+ def job_to_arg(job)
14
+ case job
15
+ when GlobalID::Identification
16
+ job
17
+ else
18
+ { class: job.class.name, attributes: job.to_hash }
19
+ end
20
+ end
21
+ end
22
+
23
+ class SayWhenJob < ActiveJob::Base
24
+ queue_as SayWhen.options[:queue]
25
+
26
+ def perform(job)
27
+ if job.is_a?(Hash)
28
+ job = job[:class].constantize.new(job[:attributes])
29
+ end
30
+ job.execute
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module SayWhen
4
+ module Processor
5
+ class SimpleStrategy
6
+ class << self
7
+ def process(job)
8
+ job.execute
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module SayWhen
4
+ module Processor
5
+ class TestStrategy
6
+ class << self
7
+ def process(job)
8
+ self.jobs << job
9
+ end
10
+
11
+ def reset
12
+ self.jobs = []
13
+ end
14
+
15
+ def jobs
16
+ @jobs ||= []
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,133 +1,99 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'say_when/utils'
4
+
3
5
  module SayWhen
4
6
  class Scheduler
7
+ include SayWhen::Utils
5
8
 
6
- DEFAULT_PROCESSOR_CLASS = SayWhen::Processor::Simple
7
- DEFAULT_STORAGE_STRATEGY = :memory
8
-
9
- @@scheduler = nil
10
- @@lock = Mutex.new
11
-
12
- attr_accessor :storage_strategy, :processor_class, :tick_length
13
-
14
- attr_accessor :running
9
+ # When passing in a job, can be a Hash, String, or Class
10
+ # Hash: { class: '<class name>' } or { job_class: '<class name>' }
11
+ # String: '<class name>'
12
+ # Class: <job class>
13
+ def schedule(job)
14
+ storage.create(job)
15
+ end
15
16
 
16
- # support for a singleton scheduler, but you are not restricted to this
17
- class << self
18
- def scheduler
19
- return @@scheduler if @@scheduler
20
- @@lock.synchronize { @@scheduler = self.new if @@scheduler.nil? }
21
- @@scheduler
22
- end
17
+ def schedule_cron(expression, job)
18
+ time_zone = if job.is_a?(Hash)
19
+ job.delete(:time_zone)
20
+ end || 'UTC'
21
+ options = job_options(job)
22
+ options[:trigger_strategy] = :cron
23
+ options[:trigger_options] = { expression: expression, time_zone: time_zone }
24
+ schedule(options)
25
+ end
23
26
 
24
- def configure
25
- yield scheduler
26
- scheduler
27
- end
27
+ def schedule_instance(next_at_method = 'next_fire_at', job = {})
28
+ options = job_options(job)
29
+ options[:trigger_strategy] = 'instance'
30
+ options[:trigger_options] = { next_at_method: next_at_method }
31
+ schedule(options)
32
+ end
28
33
 
29
- def schedule(job)
30
- scheduler.schedule(job)
31
- end
34
+ def schedule_once(time, job = {})
35
+ options = job_options(job)
36
+ options[:trigger_strategy] = 'once'
37
+ options[:trigger_options] = { at: time}
38
+ schedule(options)
39
+ end
32
40
 
33
- def start
34
- scheduler.start
35
- end
41
+ def schedule_in(after, job = {})
42
+ options = job_options(job)
43
+ options[:trigger_strategy] = 'once'
44
+ options[:trigger_options] = { at: (Time.now + after)}
45
+ schedule(options)
36
46
  end
37
47
 
38
- def initialize
39
- self.tick_length = [ENV['SAY_WHEN_TICK_LENGTH'].to_i, 5].max
48
+ def job_options(job)
49
+ {
50
+ scheduled: extract_scheduled(job),
51
+ job_class: extract_job_class(job),
52
+ job_method: extract_job_method(job),
53
+ data: extract_data(job)
54
+ }
40
55
  end
41
56
 
42
- def processor
43
- if @processor.nil?
44
- @processor_class ||= DEFAULT_PROCESSOR_CLASS
45
- @processor = @processor_class.new(self)
46
- end
47
- @processor
57
+ def extract_scheduled(job)
58
+ job[:scheduled] if job.is_a?(Hash)
48
59
  end
49
60
 
50
- def start
51
- logger.info "SayWhen::Scheduler starting"
52
-
53
- [$stdout, $stderr].each{|s| s.sync = true; s.flush}
54
-
55
- trap("TERM", "EXIT")
56
-
57
- begin
58
-
59
- self.running = true
60
-
61
- logger.info "SayWhen::Scheduler running"
62
- job = nil
63
- while running
64
- begin
65
- time_now = Time.now
66
- logger.debug "SayWhen:: Looking for job that should be ready to fire before #{time_now}"
67
- job = job_class.acquire_next(time_now)
68
- if job.nil?
69
- logger.debug "SayWhen:: no jobs to acquire, sleep"
70
- sleep(tick_length)
71
- else
72
- logger.debug "SayWhen:: got a job: #{job.inspect}"
73
- # delegate processing the trigger to the processor
74
- self.processor.process(job)
75
- logger.debug "SayWhen:: job processed"
76
-
77
- # this should update next fire at, and put back in list of scheduled jobs
78
- job.fired(time_now)
79
- logger.debug "SayWhen:: job fired complete"
80
- end
81
- rescue StandardError => ex
82
- job_msg = job && "job: #{job.inspect} "
83
- logger.error "SayWhen:: Failure: #{job_msg}exception: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
84
- safe_release(job)
85
- sleep(tick_length)
86
- rescue Interrupt => ex
87
- job_msg = job && "\n - interrupted job: #{job.inspect}\n"
88
- logger.error "\nSayWhen:: Interrupt! #{ex.inspect}#{job_msg}"
89
- safe_release(job)
90
- exit
91
- rescue Exception => ex
92
- job_msg = job && "job: #{job.inspect} "
93
- logger.error "SayWhen:: Exception: #{job_msg}exception: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
94
- safe_release(job)
95
- exit
96
- end
97
- end
61
+ def extract_job_class(job)
62
+ job_class = if job.is_a?(Hash)
63
+ job[:class] || job[:job_class]
64
+ elsif job.is_a?(Class)
65
+ job.name
66
+ elsif job.is_a?(String)
67
+ job
98
68
  end
99
69
 
100
- logger.info "SayWhen::Scheduler stopped"
101
- end
70
+ if !job_class
71
+ raise "Could not identify job class from: #{job}"
72
+ end
102
73
 
103
- def safe_release(job)
104
- job.release if job
105
- rescue
106
- logger "Failed to release job: #{job.inspect}" rescue nil
74
+ job_class
107
75
  end
108
76
 
109
- def stop
110
- logger.info "SayWhen::Scheduler stopping..."
111
- self.running = false
77
+ def extract_job_method(job)
78
+ if job.is_a?(Hash)
79
+ job[:method] || job[:job_method]
80
+ end || 'execute'
112
81
  end
113
82
 
114
- def job_class
115
- @job_class ||= load_job_class
83
+ def extract_data(job)
84
+ job[:data] if job && job.is_a?(Hash)
116
85
  end
117
86
 
118
- def load_job_class
119
- strategy = @storage_strategy || DEFAULT_STORAGE_STRATEGY
120
- require "say_when/storage/#{strategy}/job"
121
- job_class_name = "SayWhen::Storage::#{strategy.to_s.camelize}::Job"
122
- job_class_name.constantize
87
+ def storage=(s)
88
+ @storage = s
123
89
  end
124
90
 
125
- def schedule(job)
126
- job_class.create(job)
91
+ def storage
92
+ @storage ||= load_strategy(:storage, SayWhen.options[:storage_strategy])
127
93
  end
128
94
 
129
95
  def logger
130
- SayWhen::logger
96
+ SayWhen.logger
131
97
  end
132
98
  end
133
99
  end
@@ -0,0 +1,204 @@
1
+ require 'active_record'
2
+ require 'say_when/storage/base_job'
3
+
4
+ module SayWhen
5
+ module Storage
6
+ class ActiveRecordStrategy
7
+ class << self
8
+ def acquire_next(no_later_than = nil)
9
+ SayWhen::Storage::ActiveRecordStrategy::Job.acquire_next(no_later_than)
10
+ end
11
+
12
+ def reset_acquired(older_than_seconds)
13
+ SayWhen::Storage::ActiveRecordStrategy::Job.reset_acquired(older_than_seconds)
14
+ end
15
+
16
+ def create(job)
17
+ SayWhen::Storage::ActiveRecordStrategy::Job.job_create(job)
18
+ end
19
+
20
+ def fired(job, fired_at = Time.now)
21
+ job.fired(fired_at)
22
+ end
23
+
24
+ def release(job)
25
+ job.release
26
+ end
27
+
28
+ def serialize(job)
29
+ job
30
+ end
31
+
32
+ def deserialize(job)
33
+ job
34
+ end
35
+ end
36
+
37
+ class JobExecution < ActiveRecord::Base
38
+ self.table_name = 'say_when_job_executions'
39
+ belongs_to :job, class_name: 'SayWhen::Storage::ActiveRecordStrategy::Job'
40
+ end
41
+
42
+ class Job < ActiveRecord::Base
43
+ include SayWhen::Storage::BaseJob
44
+
45
+ self.table_name = 'say_when_jobs'
46
+
47
+ serialize :trigger_options
48
+ serialize :data
49
+
50
+ belongs_to :scheduled, polymorphic: true
51
+ has_many :job_executions, class_name: 'SayWhen::Storage::ActiveRecordStrategy::JobExecution'
52
+
53
+ before_create :set_defaults
54
+
55
+ def self.job_create(job)
56
+ if existing_job = find_named_job(job[:group], job[:name])
57
+ existing_job.tap { |j| j.update_attributes(job) }
58
+ else
59
+ create(job)
60
+ end
61
+ end
62
+
63
+ def self.find_named_job(group, name)
64
+ group && name && where(name: name, group: group).first
65
+ end
66
+
67
+ def self.acquire_next(no_later_than = nil)
68
+ next_job = nil
69
+ no_later_than = (no_later_than || Time.now).in_time_zone('UTC')
70
+
71
+ check_connection
72
+ hide_logging do
73
+ SayWhen::Storage::ActiveRecordStrategy::Job.transaction do
74
+ # select and lock the next job that needs executin' (status waiting, and after no_later_than)
75
+ next_job = where(status: STATE_WAITING).
76
+ where('next_fire_at < ?', no_later_than).
77
+ order('next_fire_at ASC').
78
+ lock(true).
79
+ first
80
+
81
+ # set status to acquired to take it out of rotation
82
+ next_job.update_attribute(:status, STATE_ACQUIRED) if next_job
83
+ end
84
+ end
85
+ next_job
86
+ end
87
+
88
+ def self.reset_acquired(older_than_seconds)
89
+ return unless older_than_seconds.to_i > 0
90
+ older_than = (Time.now - older_than_seconds.to_i)
91
+ where('status = ? and updated_at < ?', STATE_ACQUIRED, older_than).update_all("status = '#{STATE_WAITING}'")
92
+ end
93
+
94
+ def self.check_connection
95
+ if ActiveRecord::Base.respond_to?(:clear_active_connections!)
96
+ ActiveRecord::Base.clear_active_connections!
97
+ elsif ActiveRecord::Base.respond_to?(:verify_active_connections!)
98
+ ActiveRecord::Base.verify_active_connections!
99
+ end
100
+ end
101
+
102
+ def self.hide_logging
103
+ old_logger = nil
104
+ begin
105
+ old_logger = ::ActiveRecord::Base.logger
106
+ ::ActiveRecord::Base.logger = nil
107
+ yield
108
+ ensure
109
+ ::ActiveRecord::Base.logger = old_logger
110
+ end
111
+ end
112
+
113
+ def set_defaults
114
+ self.status = STATE_WAITING
115
+ self.next_fire_at = self.trigger.next_fire_at
116
+ end
117
+
118
+ def fired(fired_at=Time.now)
119
+ self.class.transaction {
120
+ super
121
+ self.save!
122
+ }
123
+ end
124
+
125
+ def release
126
+ self.class.transaction {
127
+ super
128
+ self.save!
129
+ }
130
+ end
131
+
132
+ # default impl with some error handling and result recording
133
+ def execute
134
+ result = nil
135
+ execution = JobExecution.create(job: self, status: STATE_EXECUTING, start_at: Time.now)
136
+
137
+ begin
138
+ result = self.execute_job(data)
139
+ execution.result = result
140
+ execution.status = 'complete'
141
+ rescue Object => ex
142
+ execution.result = "#{ex.class.name}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
143
+ execution.status = 'error'
144
+ end
145
+
146
+ execution.end_at = Time.now
147
+ execution.save!
148
+ result
149
+ end
150
+ end
151
+
152
+ module Acts #:nodoc:
153
+ extend ActiveSupport::Concern
154
+
155
+ module ClassMethods
156
+ def acts_as_scheduled
157
+ include SayWhen::Storage::ActiveRecordStrategy::Acts::InstanceMethods
158
+
159
+ has_many :scheduled_jobs,
160
+ as: :scheduled,
161
+ class_name: 'SayWhen::Storage::ActiveRecordStrategy::Job',
162
+ dependent: :destroy
163
+ end
164
+ end
165
+
166
+ module InstanceMethods
167
+ def schedule(job)
168
+ Scheduler.schedule(set_scheduled(job))
169
+ end
170
+
171
+ def schedule_instance(next_at_method = 'next_fire_at', job = {})
172
+ Scheduler.schedule_instance(next_at_method, set_scheduled(job))
173
+ end
174
+
175
+ def schedule_cron(expression, job = {})
176
+ Scheduler.schedule_cron(expression, set_scheduled(job))
177
+ end
178
+
179
+ def schedule_once(time, job = {})
180
+ Scheduler.schedule_once(time, set_scheduled(job))
181
+ end
182
+
183
+ def schedule_in(after, job = {})
184
+ Scheduler.schedule_in(after, set_scheduled(job))
185
+ end
186
+
187
+ def set_scheduled(job)
188
+ if job.is_a?(Hash)
189
+ job[:scheduled] = self
190
+ elsif job.respond_to?(:scheduled)
191
+ job.scheduled = self
192
+ end
193
+ job
194
+ end
195
+ end # InstanceMethods
196
+ end # class << self
197
+ end
198
+ end
199
+ end
200
+
201
+ aas = SayWhen::Storage::ActiveRecordStrategy::Acts
202
+ unless ActiveRecord::Base.include?(aas)
203
+ ActiveRecord::Base.send(:include, aas)
204
+ end