tickwork 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,174 @@
1
+ module Tickwork
2
+ class Manager
3
+ class NoHandlerDefined < RuntimeError; end
4
+ class NoDataStoreDefined < RuntimeError; end
5
+ class DuplicateJobName < RuntimeError; end
6
+
7
+ MANAGER_KEY = '__manager'
8
+
9
+ attr_reader :config
10
+
11
+ def initialize
12
+ @events = []
13
+ @callbacks = {}
14
+ @config = default_configuration
15
+ @handler = nil
16
+ @error_handler = nil
17
+ end
18
+
19
+ def thread_available?
20
+ Thread.list.select { |t| t['creator'] == self }.count < config[:max_threads]
21
+ end
22
+
23
+ def configure
24
+ yield(config)
25
+ if config[:sleep_timeout]
26
+ config[:logger].warn 'INCORRECT USAGE: sleep_timeout is not used'
27
+ if config[:sleep_timeout] < 1
28
+ config[:logger].warn 'sleep_timeout must be >= 1 second'
29
+ end
30
+ end
31
+ if config[:data_store].nil?
32
+ raise NoDataStoreDefined.new
33
+ end
34
+ if config[:tick_size] > 60
35
+ config[:logger].warn 'tick_size is greater than 60. Events scheduled for a specific time may be missed'
36
+ end
37
+ end
38
+
39
+ def default_configuration
40
+ {
41
+ logger: Logger.new(STDOUT),
42
+ thread: false,
43
+ max_threads: 10,
44
+ namespace: '_tickwork_',
45
+ tick_size: 60, # 1 minute
46
+ max_ticks: 10,
47
+ max_catchup: 3600 # 1 hour
48
+ }
49
+ end
50
+
51
+ def handler(&block)
52
+ @handler = block if block_given?
53
+ raise NoHandlerDefined unless @handler
54
+ @handler
55
+ end
56
+
57
+ def error_handler(&block)
58
+ @error_handler = block if block_given?
59
+ @error_handler
60
+ end
61
+
62
+ def data_store
63
+ config[:data_store]
64
+ end
65
+
66
+ def on(event, options={}, &block)
67
+ raise "Unsupported callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym)
68
+ (@callbacks[event.to_sym]||=[]) << block
69
+ end
70
+
71
+ def every(period, job, options={}, &block)
72
+ if period < config[:tick_size]
73
+ config[:logger].warn 'period is smaller than tick size. will fail to schedule all events'
74
+ end
75
+ if options[:at].respond_to?(:each)
76
+ every_with_multiple_times(period, job, options, &block)
77
+ else
78
+ register(period, job, block, options)
79
+ end
80
+ end
81
+
82
+ def fire_callbacks(event, *args)
83
+ @callbacks[event].nil? || @callbacks[event].all? { |h| h.call(*args) }
84
+ end
85
+
86
+ def data_store_key
87
+ @data_store_key ||= config[:namespace] + MANAGER_KEY
88
+ end
89
+
90
+ # pretty straight forward if you think about it
91
+ # run the ticks from the last time we ran to our max
92
+ # but don't run ticks in the future
93
+ def run
94
+ raise NoDataStoreDefined.new if data_store.nil?
95
+ log "Starting clock for #{@events.size} events: [ #{@events.map(&:to_s).join(' ')} ]"
96
+
97
+ last = last_t = data_store.get(data_store_key)
98
+ last ||= Time.now.to_i - config[:tick_size]
99
+ if !config[:max_catchup].nil? && config[:max_catchup] > 0 && last < Time.now.to_i - config[:max_catchup]
100
+ last = Time.now.to_i - config[:max_catchup] - config[:tick_size]
101
+ end
102
+
103
+ ticks = 0
104
+ tick_time = last + config[:tick_size]
105
+
106
+ while ticks < config[:max_ticks] && tick_time <= Time.now.to_i do
107
+ tick(tick_time)
108
+ last = tick_time
109
+ tick_time += config[:tick_size]
110
+ ticks += 1
111
+ end
112
+ data_store.set(data_store_key, last)
113
+ last
114
+ end
115
+
116
+ def tick(t=Time.now.to_i)
117
+ t = Time.at(t) # TODO refactor below
118
+ if (fire_callbacks(:before_tick))
119
+ events = events_to_run(t)
120
+ events.each do |event|
121
+ if (fire_callbacks(:before_run, event, t))
122
+ event.run(t)
123
+ fire_callbacks(:after_run, event, t)
124
+ end
125
+ end
126
+ end
127
+ fire_callbacks(:after_tick)
128
+ events
129
+ end
130
+
131
+ def clear!
132
+ data_store.set(data_store_key, nil)
133
+ end
134
+
135
+ def log_error(e)
136
+ config[:logger].error(e)
137
+ end
138
+
139
+ def handle_error(e)
140
+ error_handler.call(e) if error_handler
141
+ end
142
+
143
+ def log(msg)
144
+ config[:logger].info(msg)
145
+ end
146
+
147
+ private
148
+ def events_to_run(t)
149
+ @events.select{ |event| event.run_now?(t) }
150
+ end
151
+
152
+ def register(period, job, block, options)
153
+ options.merge({:namespace => config[:namespace]})
154
+ event = Event.new(self, period, job, block || handler, options)
155
+ guard_duplicate_events(event)
156
+ @events << event
157
+ event
158
+ end
159
+
160
+ def guard_duplicate_events(event)
161
+ if @events.map{|e| e.to_s }.include? event.to_s
162
+ raise DuplicateJobName
163
+ end
164
+ end
165
+
166
+ def every_with_multiple_times(period, job, options={}, &block)
167
+ each_options = options.clone
168
+ options[:at].each do |at|
169
+ each_options[:at] = at
170
+ register(period, job + '_' + at, block, each_options)
171
+ end
172
+ end
173
+ end
174
+ end
data/lib/tickwork.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'logger'
2
+ require 'active_support/time'
3
+
4
+ require 'tickwork/at'
5
+ require 'tickwork/event'
6
+ require 'tickwork/manager'
7
+
8
+ module Tickwork
9
+ class << self
10
+ def included(klass)
11
+ klass.send "include", Methods
12
+ klass.extend Methods
13
+ end
14
+
15
+ def manager
16
+ @manager ||= Manager.new
17
+ end
18
+
19
+ def manager=(manager)
20
+ @manager = manager
21
+ end
22
+ end
23
+
24
+ module Methods
25
+ def configure(&block)
26
+ Tickwork.manager.configure(&block)
27
+ end
28
+
29
+ def handler(&block)
30
+ Tickwork.manager.handler(&block)
31
+ end
32
+
33
+ def error_handler(&block)
34
+ Tickwork.manager.error_handler(&block)
35
+ end
36
+
37
+ def on(event, options={}, &block)
38
+ Tickwork.manager.on(event, options, &block)
39
+ end
40
+
41
+ def every(period, job, options={}, &block)
42
+ Tickwork.manager.every(period, job, options, &block)
43
+ end
44
+
45
+ def run
46
+ Tickwork.manager.run
47
+ end
48
+
49
+ def clear!
50
+ Tickwork.manager.clear! unless Tickwork.manager.nil?
51
+ Tickwork.manager = Manager.new
52
+ end
53
+ end
54
+
55
+ extend Methods
56
+ end
data/test/at_test.rb ADDED
@@ -0,0 +1,116 @@
1
+ require File.expand_path('../../lib/tickwork', __FILE__)
2
+ require "minitest/autorun"
3
+ require 'mocha/mini_test'
4
+ require 'time'
5
+ require 'active_support/time'
6
+
7
+ describe 'Tickwork::At' do
8
+ def time_in_day(hour, minute)
9
+ Time.new(2013, 1, 1, hour, minute, 0)
10
+ end
11
+
12
+ it '16:20' do
13
+ at = Tickwork::At.parse('16:20')
14
+ assert !at.ready?(time_in_day(16, 19))
15
+ assert at.ready?(time_in_day(16, 20))
16
+ assert !at.ready?(time_in_day(16, 21))
17
+ end
18
+
19
+ it '8:20' do
20
+ at = Tickwork::At.parse('8:20')
21
+ assert !at.ready?(time_in_day(8, 19))
22
+ assert at.ready?(time_in_day(8, 20))
23
+ assert !at.ready?(time_in_day(8, 21))
24
+ end
25
+
26
+ it '**:20 with two stars' do
27
+ at = Tickwork::At.parse('**:20')
28
+
29
+ assert !at.ready?(time_in_day(15, 19))
30
+ assert at.ready?(time_in_day(15, 20))
31
+ assert !at.ready?(time_in_day(15, 21))
32
+
33
+ assert !at.ready?(time_in_day(16, 19))
34
+ assert at.ready?(time_in_day(16, 20))
35
+ assert !at.ready?(time_in_day(16, 21))
36
+ end
37
+
38
+ it '*:20 with one star' do
39
+ at = Tickwork::At.parse('*:20')
40
+
41
+ assert !at.ready?(time_in_day(15, 19))
42
+ assert at.ready?(time_in_day(15, 20))
43
+ assert !at.ready?(time_in_day(15, 21))
44
+
45
+ assert !at.ready?(time_in_day(16, 19))
46
+ assert at.ready?(time_in_day(16, 20))
47
+ assert !at.ready?(time_in_day(16, 21))
48
+ end
49
+
50
+ it '16:**' do
51
+ at = Tickwork::At.parse('16:**')
52
+
53
+ assert !at.ready?(time_in_day(15, 59))
54
+ assert at.ready?(time_in_day(16, 00))
55
+ assert at.ready?(time_in_day(16, 30))
56
+ assert at.ready?(time_in_day(16, 59))
57
+ assert !at.ready?(time_in_day(17, 00))
58
+ end
59
+
60
+ it '8:**' do
61
+ at = Tickwork::At.parse('8:**')
62
+
63
+ assert !at.ready?(time_in_day(7, 59))
64
+ assert at.ready?(time_in_day(8, 00))
65
+ assert at.ready?(time_in_day(8, 30))
66
+ assert at.ready?(time_in_day(8, 59))
67
+ assert !at.ready?(time_in_day(9, 00))
68
+ end
69
+
70
+ it 'Saturday 12:00' do
71
+ at = Tickwork::At.parse('Saturday 12:00')
72
+
73
+ assert !at.ready?(Time.new(2010, 1, 1, 12, 00))
74
+ assert at.ready?(Time.new(2010, 1, 2, 12, 00)) # Saturday
75
+ assert !at.ready?(Time.new(2010, 1, 3, 12, 00))
76
+ assert at.ready?(Time.new(2010, 1, 9, 12, 00))
77
+ end
78
+
79
+ it 'sat 12:00' do
80
+ at = Tickwork::At.parse('sat 12:00')
81
+
82
+ assert !at.ready?(Time.new(2010, 1, 1, 12, 00))
83
+ assert at.ready?(Time.new(2010, 1, 2, 12, 00))
84
+ assert !at.ready?(Time.new(2010, 1, 3, 12, 00))
85
+ end
86
+
87
+ it 'invalid time 32:00' do
88
+ assert_raises Tickwork::At::FailedToParse do
89
+ Tickwork::At.parse('32:00')
90
+ end
91
+ end
92
+
93
+ it 'invalid multi-line with Sat 12:00' do
94
+ assert_raises Tickwork::At::FailedToParse do
95
+ Tickwork::At.parse("sat 12:00\nreally invalid time")
96
+ end
97
+ end
98
+
99
+ it 'invalid multi-line with 8:30' do
100
+ assert_raises Tickwork::At::FailedToParse do
101
+ Tickwork::At.parse("8:30\nreally invalid time")
102
+ end
103
+ end
104
+
105
+ it 'invalid multi-line with *:10' do
106
+ assert_raises Tickwork::At::FailedToParse do
107
+ Tickwork::At.parse("*:10\nreally invalid time")
108
+ end
109
+ end
110
+
111
+ it 'invalid multi-line with 12:**' do
112
+ assert_raises Tickwork::At::FailedToParse do
113
+ Tickwork::At.parse("12:**\nreally invalid time")
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,21 @@
1
+ module Tickwork
2
+ class FakeStore
3
+
4
+ def initialize
5
+ @data_store = {}
6
+ end
7
+
8
+ def get(key)
9
+ @data_store[key]
10
+ end
11
+
12
+ def set(key, value)
13
+ @data_store[key] = value
14
+ end
15
+
16
+ # not part of the interface but used for testing
17
+ def size
18
+ @data_store.size
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path('../../lib/tickwork', __FILE__)
2
+ require File.expand_path('../data_stores/fake_store.rb', __FILE__)
3
+ require 'mocha/mini_test'
4
+ require "minitest/autorun"
5
+
6
+ describe Tickwork::Event do
7
+ describe '#thread?' do
8
+ before do
9
+ @manager = Class.new
10
+ @data_store = Tickwork::FakeStore.new
11
+ end
12
+
13
+ describe 'manager config thread option set to true' do
14
+ before do
15
+ @manager.stubs(:config).returns({ :thread => true })
16
+ @manager.stubs(:data_store).returns(@data_store)
17
+ end
18
+
19
+ it 'is true' do
20
+ event = Tickwork::Event.new(@manager, nil, 'unnamed', nil)
21
+ assert_equal true, event.thread?
22
+ end
23
+
24
+ it 'is false when event thread option set' do
25
+ event = Tickwork::Event.new(@manager, nil, 'unnamed', nil, :thread => false)
26
+ assert_equal false, event.thread?
27
+ end
28
+ end
29
+
30
+ describe 'manager config thread option not set' do
31
+ before do
32
+ @manager.stubs(:config).returns({})
33
+ end
34
+
35
+ it 'is true if event thread option is true' do
36
+ event = Tickwork::Event.new(@manager, nil, 'unnamed', nil, :thread => true)
37
+ assert_equal true, event.thread?
38
+ end
39
+ end
40
+
41
+ describe 'job name' do
42
+ before do
43
+ @manager.stubs(:config).returns({})
44
+ end
45
+ it 'is required' do
46
+ assert_raises(Tickwork::Event::IllegalJobName) do
47
+ Tickwork::Event.new(@manager, nil, nil, nil)
48
+ end
49
+ end
50
+ it 'must be a string' do
51
+ assert_raises(Tickwork::Event::IllegalJobName) do
52
+ Tickwork::Event.new(@manager, nil, Class.new, nil)
53
+ end
54
+ end
55
+ it 'must not be empty' do
56
+ assert_raises(Tickwork::Event::IllegalJobName) do
57
+ Tickwork::Event.new(@manager, nil, '', nil)
58
+ end
59
+ end
60
+ it 'raises exception on manager key name clash' do
61
+ assert_raises(Tickwork::Event::IllegalJobName) do
62
+ Tickwork::Event.new(@manager, nil, '__manager', nil)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end