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 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 quite simply:
10
+
11
+ Your first Pigeon engine can be defined by declaring a subclass:
12
12
 
13
13
  class MyEngine < Pigeon::Engine
14
- def self.included(engine)
15
- engine.after_start do
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
- def self.included(engine)
34
- engine.after_start do
35
- engine.periodically_trigger_task(10) do
36
- end
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
- COMMAND_NAME = 'launcher'
48
- engine = engine
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
- options = {
51
- :dir => engine.pid_dir,
52
- :debug => true, # ((ENV['RAILS_ENV'] == 'production') ? ENV['PINGITY_DEBUG'] : true),
53
- :modules => [ ]
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
- begin
57
- case (command)
58
- when 'start'
59
- engine.start(options) do |pid|
60
- puts "Pigeon Engine now running. [%d]" % pid
61
- end
62
- when 'stop'
63
- engine.stop(options) do |pid|
64
- if (pid)
65
- puts "Pigeon Engine shut down. [%d]" % pid
66
- else
67
- puts "Pigeon Engine was not running."
68
- end
69
- end
70
- when 'restart'
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/**/test_*.rb'
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.3.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
- @queue = { }
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
- @task_lock[task_name].synchronize do
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 queue method.
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 queue a block for immediate processing on a background thread.
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 queue(name = :default, &block)
296
- target_queue = @queue[name] ||= Pigeon::Queue.new(name == :default ? nil : 1)
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
- !!@options[:debug]
340
+ !!self.debug
332
341
  end
333
342
 
334
343
  # Returns true if running in the foreground, false otherwise.
335
344
  def foreground?
336
- !!@options[:foreground]
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