say_when 1.0.0 → 2.0.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 (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