tickwork 0.0.1

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.
@@ -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