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