pigeon 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/README.rdoc
CHANGED
@@ -7,14 +7,12 @@ batch processing, or for providing specific network services.
|
|
7
7
|
Installation should be as simple as:
|
8
8
|
|
9
9
|
gem install pigeon
|
10
|
-
|
11
|
-
Your first Pigeon engine can be defined
|
10
|
+
|
11
|
+
Your first Pigeon engine can be defined by declaring a subclass:
|
12
12
|
|
13
13
|
class MyEngine < Pigeon::Engine
|
14
|
-
|
15
|
-
|
16
|
-
# Operations to be performed after start
|
17
|
-
end
|
14
|
+
after_start do
|
15
|
+
# Operations to be performed after start
|
18
16
|
end
|
19
17
|
end
|
20
18
|
|
@@ -30,10 +28,11 @@ A primary function of an engine might be to intermittently perform a task.
|
|
30
28
|
Several methods exist to facilitate this:
|
31
29
|
|
32
30
|
class MyEngine < Pigeon::Engine
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
after_start do
|
32
|
+
periodically_trigger_task(10) do
|
33
|
+
# Arbitrary block of code is executed every ten seconds but only
|
34
|
+
# one instance of this block can be running at a time.
|
35
|
+
do_stuff_every_ten_seconds
|
37
36
|
end
|
38
37
|
end
|
39
38
|
end
|
@@ -43,60 +42,37 @@ Starting your application can be done with a wrapper script that is constructed
|
|
43
42
|
An example would look like:
|
44
43
|
|
45
44
|
#!/usr/bin/env ruby
|
45
|
+
|
46
|
+
require 'rubygems'
|
47
|
+
gem 'pigeon'
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
+
# Adjust search path to include the ../lib directory
|
50
|
+
$LOAD_PATH << File.expand_path(
|
51
|
+
File.join(*%w[ .. lib ]), File.dirname(__FILE__)
|
52
|
+
)
|
49
53
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
# Use Pigeon::Launcher to launch your own engine by replacing
|
55
|
+
# the parameter Pigeon::Engine with your specific subclass.
|
56
|
+
Pigeon::Launcher.new(Pigeon::Engine).handle_args(ARGV)
|
57
|
+
|
58
|
+
== Components
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
engine.restart(options) do |old_pid, new_pid|
|
72
|
-
if (old_pid)
|
73
|
-
puts "Pigeon Engine terminated. [%d]" % old_pid
|
74
|
-
end
|
75
|
-
puts "Pigeon Engine now running. [%d]" % new_pid
|
76
|
-
end
|
77
|
-
when 'status'
|
78
|
-
engine.status(options) do |pid|
|
79
|
-
if (pid)
|
80
|
-
puts "Pigeon Engine running. [%d]" % pid
|
81
|
-
else
|
82
|
-
puts "Pigeon Engine is not running."
|
83
|
-
end
|
84
|
-
end
|
85
|
-
when 'run'
|
86
|
-
options[:logger] = Pigeon::Logger.new(STDOUT)
|
60
|
+
There are several key components used by Pigeon to create an event-driven
|
61
|
+
engine.
|
62
|
+
|
63
|
+
=== Pigeon::Dispatcher
|
64
|
+
|
65
|
+
The dispatcher functions as a thread pool for processing small, discrete
|
66
|
+
operations. These threads are created on demand and destroyed when no longer
|
67
|
+
in use. By limiting the number of threads a pool can contain it is possible
|
68
|
+
to schedule sequential operations, manage control over a single shared
|
69
|
+
resource, or to run through large lists of operations in parallel.
|
70
|
+
|
71
|
+
=== Pigeon::SortedArray
|
72
|
+
|
73
|
+
This utility class provides a simple self-sorting array. This is used as a
|
74
|
+
priority queue within the Pigeon::Queue.
|
87
75
|
|
88
|
-
engine.run(options) do |pid|
|
89
|
-
puts "Pigeon Engine now running. [%d]" % pid
|
90
|
-
puts "Use ^C to terminate."
|
91
|
-
end
|
92
|
-
else
|
93
|
-
puts "Usage: #{COMMAND_NAME} [start|stop|restart|status|run]"
|
94
|
-
end
|
95
|
-
rescue Interrupt
|
96
|
-
puts "Shutting down."
|
97
|
-
exit(0)
|
98
|
-
end
|
99
|
-
|
100
76
|
== Status
|
101
77
|
|
102
78
|
This engine is currently in development.
|
data/Rakefile
CHANGED
@@ -20,23 +20,10 @@ end
|
|
20
20
|
require 'rake/testtask'
|
21
21
|
Rake::TestTask.new(:test) do |test|
|
22
22
|
test.libs << 'lib' << 'test'
|
23
|
-
test.pattern = 'test
|
23
|
+
test.pattern = 'test/**/*_test.rb'
|
24
24
|
test.verbose = true
|
25
25
|
end
|
26
26
|
|
27
|
-
begin
|
28
|
-
require 'rcov/rcovtask'
|
29
|
-
Rcov::RcovTask.new do |test|
|
30
|
-
test.libs << 'test'
|
31
|
-
test.pattern = 'test/**/test_*.rb'
|
32
|
-
test.verbose = true
|
33
|
-
end
|
34
|
-
rescue LoadError
|
35
|
-
task :rcov do
|
36
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
27
|
task :test => :check_dependencies
|
41
28
|
|
42
29
|
task :default => :test
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/bin/launcher.example
CHANGED
@@ -1,4 +1,19 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# launcher Pigeon launcher
|
4
|
+
#
|
5
|
+
# chkconfig: - 90 10
|
6
|
+
# description: This is an example of a Pigeon launcher.
|
7
|
+
# processname: launcher
|
8
|
+
# pidfile: /var/run/pigeon.pid
|
9
|
+
#
|
10
|
+
### BEGIN INIT INFO
|
11
|
+
# Provides: pigeon
|
12
|
+
# Required-Start: $local_fs $remote_fs $network $named
|
13
|
+
# Required-Stop: $local_fs $remote_fs $network
|
14
|
+
# Short-Description: start and stop the Pigeon engine
|
15
|
+
# Description: This is an example of a Pigeon launcher.
|
16
|
+
### END INIT INFO
|
2
17
|
|
3
18
|
$LOAD_PATH << File.expand_path(File.join(*%w[ .. lib ]), File.dirname(__FILE__))
|
4
19
|
require 'pigeon'
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'thwait'
|
2
|
+
|
3
|
+
class Pigeon::Dispatcher
|
4
|
+
# == Extensions ===========================================================
|
5
|
+
|
6
|
+
extend Pigeon::OptionAccessor
|
7
|
+
|
8
|
+
# == Constants ============================================================
|
9
|
+
|
10
|
+
# == Properties ===========================================================
|
11
|
+
|
12
|
+
option_accessor :thread_limit,
|
13
|
+
:default => 24
|
14
|
+
|
15
|
+
attr_reader :exceptions
|
16
|
+
|
17
|
+
# == Class Methods ========================================================
|
18
|
+
|
19
|
+
# == Instance Methods =====================================================
|
20
|
+
|
21
|
+
# Creates a new instance of a dispatcher. An optional limit parameter is
|
22
|
+
# used to specify how many threads can be used, which if nil will use the
|
23
|
+
# concurrency_limit established for the class.
|
24
|
+
def initialize(limit = nil)
|
25
|
+
@thread_limit = limit
|
26
|
+
@backlog = [ ]
|
27
|
+
@threads = [ ]
|
28
|
+
@exceptions = [ ]
|
29
|
+
@sempaphore = Mutex.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(*args, &block)
|
33
|
+
@backlog << [ block, args, caller(0) ]
|
34
|
+
|
35
|
+
@sempaphore.synchronize do
|
36
|
+
if (@threads.length < self.thread_limit and @threads.length < @backlog.length)
|
37
|
+
create_thread
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns true if there are no operations in the backlog queue or running,
|
43
|
+
# false otherwise.
|
44
|
+
def empty?
|
45
|
+
@backlog.empty? and @threads.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns true if any exceptions have been generated, false otherwise.
|
49
|
+
def exceptions?
|
50
|
+
!@exceptions.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the number of items in the backlog queue.
|
54
|
+
def backlog_size
|
55
|
+
@backlog.length
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the current number of threads executing.
|
59
|
+
def thread_count
|
60
|
+
@threads.length
|
61
|
+
end
|
62
|
+
|
63
|
+
# Waits until all operations have completed, including the backlog.
|
64
|
+
def wait!
|
65
|
+
while (!@threads.empty?)
|
66
|
+
ThreadsWait.new(@threads).join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
def create_thread
|
72
|
+
@threads << Thread.new do
|
73
|
+
Thread.current.abort_on_exception = true
|
74
|
+
|
75
|
+
begin
|
76
|
+
while (block = @backlog.pop)
|
77
|
+
begin
|
78
|
+
block[0].call(*block[1])
|
79
|
+
rescue Object => e
|
80
|
+
puts "#{e.class}: #{e} #{e.backtrace.join("\n")}"
|
81
|
+
@exceptions << e
|
82
|
+
end
|
83
|
+
|
84
|
+
Thread.pass
|
85
|
+
end
|
86
|
+
ensure
|
87
|
+
@sempaphore.synchronize do
|
88
|
+
@threads.delete(Thread.current)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/pigeon/engine.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'eventmachine'
|
2
2
|
require 'socket'
|
3
|
-
require 'digest/sha1'
|
4
3
|
|
5
4
|
class Pigeon::Engine
|
6
5
|
# == Submodules ===========================================================
|
@@ -18,33 +17,30 @@ class Pigeon::Engine
|
|
18
17
|
# == Properties ===========================================================
|
19
18
|
|
20
19
|
option_accessor :logger
|
21
|
-
|
22
20
|
option_accessor :name
|
23
21
|
option_accessor :pid_file_name
|
24
22
|
option_accessor :foreground,
|
25
23
|
:boolean => true
|
26
24
|
option_accessor :debug,
|
27
25
|
:boolean => true
|
28
|
-
|
29
26
|
option_accessor :engine_log_name,
|
30
27
|
:default => 'engine.log'
|
31
28
|
option_accessor :engine_logger
|
32
|
-
|
33
29
|
option_accessor :query_log_name,
|
34
30
|
:default => 'query.log'
|
35
31
|
option_accessor :query_logger
|
36
|
-
|
37
32
|
option_accessor :try_pid_dirs,
|
38
33
|
:default => %w[
|
39
34
|
/var/run
|
40
35
|
/tmp
|
41
36
|
].freeze
|
42
|
-
|
43
37
|
option_accessor :try_log_dirs,
|
44
38
|
:default => %w[
|
45
39
|
/var/log
|
46
40
|
/tmp
|
47
41
|
].freeze
|
42
|
+
|
43
|
+
attr_reader :id
|
48
44
|
|
49
45
|
# == Constants ============================================================
|
50
46
|
|
@@ -89,6 +85,8 @@ class Pigeon::Engine
|
|
89
85
|
|
90
86
|
# Launches the engine with the specified options
|
91
87
|
def self.launch(options = nil)
|
88
|
+
engine = nil
|
89
|
+
|
92
90
|
EventMachine.run do
|
93
91
|
engine = new(options)
|
94
92
|
|
@@ -98,8 +96,12 @@ class Pigeon::Engine
|
|
98
96
|
engine.terminate
|
99
97
|
end
|
100
98
|
|
99
|
+
Pigeon::Engine.register_engine(engine)
|
100
|
+
|
101
101
|
engine.run
|
102
102
|
end
|
103
|
+
|
104
|
+
Pigeon::Engine.unregister_engine(engine)
|
103
105
|
end
|
104
106
|
|
105
107
|
def self.pid_file
|
@@ -181,10 +183,30 @@ class Pigeon::Engine
|
|
181
183
|
Pigeon::Logger.new(f)
|
182
184
|
end
|
183
185
|
end
|
186
|
+
|
187
|
+
# Returns a handle to the engine currently running, or nil if no engine is
|
188
|
+
# currently active.
|
189
|
+
def self.default_engine
|
190
|
+
@engines and @engines[0]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Registers the engine as running. The first engine running will show up
|
194
|
+
# as the default engine.
|
195
|
+
def self.register_engine(engine)
|
196
|
+
@engines ||= [ ]
|
197
|
+
@engines << engine
|
198
|
+
end
|
199
|
+
|
200
|
+
# Removes the engine from the list of running engines.
|
201
|
+
def self.unregister_engine(engine)
|
202
|
+
@engines.delete(engine)
|
203
|
+
end
|
184
204
|
|
185
205
|
# == Instance Methods =====================================================
|
186
206
|
|
187
207
|
def initialize(options = nil)
|
208
|
+
@id = Pigeon::Support.unique_id
|
209
|
+
|
188
210
|
@options = options || { }
|
189
211
|
|
190
212
|
@task_lock = Mutex.new
|
@@ -193,7 +215,7 @@ class Pigeon::Engine
|
|
193
215
|
self.logger ||= self.engine_logger
|
194
216
|
self.logger.level = Pigeon::Logger::DEBUG if (self.debug?)
|
195
217
|
|
196
|
-
@
|
218
|
+
@dispatcher = { }
|
197
219
|
|
198
220
|
run_chain(:after_initialize)
|
199
221
|
end
|
@@ -203,19 +225,6 @@ class Pigeon::Engine
|
|
203
225
|
Socket.gethostname
|
204
226
|
end
|
205
227
|
|
206
|
-
# Returns a unique 160-bit identifier for this engine expressed as a 40
|
207
|
-
# character hexadecimal string. The first 32-bit sequence is a timestamp
|
208
|
-
# so these numbers increase over time and can be used to identify when
|
209
|
-
# a particular instance was launched.
|
210
|
-
def id
|
211
|
-
@id ||= '%8x%s' % [
|
212
|
-
Time.now.to_i,
|
213
|
-
Digest::SHA1.hexdigest(
|
214
|
-
'%.8f%8x' % [ Time.now.to_f, rand(1 << 32) ]
|
215
|
-
)[0, 32]
|
216
|
-
]
|
217
|
-
end
|
218
|
-
|
219
228
|
# Handles the run phase of the engine, triggers the before_start and
|
220
229
|
# after_start events accordingly.
|
221
230
|
def run
|
@@ -256,7 +265,7 @@ class Pigeon::Engine
|
|
256
265
|
|
257
266
|
return if (@task_locks[task_name].locked?)
|
258
267
|
|
259
|
-
@
|
268
|
+
@task_locks[task_name].synchronize do
|
260
269
|
yield if (block_given?)
|
261
270
|
end
|
262
271
|
end
|
@@ -273,7 +282,7 @@ class Pigeon::Engine
|
|
273
282
|
|
274
283
|
# Used to defer a block of work for near-immediate execution. Is a
|
275
284
|
# wrapper around EventMachine#defer and does not perform as well as using
|
276
|
-
# the alternate
|
285
|
+
# the alternate dispatch method.
|
277
286
|
def defer(&block)
|
278
287
|
EventMachine.defer(&block)
|
279
288
|
end
|
@@ -288,12 +297,12 @@ class Pigeon::Engine
|
|
288
297
|
run_chain(:after_stop)
|
289
298
|
end
|
290
299
|
|
291
|
-
# Used to
|
300
|
+
# Used to dispatch a block for immediate processing on a background thread.
|
292
301
|
# An optional queue name can be used to sequence tasks properly. The main
|
293
302
|
# queue has a large number of threads, while the named queues default
|
294
303
|
# to only one so they can be processed sequentially.
|
295
|
-
def
|
296
|
-
target_queue = @
|
304
|
+
def dispatch(name = :default, &block)
|
305
|
+
target_queue = @dispatcher[name] ||= Pigeon::Dispatcher.new(name == :default ? nil : 1)
|
297
306
|
|
298
307
|
target_queue.perform(&block)
|
299
308
|
end
|
@@ -328,12 +337,12 @@ class Pigeon::Engine
|
|
328
337
|
|
329
338
|
# Returns true if the debug option was set, false otherwise.
|
330
339
|
def debug?
|
331
|
-
|
340
|
+
!!self.debug
|
332
341
|
end
|
333
342
|
|
334
343
|
# Returns true if running in the foreground, false otherwise.
|
335
344
|
def foreground?
|
336
|
-
|
345
|
+
!!self.foreground
|
337
346
|
end
|
338
347
|
|
339
348
|
protected
|
@@ -16,6 +16,8 @@ module Pigeon::OptionAccessor
|
|
16
16
|
# but these defaults can be over-ridden in subclasses and instances
|
17
17
|
# without interference. Optional hash at end of list can be used to set:
|
18
18
|
# * :default => Assigns a default value which is otherwise nil
|
19
|
+
# * :boolean => If true, creates an additional name? method and will
|
20
|
+
# convert all assigned values to a boolean true/false.
|
19
21
|
def option_reader(*names)
|
20
22
|
names = [ names ].flatten.compact
|
21
23
|
options = names.last.is_a?(Hash) ? names.pop : { }
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Pigeon::Processor
|
2
|
+
# == Exceptions ===========================================================
|
3
|
+
|
4
|
+
class AlreadyBoundToQueue < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
# == Constants ============================================================
|
8
|
+
|
9
|
+
# == Properties ===========================================================
|
10
|
+
|
11
|
+
attr_reader :task
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
# == Class Methods ========================================================
|
15
|
+
|
16
|
+
# == Instance Methods =====================================================
|
17
|
+
|
18
|
+
def initialize(queue = nil, &filter)
|
19
|
+
@id = Pigeon::Support.unique_id
|
20
|
+
@lock = Mutex.new
|
21
|
+
@filter = filter || lambda { |task| true }
|
22
|
+
|
23
|
+
self.queue = queue if (queue)
|
24
|
+
|
25
|
+
switch_to_next_task!
|
26
|
+
end
|
27
|
+
|
28
|
+
def queue=(queue)
|
29
|
+
raise AlreadyBoundToQueue, @queue if (@queue)
|
30
|
+
|
31
|
+
@queue = queue
|
32
|
+
|
33
|
+
@queue.observe do |task|
|
34
|
+
@lock.synchronize do
|
35
|
+
if (!@task and @filter.call(task))
|
36
|
+
@task = queue.claim(task)
|
37
|
+
|
38
|
+
@task.run! do
|
39
|
+
switch_to_next_task!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def accept?(task)
|
47
|
+
@filter.call(task)
|
48
|
+
end
|
49
|
+
|
50
|
+
def task?
|
51
|
+
!!@task
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
def switch_to_next_task!
|
56
|
+
@lock.synchronize do
|
57
|
+
@task = nil
|
58
|
+
|
59
|
+
if (@task = @queue.pop(&@filter))
|
60
|
+
@task.run! do
|
61
|
+
switch_to_next_task!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|