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/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
|