pigeon 0.3.0 → 0.4.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.
- data/README.rdoc +36 -60
- data/Rakefile +1 -14
- data/VERSION +1 -1
- data/bin/launcher.example +15 -0
- data/lib/pigeon/dispatcher.rb +93 -0
- data/lib/pigeon/engine.rb +36 -27
- data/lib/pigeon/option_accessor.rb +2 -0
- data/lib/pigeon/processor.rb +66 -0
- data/lib/pigeon/queue.rb +269 -36
- data/lib/pigeon/scheduler.rb +101 -0
- data/lib/pigeon/sorted_array.rb +56 -0
- data/lib/pigeon/support.rb +15 -0
- data/lib/pigeon/task.rb +188 -0
- data/lib/pigeon.rb +9 -1
- data/pigeon.gemspec +29 -12
- data/test/helper.rb +19 -0
- data/test/unit/pigeon_backlog_test.rb +25 -0
- data/test/unit/pigeon_dispatcher_test.rb +62 -0
- data/test/{test_pigeon_engine.rb → unit/pigeon_engine_test.rb} +1 -1
- data/test/{test_pigeon_launcher.rb → unit/pigeon_launcher_test.rb} +1 -1
- data/test/{test_pigeon_option_accessor.rb → unit/pigeon_option_accessor_test.rb} +1 -1
- data/test/unit/pigeon_processor_test.rb +94 -0
- data/test/unit/pigeon_queue_test.rb +197 -0
- data/test/unit/pigeon_scheduler_test.rb +70 -0
- data/test/unit/pigeon_sorted_array_test.rb +52 -0
- data/test/unit/pigeon_task_test.rb +164 -0
- data/test/{test_pigeon.rb → unit/pigeon_test.rb} +1 -1
- metadata +30 -13
- data/test/test_pigeon_queue.rb +0 -29
data/lib/pigeon/queue.rb
CHANGED
@@ -1,66 +1,299 @@
|
|
1
1
|
class Pigeon::Queue
|
2
2
|
# == Constants ============================================================
|
3
|
+
|
4
|
+
# == Exceptions ===========================================================
|
3
5
|
|
4
|
-
|
6
|
+
class BlockRequired < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
class TaskNotQueued < Exception
|
10
|
+
def initialize(task = nil)
|
11
|
+
@task = task
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"Task #{@task.inspect} not queued."
|
16
|
+
end
|
17
|
+
alias_method :to_s, :inspect
|
18
|
+
end
|
5
19
|
|
6
|
-
# ==
|
20
|
+
# == Extensions ===========================================================
|
7
21
|
|
8
|
-
|
22
|
+
# == Relationships ========================================================
|
23
|
+
|
24
|
+
# == Scopes ===============================================================
|
25
|
+
|
26
|
+
# == Callbacks ============================================================
|
27
|
+
|
28
|
+
# == Validations ==========================================================
|
9
29
|
|
10
30
|
# == Class Methods ========================================================
|
31
|
+
|
32
|
+
def self.filters
|
33
|
+
@filters ||= {
|
34
|
+
nil => lambda { |task| true }
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.filter(name, &block)
|
39
|
+
filters[name] = block
|
40
|
+
end
|
11
41
|
|
12
42
|
# == Instance Methods =====================================================
|
13
|
-
|
14
|
-
def initialize(
|
15
|
-
@
|
16
|
-
@
|
17
|
-
|
18
|
-
@
|
43
|
+
|
44
|
+
def initialize(&block)
|
45
|
+
@filter_lock = Mutex.new
|
46
|
+
@observer_lock = Mutex.new
|
47
|
+
|
48
|
+
@claimable_task = { }
|
49
|
+
@filters = self.class.filters.dup
|
50
|
+
@observers = { }
|
51
|
+
@next_task = { }
|
52
|
+
@insert_backlog = [ ]
|
53
|
+
|
54
|
+
if (block_given?)
|
55
|
+
@sort_by = block
|
56
|
+
else
|
57
|
+
@sort_by = lambda { |a,b| a.priority <=> b.priority }
|
58
|
+
end
|
59
|
+
|
60
|
+
@tasks = Pigeon::SortedArray.new(&@sort_by)
|
19
61
|
end
|
20
62
|
|
21
|
-
def
|
22
|
-
|
63
|
+
def sort_by(&block)
|
64
|
+
raise BlockRequired unless (block_given?)
|
23
65
|
|
24
|
-
|
25
|
-
|
66
|
+
@sort_by = block
|
67
|
+
@filter_lock.synchronize do
|
68
|
+
@tasks = Pigeon::SortedArray.new(&@sort_by) + @tasks
|
69
|
+
|
70
|
+
@next_task = { }
|
26
71
|
end
|
27
72
|
end
|
28
73
|
|
29
|
-
def
|
30
|
-
|
74
|
+
def observe(filter_name = nil, &block)
|
75
|
+
raise BlockRequired unless (block_given?)
|
76
|
+
|
77
|
+
@observer_lock.synchronize do
|
78
|
+
@observers[filter_name] ||= [ ]
|
79
|
+
end
|
80
|
+
|
81
|
+
@observers[filter_name] << block
|
82
|
+
|
83
|
+
task = assign_next_task(filter_name)
|
31
84
|
end
|
32
85
|
|
33
|
-
def
|
34
|
-
|
86
|
+
def filter(filter_name, &block)
|
87
|
+
raise BlockRequired unless (block_given?)
|
88
|
+
|
89
|
+
@filter_lock.synchronize do
|
90
|
+
@filters[filter_name] = block
|
91
|
+
end
|
92
|
+
|
93
|
+
assign_next_task(filter_name)
|
35
94
|
end
|
95
|
+
|
96
|
+
def <<(task)
|
97
|
+
# If there is an insert operation already in progress, put this task in
|
98
|
+
# the backlog for subsequent processing.
|
99
|
+
|
100
|
+
if (@observer_lock.locked?)
|
101
|
+
@insert_backlog << task
|
102
|
+
return task
|
103
|
+
end
|
104
|
+
|
105
|
+
active_task = task
|
106
|
+
|
107
|
+
while (active_task) do
|
108
|
+
# Set the claimable task flag for this task since it is not yet in the
|
109
|
+
# actual task queue.
|
110
|
+
@filter_lock.synchronize do
|
111
|
+
@claimable_task[active_task] = true
|
112
|
+
end
|
113
|
+
|
114
|
+
unless (@observers.empty?)
|
115
|
+
@observer_lock.synchronize do
|
116
|
+
@observers.each do |filter_name, list|
|
117
|
+
# Check if this task matches the filter restrictions, and if it
|
118
|
+
# does then call the observer chain in order.
|
119
|
+
if (@filters[filter_name].call(active_task))
|
120
|
+
@observers[filter_name].each do |proc|
|
121
|
+
case (proc.arity)
|
122
|
+
when 2
|
123
|
+
proc.call(self, active_task)
|
124
|
+
else
|
125
|
+
proc.call(active_task)
|
126
|
+
end
|
127
|
+
|
128
|
+
# An observer callback has the opportunity to claim a task,
|
129
|
+
# and if it does, the claimable task flag will be false. Loop
|
130
|
+
# only while the task is claimable.
|
131
|
+
break unless (@claimable_task[active_task])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# If this task wasn't claimed by an observer then insert it in the
|
139
|
+
# main task queue.
|
140
|
+
if (@claimable_task.delete(active_task))
|
141
|
+
@filter_lock.synchronize do
|
142
|
+
@tasks << active_task
|
143
|
+
|
144
|
+
# Update the next task slots for all of the unassigned filters and
|
145
|
+
# trigger observer callbacks as required.
|
146
|
+
@next_task.each do |filter_name, next_task|
|
147
|
+
next if (next_task)
|
148
|
+
|
149
|
+
if (@filters[filter_name].call(active_task))
|
150
|
+
@next_task[filter_name] = active_task
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
active_task = @insert_backlog.shift
|
157
|
+
end
|
36
158
|
|
37
|
-
|
38
|
-
@blocks.length
|
159
|
+
task
|
39
160
|
end
|
40
161
|
|
41
|
-
def
|
42
|
-
@
|
162
|
+
def each
|
163
|
+
@filter_lock.synchronize do
|
164
|
+
tasks = @tasks.dup
|
165
|
+
end
|
166
|
+
|
167
|
+
tasks.each do
|
168
|
+
yield(task)
|
169
|
+
end
|
43
170
|
end
|
44
171
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
172
|
+
def peek(filter_name = nil, &block)
|
173
|
+
if (block_given?)
|
174
|
+
@filter_lock.synchronize do
|
175
|
+
@tasks.find(&block)
|
176
|
+
end
|
177
|
+
else
|
178
|
+
@next_task[filter_name] ||= begin
|
179
|
+
@filter_lock.synchronize do
|
180
|
+
filter_proc = @filters[filter_name]
|
49
181
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
182
|
+
filter_proc and @tasks.find(&filter_proc)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def pull(filter_name = nil, &block)
|
189
|
+
unless (block_given?)
|
190
|
+
block = @filters[filter_name]
|
191
|
+
end
|
192
|
+
|
193
|
+
@filter_lock.synchronize do
|
194
|
+
tasks = @tasks.select(&block)
|
195
|
+
|
196
|
+
@tasks -= tasks
|
197
|
+
|
198
|
+
@next_task.each do |filter_name, next_task|
|
199
|
+
if (tasks.include?(@next_task[filter_name]))
|
200
|
+
@next_task[filter_name] = nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
tasks
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def pop(filter_name = nil, &block)
|
209
|
+
@filter_lock.synchronize do
|
210
|
+
task =
|
211
|
+
if (block_given?)
|
212
|
+
@tasks.find(&block)
|
213
|
+
else
|
214
|
+
@next_task[filter_name] || begin
|
215
|
+
filter_proc = @filters[filter_name]
|
216
|
+
|
217
|
+
filter_proc and @tasks.find(&filter_proc)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
if (task)
|
222
|
+
@tasks.delete(task)
|
223
|
+
|
224
|
+
@next_task.each do |filter_name, next_task|
|
225
|
+
if (task == next_task)
|
226
|
+
@next_task[filter_name] = nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
task
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def claim(task)
|
236
|
+
@filter_lock.synchronize do
|
237
|
+
if (@claimable_task[task])
|
238
|
+
@claimable_task[task] = false
|
239
|
+
elsif (@tasks.delete(task))
|
240
|
+
@next_task.each do |filter_name, next_task|
|
241
|
+
if (task == next_task)
|
242
|
+
@next_task[filter_name] = nil
|
57
243
|
end
|
58
|
-
|
59
|
-
Thread.pass
|
60
244
|
end
|
61
|
-
|
62
|
-
|
245
|
+
else
|
246
|
+
raise TaskNotQueued, task
|
63
247
|
end
|
64
248
|
end
|
249
|
+
|
250
|
+
task
|
251
|
+
end
|
252
|
+
|
253
|
+
def exist?(task)
|
254
|
+
@filter_lock.synchronize do
|
255
|
+
@tasks.exist?(task)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def empty?(filter_name = nil, &block)
|
260
|
+
if (block_given?)
|
261
|
+
@filter_lock.synchronize do
|
262
|
+
!@tasks.find(&block)
|
263
|
+
end
|
264
|
+
else
|
265
|
+
!peek(filter_name)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def length(filter_name = nil, &block)
|
270
|
+
filter_proc = @filters[filter_name]
|
271
|
+
|
272
|
+
@filter_lock.synchronize do
|
273
|
+
filter_proc ? @tasks.count(&filter_proc) : nil
|
274
|
+
end
|
275
|
+
end
|
276
|
+
alias_method :size, :length
|
277
|
+
alias_method :count, :length
|
278
|
+
|
279
|
+
def to_a
|
280
|
+
@filter_lock.synchronize do
|
281
|
+
@tasks.dup
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
protected
|
286
|
+
def assign_next_task(filter_name)
|
287
|
+
filter = @filters[filter_name]
|
288
|
+
|
289
|
+
return unless (filter)
|
290
|
+
|
291
|
+
if (task = @next_task[filter_name])
|
292
|
+
return task
|
293
|
+
end
|
294
|
+
|
295
|
+
@filter_lock.synchronize do
|
296
|
+
@next_task[filter_name] ||= @tasks.find(&filter)
|
297
|
+
end
|
65
298
|
end
|
66
299
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class Pigeon::Scheduler
|
2
|
+
# == Properties ===========================================================
|
3
|
+
|
4
|
+
attr_reader :queues
|
5
|
+
attr_reader :processors
|
6
|
+
|
7
|
+
# == Class Methods ========================================================
|
8
|
+
|
9
|
+
# == Instance Methods =====================================================
|
10
|
+
|
11
|
+
# Creates a new scheduler. If queue is specified, then that queue will
|
12
|
+
# become the default queue. One or more processors can be supplied to work
|
13
|
+
# with this queue, though they should be supplied bound to the queue in
|
14
|
+
# order to properly receive tasks.
|
15
|
+
def initialize(queue = nil, *processors)
|
16
|
+
@queues = {
|
17
|
+
nil => queue || Pigeon::Queue.new
|
18
|
+
}
|
19
|
+
|
20
|
+
processors.flatten!
|
21
|
+
|
22
|
+
@processors = processors.empty? ? [ Pigeon::Processor.new(@queues[nil]) ] : processors
|
23
|
+
end
|
24
|
+
|
25
|
+
# Adds one or more tasks to the schedule, where the tasks can be provided
|
26
|
+
# individually, as a list, or as an array.
|
27
|
+
def add(*tasks)
|
28
|
+
tasks.flatten.each do |task|
|
29
|
+
enqueue_task(task)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the default queue used for scheduling.
|
34
|
+
def default_queue
|
35
|
+
@queues[nil]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Used to assign the default queue.
|
39
|
+
def default_queue=(queue)
|
40
|
+
@queues[nil] = queue
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the queue with the given name if one is defined, nil otherwise.
|
44
|
+
def queue(queue_name)
|
45
|
+
@queues[queue_name]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the scheduler running.
|
49
|
+
def run!
|
50
|
+
@state = :running
|
51
|
+
end
|
52
|
+
|
53
|
+
# Pauses the scheduler which will prevent additional tasks from being
|
54
|
+
# initiated. Any tasks in progress will continue to run. Tasks can still
|
55
|
+
# be added but will not be executed until the scheduler is running.
|
56
|
+
def pause!
|
57
|
+
@state = :paused
|
58
|
+
end
|
59
|
+
|
60
|
+
# Stops the scheduler and clears out the queue. No new tasks will be
|
61
|
+
# accepted until the scheduler is in a paused or running state.
|
62
|
+
def stop!
|
63
|
+
@state = :stopped
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns true if the scheduler is running, false otherwise.
|
67
|
+
def running?
|
68
|
+
@state == :running
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns true if the scheduler is paused, false otherwise.
|
72
|
+
def paused?
|
73
|
+
@state == :paused
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if the scheduler is stopped, false otherwise.
|
77
|
+
def stopped?
|
78
|
+
@state == :stopped
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the number of tasks that have been queued up.
|
82
|
+
def queue_length
|
83
|
+
@queues.inject(0) do |length, (name, queue)|
|
84
|
+
length + queue.length
|
85
|
+
end
|
86
|
+
end
|
87
|
+
alias_method :queue_size, :queue_length
|
88
|
+
|
89
|
+
# Returns the number of processors that are attached to this scheduler.
|
90
|
+
def processors_count
|
91
|
+
@processors.length
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
# This method defines how to handle adding a task to the scheduler, which
|
96
|
+
# in this case simply puts it into the default queue. Subclasses should
|
97
|
+
# redefine this to organize tasks as required.
|
98
|
+
def enqueue_task(task)
|
99
|
+
default_queue << task
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Pigeon::SortedArray < Array
|
2
|
+
# == Exceptions ===========================================================
|
3
|
+
|
4
|
+
class SortArgumentRequired < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
# == Class Methods ========================================================
|
8
|
+
|
9
|
+
# == Instance Methods =====================================================
|
10
|
+
|
11
|
+
# Creates a new sorted array with an optional sort method supplied as
|
12
|
+
# a block. The sort method supplied should accept two arguments that are
|
13
|
+
# objects in the array to be compared and should return -1, 0, or 1 based
|
14
|
+
# on their sorting order. By default the comparison performed is <=>
|
15
|
+
def initialize(&sort_method)
|
16
|
+
sort_method ||= lambda { |a,b| a <=> b }
|
17
|
+
|
18
|
+
@sort_method =
|
19
|
+
case (sort_method && sort_method.arity)
|
20
|
+
when 2
|
21
|
+
sort_method
|
22
|
+
when 1
|
23
|
+
lambda { |a,b| sort_method.call(a) <=> sort_method.call(b) }
|
24
|
+
else
|
25
|
+
raise SortArgumentRequired
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Adds an object to the array by inserting it into the appropriate sorted
|
30
|
+
# location directly.
|
31
|
+
def <<(object)
|
32
|
+
low = 0
|
33
|
+
high = length
|
34
|
+
|
35
|
+
while (low != high)
|
36
|
+
mid = (high + low) / 2
|
37
|
+
|
38
|
+
comparison = @sort_method.call(object, at(mid))
|
39
|
+
|
40
|
+
if (comparison < 0)
|
41
|
+
high = mid
|
42
|
+
elsif (comparison > 0)
|
43
|
+
low = mid + 1
|
44
|
+
else
|
45
|
+
break
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
insert(low, object)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Combines another array with this one and returns the sorted result.
|
53
|
+
def +(array)
|
54
|
+
self.class[*super(array).sort(&@sort_method)]
|
55
|
+
end
|
56
|
+
end
|
data/lib/pigeon/support.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
1
3
|
module Pigeon::Support
|
2
4
|
# Uses the double-fork method to create a fully detached background
|
3
5
|
# process. Returns the process ID of the created process. May throw an
|
@@ -30,6 +32,19 @@ module Pigeon::Support
|
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
35
|
+
# Returns a unique 160-bit identifier for this engine expressed as a 40
|
36
|
+
# character hexadecimal string. The first 32-bit sequence is a timestamp
|
37
|
+
# so these numbers increase over time and can be used to identify when
|
38
|
+
# a particular instance was launched.
|
39
|
+
def unique_id
|
40
|
+
'%8x%s' % [
|
41
|
+
Time.now.to_i,
|
42
|
+
Digest::SHA1.hexdigest(
|
43
|
+
'%.8f%8x' % [ Time.now.to_f, rand(1 << 32) ]
|
44
|
+
)[0, 32]
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
33
48
|
# Make all methods callable directly without having to include it
|
34
49
|
extend self
|
35
50
|
end
|
data/lib/pigeon/task.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
class Pigeon::Task
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
# == Properties ===========================================================
|
5
|
+
|
6
|
+
attr_reader :state
|
7
|
+
attr_reader :engine
|
8
|
+
attr_reader :exception
|
9
|
+
attr_reader :created_at
|
10
|
+
attr_reader :started_at
|
11
|
+
|
12
|
+
# == Class Methods ========================================================
|
13
|
+
|
14
|
+
# Defines the initial state of this type of task. Default is :initialized
|
15
|
+
# but this can be customized in a subclass.
|
16
|
+
def self.initial_state
|
17
|
+
:initialized
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an array of the terminal states for this task. Default is
|
21
|
+
# :failed, :finished but this can be customized in a subclass.
|
22
|
+
def self.terminal_states
|
23
|
+
@terminal_states ||= [ :failed, :finished ].freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
# == Instance Methods =====================================================
|
27
|
+
|
28
|
+
def initialize(engine = nil)
|
29
|
+
@engine = engine || Pigeon::Engine.default_engine
|
30
|
+
@created_at = Time.now
|
31
|
+
|
32
|
+
after_initialized
|
33
|
+
end
|
34
|
+
|
35
|
+
# Kicks off the task. An optional callback is executed just before each
|
36
|
+
# state is excuted and is passed the state name as a symbol.
|
37
|
+
def run!(engine = nil, &callback)
|
38
|
+
@engine = engine if (engine)
|
39
|
+
@callback = callback if (block_given?)
|
40
|
+
|
41
|
+
@state = self.class.initial_state
|
42
|
+
@started_at = Time.now
|
43
|
+
|
44
|
+
run_state!(@state)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if the task is in the finished state, false otherwise.
|
48
|
+
def finished?
|
49
|
+
@state == :finished
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns true if the task is in the failed state, false otherwise.
|
53
|
+
def failed?
|
54
|
+
@state == :failed
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns true if an exception was thrown, false otherwise.
|
58
|
+
def exception?
|
59
|
+
!!@exception
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns true if the task is in any terminal state.
|
63
|
+
def terminal_state?
|
64
|
+
self.class.terminal_states.include?(@state)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Dispatches a block to be run as soon as possible.
|
68
|
+
def dispatch(&block)
|
69
|
+
@engine.dispatch(&block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a numerical priority order. If redefined in a subclass,
|
73
|
+
# should return a comparable value.
|
74
|
+
def priority
|
75
|
+
@created_at
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
"<#{self.class}\##{self.object_id}>"
|
80
|
+
end
|
81
|
+
|
82
|
+
def <=>(task)
|
83
|
+
self.priority <=> task.priority
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
def run_state!(state)
|
88
|
+
# Grab the current state and save it here, as it may switch at any time
|
89
|
+
@state = state
|
90
|
+
terminate = self.class.terminal_states.include?(state)
|
91
|
+
|
92
|
+
before_state(state)
|
93
|
+
|
94
|
+
send_callback(state) if (@callback)
|
95
|
+
|
96
|
+
unless (terminate)
|
97
|
+
state_method = :"state_#{state}!"
|
98
|
+
|
99
|
+
# Only perform this state action if it is defined, otherwise ignore
|
100
|
+
# as some states may be deliberately NOOP in order to wait for some
|
101
|
+
# action to be completed asynchronously.
|
102
|
+
if (respond_to?(state_method))
|
103
|
+
send(state_method)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
rescue Object => e
|
108
|
+
@exception = e
|
109
|
+
|
110
|
+
handle_exception(e) rescue nil
|
111
|
+
|
112
|
+
transition_to_state(:failed) unless (self.failed?)
|
113
|
+
|
114
|
+
self.after_failed
|
115
|
+
self.after_terminated
|
116
|
+
ensure
|
117
|
+
after_state(state)
|
118
|
+
|
119
|
+
if (terminate)
|
120
|
+
self.after_finished
|
121
|
+
|
122
|
+
# Send a final notification callback
|
123
|
+
if (@callback and @callback.arity == 0)
|
124
|
+
@callback.call
|
125
|
+
end
|
126
|
+
|
127
|
+
self.after_terminated
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Schedules the next state to be executed. This method should only be
|
132
|
+
# called once per state or it may result in duplicated state actions.
|
133
|
+
def transition_to_state(state)
|
134
|
+
@engine.dispatch do
|
135
|
+
run_state!(state)
|
136
|
+
end
|
137
|
+
|
138
|
+
state
|
139
|
+
end
|
140
|
+
|
141
|
+
def send_callback(state)
|
142
|
+
# State-notificaton callbacks are not made to blocks that do not take
|
143
|
+
# arguments, but instead a singe final callback is made.
|
144
|
+
case (@callback.arity)
|
145
|
+
when 2
|
146
|
+
@callback.call(self, state)
|
147
|
+
when 1
|
148
|
+
@callback.call(state)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Called just after the task is initialized.
|
153
|
+
def after_initialized
|
154
|
+
end
|
155
|
+
|
156
|
+
# Called before a particular state is executed.
|
157
|
+
def before_state(state)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Called after a particular state is executed.
|
161
|
+
def after_state(state)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Called just after the task is finished.
|
165
|
+
def after_finished
|
166
|
+
end
|
167
|
+
|
168
|
+
# Called just after the task fails.
|
169
|
+
def after_failed
|
170
|
+
end
|
171
|
+
|
172
|
+
# Called after the task finishes or terminates.
|
173
|
+
def after_terminated
|
174
|
+
end
|
175
|
+
|
176
|
+
# Called when an exception is thrown during processing with the exception
|
177
|
+
# passed as the first argument. Default behavior is to do nothing but
|
178
|
+
# this can be customized in a subclass. Any exceptions thrown by this
|
179
|
+
# method are ignored.
|
180
|
+
def handle_exception(exception)
|
181
|
+
end
|
182
|
+
|
183
|
+
# This defines the behaivor of the intialized state. By default this
|
184
|
+
# simply transitions to the finished state.
|
185
|
+
def state_initialized!
|
186
|
+
transition_to_state(:finished)
|
187
|
+
end
|
188
|
+
end
|