pigeon 0.1.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/.document +5 -0
- data/.gitignore +13 -0
- data/LICENSE +20 -0
- data/README.rdoc +107 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/bin/launcher.example +55 -0
- data/lib/pigeon.rb +7 -0
- data/lib/pigeon/engine.rb +246 -0
- data/lib/pigeon/logger.rb +15 -0
- data/lib/pigeon/pidfile.rb +35 -0
- data/lib/pigeon/queue.rb +66 -0
- data/lib/pigeon/support.rb +21 -0
- data/pigeon.gemspec +66 -0
- data/test/helper.rb +11 -0
- data/test/test_pigeon.rb +7 -0
- data/test/test_pigeon_engine.rb +50 -0
- data/test/test_pigeon_queue.rb +29 -0
- metadata +97 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Scott Tadman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
= pigeon
|
2
|
+
|
3
|
+
This is a simple framework for building EventMachine engines that are
|
4
|
+
constantly running. These are commonly used for background processing jobs,
|
5
|
+
batch processing, or for providing specific network services.
|
6
|
+
|
7
|
+
Installation should be as simple as:
|
8
|
+
|
9
|
+
gem install pigeon
|
10
|
+
|
11
|
+
Your first Pigeon engine can be defined quite simply:
|
12
|
+
|
13
|
+
class MyEngine < Pigeon::Engine
|
14
|
+
def self.included(engine)
|
15
|
+
engine.after_start do
|
16
|
+
# Operations to be performed after start
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Other handlers can be defined:
|
22
|
+
|
23
|
+
after_initialize
|
24
|
+
before_start
|
25
|
+
after_start
|
26
|
+
before_stop
|
27
|
+
after_stop
|
28
|
+
|
29
|
+
A primary function of an engine might be to intermittently perform a task.
|
30
|
+
Several methods exist to facilitate this:
|
31
|
+
|
32
|
+
class MyEngine < Pigeon::Engine
|
33
|
+
def self.included(engine)
|
34
|
+
engine.after_start do
|
35
|
+
engine.periodically_trigger_task(10) do
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Starting your application can be done with a wrapper script that is constructed somewhat like bin/launcher.example
|
42
|
+
|
43
|
+
An example would look like:
|
44
|
+
|
45
|
+
#!/usr/bin/env ruby
|
46
|
+
|
47
|
+
COMMAND_NAME = 'launcher'
|
48
|
+
engine = engine
|
49
|
+
|
50
|
+
options = {
|
51
|
+
:dir_mode => :normal,
|
52
|
+
:dir => engine.pid_dir,
|
53
|
+
:debug => true, # ((ENV['RAILS_ENV'] == 'production') ? ENV['PINGITY_DEBUG'] : true),
|
54
|
+
:modules => [ ]
|
55
|
+
}
|
56
|
+
|
57
|
+
begin
|
58
|
+
case (command)
|
59
|
+
when 'start'
|
60
|
+
engine.start(options) do |pid|
|
61
|
+
puts "Pigeon Engine now running. [%d]" % pid
|
62
|
+
end
|
63
|
+
when 'stop'
|
64
|
+
engine.stop(options) do |pid|
|
65
|
+
if (pid)
|
66
|
+
puts "Pigeon Engine shut down. [%d]" % pid
|
67
|
+
else
|
68
|
+
puts "Pigeon Engine was not running."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
when 'restart'
|
72
|
+
engine.restart(options) do |old_pid, new_pid|
|
73
|
+
if (old_pid)
|
74
|
+
puts "Pigeon Engine terminated. [%d]" % old_pid
|
75
|
+
end
|
76
|
+
puts "Pigeon Engine now running. [%d]" % new_pid
|
77
|
+
end
|
78
|
+
when 'status'
|
79
|
+
engine.status(options) do |pid|
|
80
|
+
if (pid)
|
81
|
+
puts "Pigeon Engine running. [%d]" % pid
|
82
|
+
else
|
83
|
+
puts "Pigeon Engine is not running."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
when 'run'
|
87
|
+
options[:logger] = Pigeon::Logger.new(STDOUT)
|
88
|
+
|
89
|
+
engine.run(options) do |pid|
|
90
|
+
puts "Pigeon Engine now running. [%d]" % pid
|
91
|
+
puts "Use ^C to terminate."
|
92
|
+
end
|
93
|
+
else
|
94
|
+
puts "Usage: #{COMMAND_NAME} [start|stop|restart|status|run]"
|
95
|
+
end
|
96
|
+
rescue Interrupt
|
97
|
+
puts "Shutting down."
|
98
|
+
exit(0)
|
99
|
+
end
|
100
|
+
|
101
|
+
== Status
|
102
|
+
|
103
|
+
This engine is currently in development.
|
104
|
+
|
105
|
+
== Copyright
|
106
|
+
|
107
|
+
Copyright (c) 2010 Scott Tadman, The Working Group
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "pigeon"
|
8
|
+
gem.summary = %Q{Simple daemonized EventMachine engine framework with plug-in support}
|
9
|
+
gem.description = %Q{Pigeon is a simple way to get started building an EventMachine engine that's intended to run as a background job.}
|
10
|
+
gem.email = "github@tadman.ca"
|
11
|
+
gem.homepage = "http://github.com/tadman/pigeon"
|
12
|
+
gem.authors = %w[ tadman ]
|
13
|
+
gem.add_development_dependency 'eventmachine'
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/**/test_*.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
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
|
+
task :test => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
47
|
+
|
48
|
+
rdoc.rdoc_dir = 'rdoc'
|
49
|
+
rdoc.title = "pigeon #{version}"
|
50
|
+
rdoc.rdoc_files.include('README*')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
COMMAND_NAME = 'launcher'
|
4
|
+
engine = engine
|
5
|
+
|
6
|
+
options = {
|
7
|
+
:dir_mode => :normal,
|
8
|
+
:dir => engine.pid_dir,
|
9
|
+
:debug => true, # ((ENV['RAILS_ENV'] == 'production') ? ENV['PINGITY_DEBUG'] : true),
|
10
|
+
:modules => [ ]
|
11
|
+
}
|
12
|
+
|
13
|
+
begin
|
14
|
+
case (command)
|
15
|
+
when 'start'
|
16
|
+
engine.start(options) do |pid|
|
17
|
+
puts "Pigeon Engine now running. [%d]" % pid
|
18
|
+
end
|
19
|
+
when 'stop'
|
20
|
+
engine.stop(options) do |pid|
|
21
|
+
if (pid)
|
22
|
+
puts "Pigeon Engine shut down. [%d]" % pid
|
23
|
+
else
|
24
|
+
puts "Pigeon Engine was not running."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
when 'restart'
|
28
|
+
engine.restart(options) do |old_pid, new_pid|
|
29
|
+
if (old_pid)
|
30
|
+
puts "Pigeon Engine terminated. [%d]" % old_pid
|
31
|
+
end
|
32
|
+
puts "Pigeon Engine now running. [%d]" % new_pid
|
33
|
+
end
|
34
|
+
when 'status'
|
35
|
+
engine.status(options) do |pid|
|
36
|
+
if (pid)
|
37
|
+
puts "Pigeon Engine running. [%d]" % pid
|
38
|
+
else
|
39
|
+
puts "Pigeon Engine is not running."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
when 'run'
|
43
|
+
options[:logger] = Pigeon::Logger.new(STDOUT)
|
44
|
+
|
45
|
+
engine.run(options) do |pid|
|
46
|
+
puts "Pigeon Engine now running. [%d]" % pid
|
47
|
+
puts "Use ^C to terminate."
|
48
|
+
end
|
49
|
+
else
|
50
|
+
puts "Usage: #{COMMAND_NAME} [start|stop|restart|status|run]"
|
51
|
+
end
|
52
|
+
rescue Interrupt
|
53
|
+
puts "Shutting down."
|
54
|
+
exit(0)
|
55
|
+
end
|
data/lib/pigeon.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
class Pigeon::Engine
|
5
|
+
# == Submodules ===========================================================
|
6
|
+
|
7
|
+
class RuntimeError < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
# == Properties ===========================================================
|
11
|
+
|
12
|
+
attr_reader :logger
|
13
|
+
|
14
|
+
# == Constants ============================================================
|
15
|
+
|
16
|
+
CHAINS = %w[
|
17
|
+
after_initialize
|
18
|
+
before_start
|
19
|
+
after_start
|
20
|
+
before_stop
|
21
|
+
after_stop
|
22
|
+
].collect(&:to_sym).freeze
|
23
|
+
|
24
|
+
PID_DIR = [
|
25
|
+
File.expand_path(File.join(*%w[ .. .. .. .. shared run ]), File.dirname(__FILE__)),
|
26
|
+
'/var/run',
|
27
|
+
'/tmp'
|
28
|
+
].find { |path| File.exist?(path) and File.writable?(path) }
|
29
|
+
|
30
|
+
LOG_DIR = [
|
31
|
+
File.expand_path(File.join(*%w[ .. .. .. .. shared log ] ), File.dirname(__FILE__)),
|
32
|
+
File.expand_path(File.join(*%w[ .. .. log ]), File.dirname(__FILE__)),
|
33
|
+
'/tmp'
|
34
|
+
].find { |path| File.exist?(path) and File.writable?(path) }
|
35
|
+
|
36
|
+
DEFAULT_OPTIONS = {
|
37
|
+
:pid_file => File.expand_path('pigeon-engine.pid', PID_DIR)
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
# == Class Methods ========================================================
|
41
|
+
|
42
|
+
def self.options_with_defaults(options = nil)
|
43
|
+
options ? DEFAULT_OPTIONS.merge(options) : DEFAULT_OPTIONS
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.launch_with_options(options = nil)
|
47
|
+
EventMachine.run do
|
48
|
+
new(options_with_defaults(options)).run
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.pid_dir
|
53
|
+
PID_DIR
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.pid_file(options = nil)
|
57
|
+
Pigeon::Pidfile.new(options_with_defaults(options)[:pid_file])
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.start(options = nil)
|
61
|
+
pid = Pigeon::Support.daemonize do
|
62
|
+
launch_with_options(options)
|
63
|
+
end
|
64
|
+
|
65
|
+
pid_file(options).create!(pid)
|
66
|
+
yield(pid.to_i)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.run(options = nil)
|
70
|
+
yield($$)
|
71
|
+
launch_with_options((options || { }).merge(:foreground => true))
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.stop(options = nil)
|
75
|
+
pf = pid_file(options)
|
76
|
+
pid = pf.contents
|
77
|
+
|
78
|
+
if (pid)
|
79
|
+
begin
|
80
|
+
Process.kill('QUIT', pid)
|
81
|
+
rescue Errno::ESRCH
|
82
|
+
# No such process exception
|
83
|
+
pid = nil
|
84
|
+
end
|
85
|
+
pf.remove!
|
86
|
+
end
|
87
|
+
|
88
|
+
yield(pid)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.restart(options = nil)
|
92
|
+
self.stop(options)
|
93
|
+
self.start(options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.status(options = nil)
|
97
|
+
yield(pid_file(options).contents)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.sql_logger
|
101
|
+
f = File.open(File.expand_path("query.log", LOG_DIR), 'w+')
|
102
|
+
f.sync = true
|
103
|
+
|
104
|
+
Pigeon::Logger.new(f)
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.log_dir
|
108
|
+
LOG_DIR
|
109
|
+
end
|
110
|
+
|
111
|
+
# == Instance Methods =====================================================
|
112
|
+
|
113
|
+
def initialize(options = nil)
|
114
|
+
@options = options || { }
|
115
|
+
|
116
|
+
@task_lock = { }
|
117
|
+
|
118
|
+
@logger = @options[:logger] || Pigeon::Logger.new(File.open(File.expand_path('engine.log', LOG_DIR), 'w+'))
|
119
|
+
|
120
|
+
@logger.level = Pigeon::Logger::DEBUG if (@options[:debug])
|
121
|
+
|
122
|
+
@queue = { }
|
123
|
+
|
124
|
+
run_chain(:after_initialize)
|
125
|
+
end
|
126
|
+
|
127
|
+
def run
|
128
|
+
run_chain(:before_start)
|
129
|
+
|
130
|
+
STDOUT.sync = true
|
131
|
+
|
132
|
+
@logger.info("Engine \##{id} Running")
|
133
|
+
|
134
|
+
run_chain(:after_start)
|
135
|
+
end
|
136
|
+
|
137
|
+
def host
|
138
|
+
Socket.gethostname
|
139
|
+
end
|
140
|
+
|
141
|
+
def id
|
142
|
+
@id ||= '%8x%8x' % [ Time.now.to_i, rand(1 << 32) ]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Used to periodically execute a task or block. When giving a task name,
|
146
|
+
# a method by that name is called, otherwise a block must be supplied.
|
147
|
+
# An interval can be specified in seconds, or will default to 1.
|
148
|
+
def periodically_trigger_task(task_name = nil, interval = 1, &block)
|
149
|
+
periodically(interval) do
|
150
|
+
trigger_task(task_name, &block)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# This acts as a lock to prevent over-lapping calls to the same method.
|
155
|
+
# While the first call is in progress, all subsequent calls will be ignored.
|
156
|
+
def trigger_task(task_name = nil, &block)
|
157
|
+
task_lock(task_name || block) do
|
158
|
+
block_given? ? yield : send(task_name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def task_lock(task_name)
|
163
|
+
# NOTE: This is a somewhat naive locking mechanism that may break down
|
164
|
+
# when two requests are fired off within a nearly identical period.
|
165
|
+
# For now, this achieves a general purpose solution that should work
|
166
|
+
# under most circumstances. Refactor later to improve.
|
167
|
+
|
168
|
+
return if (@task_lock[task_name])
|
169
|
+
|
170
|
+
@task_lock[task_name] = true
|
171
|
+
|
172
|
+
yield if (block_given?)
|
173
|
+
|
174
|
+
@task_lock[task_name] = false
|
175
|
+
end
|
176
|
+
|
177
|
+
def timer(interval, &block)
|
178
|
+
EventMachine::Timer.new(interval, &block)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Periodically calls a block. No check is performed to see if the block is
|
182
|
+
# already executing.
|
183
|
+
def periodically(interval, &block)
|
184
|
+
EventMachine::PeriodicTimer.new(interval, &block)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Used to defer a block of work for near-immediate execution. Uses the
|
188
|
+
# EventMachine#defer method but is not as efficient as the queue method.
|
189
|
+
def defer(&block)
|
190
|
+
EventMachine.defer(&block)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Shuts down the engine.
|
194
|
+
def terminate
|
195
|
+
EventMachine.stop_event_loop
|
196
|
+
end
|
197
|
+
|
198
|
+
# Used to queue a block for immediate processing on a background thread.
|
199
|
+
# An optional queue name can be used to sequence tasks properly.
|
200
|
+
def queue(name = :default, &block)
|
201
|
+
target_queue = @queue[name] ||= Pigeon::Queue.new(name == :default ? nil : 1)
|
202
|
+
|
203
|
+
target_queue.perform(&block)
|
204
|
+
end
|
205
|
+
|
206
|
+
class << self
|
207
|
+
CHAINS.each do |chain_name|
|
208
|
+
define_method(chain_name) do |&block|
|
209
|
+
chain_iv = :"@_#{chain_name}_chain"
|
210
|
+
instance_variable_set(chain_iv, [ ]) unless (instance_variable_get(chain_iv))
|
211
|
+
|
212
|
+
chain = instance_variable_get(chain_iv)
|
213
|
+
|
214
|
+
unless (chain)
|
215
|
+
chain = [ ]
|
216
|
+
instance_variable_set(chain_iv, chain)
|
217
|
+
end
|
218
|
+
|
219
|
+
chain << block
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def run_chain(chain_name, instance)
|
224
|
+
chain = instance_variable_get(:"@_#{chain_name}_chain")
|
225
|
+
|
226
|
+
return unless (chain)
|
227
|
+
|
228
|
+
chain.each do |proc|
|
229
|
+
instance.instance_eval(&proc)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def debug?
|
235
|
+
!!@options[:debug]
|
236
|
+
end
|
237
|
+
|
238
|
+
def foreground?
|
239
|
+
!!@options[:foreground]
|
240
|
+
end
|
241
|
+
|
242
|
+
protected
|
243
|
+
def run_chain(chain_name)
|
244
|
+
self.class.run_chain(chain_name, self)
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class Pigeon::Logger < Logger
|
4
|
+
# Returns a sequential thread identifier which is human readable and much
|
5
|
+
# more concise than internal numbering system used.
|
6
|
+
def thread_id
|
7
|
+
@threads ||= { }
|
8
|
+
@threads[Thread.current.object_id] ||= @threads.length
|
9
|
+
end
|
10
|
+
|
11
|
+
# Over-rides the default log format.
|
12
|
+
def format_message(severity, datetime, progname, msg)
|
13
|
+
"[%s %6d] %s\n" % [ datetime.strftime("%Y-%m-%d %H:%M:%S"), thread_id, msg ]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Pigeon::Pidfile
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
# == Class Methods ========================================================
|
5
|
+
|
6
|
+
# == Instance Methods =====================================================
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@path = path
|
10
|
+
|
11
|
+
@path += '.pid' unless (@path.match(/\./))
|
12
|
+
end
|
13
|
+
|
14
|
+
def contents
|
15
|
+
File.read(@path).to_i
|
16
|
+
rescue Errno::ENOENT
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def create!(pid = nil)
|
21
|
+
open(@path, 'w') do |fh|
|
22
|
+
fh.puts pid || $$
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove!
|
27
|
+
return unless (exists?)
|
28
|
+
|
29
|
+
File.unlink(@path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def exists?
|
33
|
+
File.exist?(@path)
|
34
|
+
end
|
35
|
+
end
|
data/lib/pigeon/queue.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Pigeon::Queue
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
DEFAULT_CONCURRENCY_LIMIT = 24
|
5
|
+
|
6
|
+
# == Properties ===========================================================
|
7
|
+
|
8
|
+
attr_reader :exceptions
|
9
|
+
|
10
|
+
# == Class Methods ========================================================
|
11
|
+
|
12
|
+
# == Instance Methods =====================================================
|
13
|
+
|
14
|
+
def initialize(limit = nil)
|
15
|
+
@limit = limit || DEFAULT_CONCURRENCY_LIMIT
|
16
|
+
@blocks = [ ]
|
17
|
+
@threads = [ ]
|
18
|
+
@exceptions = [ ]
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform(*args, &block)
|
22
|
+
@blocks << [ block, args, caller(0) ]
|
23
|
+
|
24
|
+
if (@threads.length < @limit and @threads.length < @blocks.length)
|
25
|
+
create_thread
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
@blocks.empty? and @threads.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def exceptions?
|
34
|
+
!@exceptions.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def length
|
38
|
+
@blocks.length
|
39
|
+
end
|
40
|
+
|
41
|
+
def threads
|
42
|
+
@threads.length
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
def create_thread
|
47
|
+
@threads << Thread.new do
|
48
|
+
Thread.current.abort_on_exception = true
|
49
|
+
|
50
|
+
begin
|
51
|
+
while (block = @blocks.pop)
|
52
|
+
begin
|
53
|
+
block[0].call(*block[1])
|
54
|
+
rescue Object => e
|
55
|
+
puts "#{e.class}: #{e} #{e.backtrace.join("\n")}"
|
56
|
+
@exceptions << e
|
57
|
+
end
|
58
|
+
|
59
|
+
Thread.pass
|
60
|
+
end
|
61
|
+
ensure
|
62
|
+
@threads.delete(Thread.current)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pigeon::Support
|
2
|
+
def self.daemonize
|
3
|
+
rfd, wfd = IO.pipe
|
4
|
+
|
5
|
+
forked_pid = fork do
|
6
|
+
daemon_pid = fork do
|
7
|
+
yield
|
8
|
+
end
|
9
|
+
|
10
|
+
wfd.puts daemon_pid
|
11
|
+
wfd.flush
|
12
|
+
wfd.close
|
13
|
+
end
|
14
|
+
|
15
|
+
Process.wait(forked_pid)
|
16
|
+
|
17
|
+
daemon_pid = rfd.readline
|
18
|
+
|
19
|
+
daemon_pid.to_i
|
20
|
+
end
|
21
|
+
end
|
data/pigeon.gemspec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{pigeon}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["tadman"]
|
12
|
+
s.date = %q{2010-10-01}
|
13
|
+
s.default_executable = %q{launcher.example}
|
14
|
+
s.description = %q{Pigeon is a simple way to get started building an EventMachine engine that's intended to run as a background job.}
|
15
|
+
s.email = %q{github@tadman.ca}
|
16
|
+
s.executables = ["launcher.example"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".gitignore",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/launcher.example",
|
29
|
+
"lib/pigeon.rb",
|
30
|
+
"lib/pigeon/engine.rb",
|
31
|
+
"lib/pigeon/logger.rb",
|
32
|
+
"lib/pigeon/pidfile.rb",
|
33
|
+
"lib/pigeon/queue.rb",
|
34
|
+
"lib/pigeon/support.rb",
|
35
|
+
"pigeon.gemspec",
|
36
|
+
"test/helper.rb",
|
37
|
+
"test/test_pigeon.rb",
|
38
|
+
"test/test_pigeon_engine.rb",
|
39
|
+
"test/test_pigeon_queue.rb"
|
40
|
+
]
|
41
|
+
s.homepage = %q{http://github.com/tadman/pigeon}
|
42
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
43
|
+
s.require_paths = ["lib"]
|
44
|
+
s.rubygems_version = %q{1.3.7}
|
45
|
+
s.summary = %q{Simple daemonized EventMachine engine framework with plug-in support}
|
46
|
+
s.test_files = [
|
47
|
+
"test/helper.rb",
|
48
|
+
"test/test_pigeon.rb",
|
49
|
+
"test/test_pigeon_engine.rb",
|
50
|
+
"test/test_pigeon_queue.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_development_dependency(%q<eventmachine>, [">= 0"])
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<eventmachine>, [">= 0"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<eventmachine>, [">= 0"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
data/test/helper.rb
ADDED
data/test/test_pigeon.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module TestModule
|
4
|
+
def self.included(engine)
|
5
|
+
engine.after_start do
|
6
|
+
notify_was_started
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestEngine < Pigeon::Engine
|
12
|
+
include TestModule
|
13
|
+
|
14
|
+
def notify_was_started
|
15
|
+
pipe = @options[:pipe]
|
16
|
+
|
17
|
+
pipe.puts("STARTED")
|
18
|
+
pipe.flush
|
19
|
+
pipe.close
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class TestPigeonEngine < Test::Unit::TestCase
|
24
|
+
def test_create_subclass
|
25
|
+
engine_pid = nil
|
26
|
+
|
27
|
+
read_fd, write_fd = IO.pipe
|
28
|
+
|
29
|
+
TestEngine.start(:pipe => write_fd) do |pid|
|
30
|
+
assert pid
|
31
|
+
engine_pid = pid
|
32
|
+
end
|
33
|
+
|
34
|
+
Timeout::timeout(5) do
|
35
|
+
assert_equal "STARTED\n", read_fd.readline
|
36
|
+
end
|
37
|
+
|
38
|
+
TestEngine.status do |pid|
|
39
|
+
assert_equal engine_pid, pid
|
40
|
+
end
|
41
|
+
|
42
|
+
TestEngine.stop do |pid|
|
43
|
+
assert_equal engine_pid, pid
|
44
|
+
end
|
45
|
+
|
46
|
+
TestEngine.status do |pid|
|
47
|
+
assert_equal nil, pid
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class PigeonQueueTest < Test::Unit::TestCase
|
4
|
+
def test_simple_queue
|
5
|
+
queue = Pigeon::Queue.new
|
6
|
+
|
7
|
+
checks = { }
|
8
|
+
|
9
|
+
count = 1000
|
10
|
+
|
11
|
+
count.times do |n|
|
12
|
+
queue.perform do
|
13
|
+
x = 0
|
14
|
+
10_000.times { x += 1 }
|
15
|
+
checks[n] = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
while (!queue.empty?)
|
20
|
+
sleep(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
assert queue.empty?
|
24
|
+
assert_equal [ ], queue.exceptions
|
25
|
+
assert !queue.exceptions?
|
26
|
+
|
27
|
+
assert_equal (0..count - 1).to_a, checks.keys.sort
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pigeon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- tadman
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-01 00:00:00 -04:00
|
18
|
+
default_executable: launcher.example
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
description: Pigeon is a simple way to get started building an EventMachine engine that's intended to run as a background job.
|
34
|
+
email: github@tadman.ca
|
35
|
+
executables:
|
36
|
+
- launcher.example
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- LICENSE
|
41
|
+
- README.rdoc
|
42
|
+
files:
|
43
|
+
- .document
|
44
|
+
- .gitignore
|
45
|
+
- LICENSE
|
46
|
+
- README.rdoc
|
47
|
+
- Rakefile
|
48
|
+
- VERSION
|
49
|
+
- bin/launcher.example
|
50
|
+
- lib/pigeon.rb
|
51
|
+
- lib/pigeon/engine.rb
|
52
|
+
- lib/pigeon/logger.rb
|
53
|
+
- lib/pigeon/pidfile.rb
|
54
|
+
- lib/pigeon/queue.rb
|
55
|
+
- lib/pigeon/support.rb
|
56
|
+
- pigeon.gemspec
|
57
|
+
- test/helper.rb
|
58
|
+
- test/test_pigeon.rb
|
59
|
+
- test/test_pigeon_engine.rb
|
60
|
+
- test/test_pigeon_queue.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: http://github.com/tadman/pigeon
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --charset=UTF-8
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.3.7
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Simple daemonized EventMachine engine framework with plug-in support
|
93
|
+
test_files:
|
94
|
+
- test/helper.rb
|
95
|
+
- test/test_pigeon.rb
|
96
|
+
- test/test_pigeon_engine.rb
|
97
|
+
- test/test_pigeon_queue.rb
|