task_tempest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/CHANGELOG +2 -0
- data/LICENSE +20 -0
- data/README.rdoc +132 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/examples/my_tempest.rb +134 -0
- data/examples/tasks/evaler.rb +7 -0
- data/examples/tasks/greeter.rb +8 -0
- data/lib/task_tempest/active_support.rb +24 -0
- data/lib/task_tempest/bookkeeper.rb +87 -0
- data/lib/task_tempest/bootstrap.rb +77 -0
- data/lib/task_tempest/callbacks.rb +29 -0
- data/lib/task_tempest/engine.rb +168 -0
- data/lib/task_tempest/error_handling.rb +52 -0
- data/lib/task_tempest/require.rb +27 -0
- data/lib/task_tempest/settings.rb +144 -0
- data/lib/task_tempest/task.rb +54 -0
- data/lib/task_tempest/task_logger.rb +18 -0
- data/lib/task_tempest.rb +13 -0
- data/task_tempest.gemspec +68 -0
- data/test/helper.rb +10 -0
- data/test/test_task_tempest.rb +5 -0
- metadata +95 -0
data/.document
ADDED
data/.gitignore
ADDED
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Christopher J. Bottaro
|
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,132 @@
|
|
1
|
+
= task_tempest
|
2
|
+
|
3
|
+
Framework for building threaded asynchronous job processors.
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
task_tempest lets you build glorified loops. You set some configuration options, define some callbacks, then it just loops, reading messages from a queue and processing them.
|
8
|
+
|
9
|
+
You define the queue, how to read from the queue, how to write to the queue and the classes that process the queue messages. The class that does the looping is called the tempest and the classes that handle job are called tasks.
|
10
|
+
|
11
|
+
== Defining a tempest
|
12
|
+
|
13
|
+
Defining a tempest is simple. You just derive off of <tt>TaskTempest::Engine</tt>, set some configuration options and callbacks, then just instantiate the class and call +run+.
|
14
|
+
|
15
|
+
require "task_tempest"
|
16
|
+
class MyTempest < TaskTempest::Engine
|
17
|
+
process_name "my_tempest"
|
18
|
+
|
19
|
+
queue do |logger|
|
20
|
+
logger.debug "initializing queue"
|
21
|
+
SuperCoolQueue.new(...)
|
22
|
+
end
|
23
|
+
|
24
|
+
dequeue do |queue, logger|
|
25
|
+
message = queue.pop
|
26
|
+
if message
|
27
|
+
logger.info "message received"
|
28
|
+
YAML.load(message.body)
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
+queue+ is given a logger object and required to return an instance of your queue.
|
36
|
+
|
37
|
+
+dequeue+ is given the queue object and a logger object and required to return a tuple <tt>[task_id, task_class_name, *task_args]</tt>.
|
38
|
+
|
39
|
+
There are many more configuration options to set and callbacks to define, please see the rdocs.
|
40
|
+
|
41
|
+
TODO provide rdocs.
|
42
|
+
|
43
|
+
== Running a tempest
|
44
|
+
|
45
|
+
You simply instantiate the tempest and call +run+.
|
46
|
+
|
47
|
+
MyTempest.new.run
|
48
|
+
|
49
|
+
Catching an +Interrupt+ or +SystemExit+ exception will attempt a graceful shutdown, as will catching a <tt>SIGUSR2</tt> signal. Catching a +SIGTERM+ signal will try to exit immediately.
|
50
|
+
|
51
|
+
== Running as a daemon
|
52
|
+
|
53
|
+
There is no code in +task_tempest+ to run as a daemon, that is left to you. It's easy very easy with the {Daemons}[http://rubygems.org/gems/daemons] gem though.
|
54
|
+
|
55
|
+
Assuming your tempest is defined in <tt>my_tempest.rb</tt>, just put the following code at the bottom of the file.
|
56
|
+
|
57
|
+
if $0 == __FILE__
|
58
|
+
require "daemons"
|
59
|
+
Daemons.run_proc(MyTempest.settings.process_name, :log_output => true) do
|
60
|
+
MyTempest.new.run
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Now you can run it as a daemon from the command line.
|
65
|
+
|
66
|
+
ruby my_tempest.rb start
|
67
|
+
ruby my_tempest.rb stop
|
68
|
+
ruby my_tempest.rb run # Run in foreground
|
69
|
+
|
70
|
+
See the {rdoc}[http://daemons.rubyforge.org/] for {Daemons}[http://rubygems.org/gems/daemons] for more info.
|
71
|
+
|
72
|
+
== Defining a task
|
73
|
+
|
74
|
+
A task is what handles messages pulled off the queue.
|
75
|
+
|
76
|
+
require "task_tempest"
|
77
|
+
class GreeterTask < TaskTempest::Task
|
78
|
+
def start(person, greeting)
|
79
|
+
logger.info "about to greet #{person}"
|
80
|
+
puts "#{greeting}, #{person}!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
+start+ can take whatever arguments you want, but it must correspond with the arguments you put in the message.
|
85
|
+
|
86
|
+
== Messages
|
87
|
+
|
88
|
+
A message is what is returned by the +dequeue+ callback. They are simply arrays of the form <tt>[task_id, task_class_name, *task_args]</tt>. Note that if +task_id+ is nil, then one will be generated for automatically.
|
89
|
+
|
90
|
+
An message like...
|
91
|
+
|
92
|
+
[nil, "GreeterTask", "Christopher", "Hello"]
|
93
|
+
|
94
|
+
...would cause our +GreeterTask+ to puts "Hello, Christopher!".
|
95
|
+
|
96
|
+
== Submitting tasks
|
97
|
+
|
98
|
+
You can push messages on to the queue however you like, but +task_tempest+ provides a little convenience. Assuming our previous examples...
|
99
|
+
|
100
|
+
task = GreeterTask.new("Christopher", "Hello")
|
101
|
+
MyTempest.submit(task)
|
102
|
+
|
103
|
+
or
|
104
|
+
|
105
|
+
message = [nil, "GreeterTask", "Christopher", "Hello"]
|
106
|
+
MyTempest.submit(message)
|
107
|
+
|
108
|
+
For this to work, you need to define the +enqueue+ callback in your tempest definition.
|
109
|
+
|
110
|
+
require "task_tempest"
|
111
|
+
class MyTempest < TaskTempest::Engine
|
112
|
+
...
|
113
|
+
enqueue do |queue, message, logger, *args|
|
114
|
+
logger.info "enqueuing message #{message.inspect}"
|
115
|
+
queue.push(YAML.dump(message))
|
116
|
+
end
|
117
|
+
...
|
118
|
+
end
|
119
|
+
|
120
|
+
The +args+ argument is passed through via <tt>TaskTempest::Engine.submit</tt>. For example, if you called +submit+ like...
|
121
|
+
|
122
|
+
MyTempest.submit(message, "one", "two")
|
123
|
+
|
124
|
+
Then +args+ would be <tt>["one", "two"]</tt>. This is useful if you are using a priority queue and you want to submit your task with a given priority.
|
125
|
+
|
126
|
+
== Complete example
|
127
|
+
|
128
|
+
See the +examples+ directory.
|
129
|
+
|
130
|
+
== Copyright
|
131
|
+
|
132
|
+
Copyright (c) 2010 Christopher J. Bottaro. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "task_tempest"
|
8
|
+
gem.summary = %Q{Framework for creating asychronous job processors.}
|
9
|
+
gem.description = %Q{Framework for creating queue based, threaded asychronous job processors.}
|
10
|
+
gem.email = "cjbottaro@alumni.cs.utexas.edu"
|
11
|
+
gem.homepage = "http://github.com/cjbottaro/task_tempest"
|
12
|
+
gem.authors = ["Christopher J. Bottaro"]
|
13
|
+
# gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "task_tempest #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,134 @@
|
|
1
|
+
$LOAD_PATH << "../lib"
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "task_tempest"
|
5
|
+
require "memcache"
|
6
|
+
require "system_timer"
|
7
|
+
|
8
|
+
require "tasks/evaler"
|
9
|
+
require "tasks/greeter"
|
10
|
+
|
11
|
+
class MemcachedQueue
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
@name = name
|
15
|
+
@cache = MemCache.new "localhost:11211"
|
16
|
+
end
|
17
|
+
|
18
|
+
def push(item)
|
19
|
+
queue = @cache.fetch(@name){ [] }
|
20
|
+
queue.push(item)
|
21
|
+
@cache.set(@name, queue)
|
22
|
+
end
|
23
|
+
|
24
|
+
def pop
|
25
|
+
queue = @cache.fetch(@name){ [] }
|
26
|
+
item = queue.pop
|
27
|
+
@cache.set(@name, queue)
|
28
|
+
item
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# To run this example, open two shells and navagate to the examples directory (i.e. the
|
34
|
+
# the directory containing this file). In the first shell type:
|
35
|
+
# ruby my_tempest.rb run
|
36
|
+
# In the second shell, invoke irb and type the following commands:
|
37
|
+
# require "my_tempest"
|
38
|
+
# MyTempest.submit(Greeter.new("Christopher", "Hello"))
|
39
|
+
# MyTempest.submit([nil, "Greeter", "Justin", "What up"])
|
40
|
+
# Check the the first shell (and the logs dir) for output.
|
41
|
+
# Note this example requires the SystemTimer, daemons and memcache-client gems.
|
42
|
+
class MyTempest < TaskTempest::Engine
|
43
|
+
|
44
|
+
# This dictates what the logs will be named.
|
45
|
+
process_name "my_tempest"
|
46
|
+
|
47
|
+
# How many threads.
|
48
|
+
threads 5
|
49
|
+
|
50
|
+
# Where to write the log files.
|
51
|
+
log_dir "log"
|
52
|
+
|
53
|
+
# Where to look for task classes. Will require each .rb file in this directory.
|
54
|
+
task_dir "tasks"
|
55
|
+
|
56
|
+
# Time in seconds between each bookkeeping event.
|
57
|
+
bookkeeping_interval 15
|
58
|
+
|
59
|
+
# Don't display log messages below this level.
|
60
|
+
log_level Logger::INFO
|
61
|
+
|
62
|
+
# Maximum time in seconds a task is allowed to take before it is aborted.
|
63
|
+
task_timeout 5
|
64
|
+
|
65
|
+
# What timeout method to use. Timeout.timeout is unreliable.
|
66
|
+
timeout_method SystemTimer.method(:timeout_after)
|
67
|
+
|
68
|
+
# Define the queue.
|
69
|
+
queue do |logger|
|
70
|
+
MemcachedQueue.new("my_tempest_queue")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Define how to enqueue messages. This is used by MyTempest.submit.
|
74
|
+
# message is a tuple [task_id, task_class_name, *task_arguments].
|
75
|
+
# *args are passed through from MyTempest.submit.
|
76
|
+
enqueue do |queue, message, logger, *args|
|
77
|
+
logger.debug "enqueue #{message.inspect}"
|
78
|
+
queue.push(message)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define how to dequeue messages. It must return either
|
82
|
+
# nil or a tuple: [task_id, task_class_name, *task_arguments]
|
83
|
+
dequeue do |queue, logger|
|
84
|
+
if (message = queue.pop)
|
85
|
+
logger.debug "dequeue #{message.inspect}"
|
86
|
+
message
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Callback that happens after #init_logging, but before #bootstrap.
|
93
|
+
before_initialize do |logger|
|
94
|
+
end
|
95
|
+
|
96
|
+
# Callback that happens after #bootstrap.
|
97
|
+
after_initialize do |logger|
|
98
|
+
end
|
99
|
+
|
100
|
+
# Callback for an exception that happens in TaskTempest::Engine.
|
101
|
+
on_internal_exception do |e, logger|
|
102
|
+
puts "(I) #{e.class}: #{e.message}"
|
103
|
+
end
|
104
|
+
|
105
|
+
# Callback that happens when an exception occurs in a task.
|
106
|
+
on_task_exception do |task, e, logger|
|
107
|
+
puts "(T:#{task_id}) #{e.class}: #{e.message}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Callback that happens when a task exceeds the task_timeout setting.
|
111
|
+
on_task_timeout do |task, logger|
|
112
|
+
puts "(T:#{task.id}) timed out"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Callback that happens when a task calls Kernel.require.
|
116
|
+
on_require do |task, files, logger|
|
117
|
+
puts ("(T:#{task.id}) required files")
|
118
|
+
end
|
119
|
+
|
120
|
+
# Callback that happens when bookkeeping is done.
|
121
|
+
on_bookkeeping do |book, logger|
|
122
|
+
if book[:files][:total_count] > 100
|
123
|
+
puts "you have a lot of open files!"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
if $0 == __FILE__
|
130
|
+
require "daemons"
|
131
|
+
Daemons.run_proc(MyTempest.settings.process_name, :log_output => true) do
|
132
|
+
MyTempest.new.run
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Array
|
2
|
+
def separate(&block)
|
3
|
+
passed, failed = [], []
|
4
|
+
each do |item|
|
5
|
+
if block.call(item)
|
6
|
+
passed << item
|
7
|
+
else
|
8
|
+
failed << item
|
9
|
+
end
|
10
|
+
end
|
11
|
+
[passed, failed]
|
12
|
+
end unless method_defined?(:separate)
|
13
|
+
end
|
14
|
+
|
15
|
+
class Object
|
16
|
+
def metaclass
|
17
|
+
class << self; self; end
|
18
|
+
end unless method_defined?(:metaclass)
|
19
|
+
|
20
|
+
def tap
|
21
|
+
yield self
|
22
|
+
self
|
23
|
+
end unless method_defined?(:tap)
|
24
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
class Bookkeeper
|
3
|
+
attr_reader :storm
|
4
|
+
|
5
|
+
def initialize(storm)
|
6
|
+
@storm = storm
|
7
|
+
end
|
8
|
+
|
9
|
+
def book
|
10
|
+
return @book if @book
|
11
|
+
|
12
|
+
book = {}
|
13
|
+
|
14
|
+
executions = storm.clear_executions(:finished?)
|
15
|
+
ObjectSpace.garbage_collect
|
16
|
+
|
17
|
+
# Task success/error counts.
|
18
|
+
book[:tasks] = {}
|
19
|
+
book[:tasks][:total_count] = executions.length
|
20
|
+
book[:tasks][:error_count] = executions.inject(0){ |memo, e| memo += 1 if e.exception; memo }
|
21
|
+
book[:tasks][:error_percentage] = begin
|
22
|
+
if book[:tasks][:total_count] > 0
|
23
|
+
book[:tasks][:error_count].to_f / book[:tasks][:total_count] * 100.0
|
24
|
+
else
|
25
|
+
0.0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
book[:tasks][:per_thread] = tasks_per_thread(storm.threads, executions).values
|
29
|
+
book[:tasks][:avg_duration] = executions.inject(0){ |memo, e| memo += e.duration; memo }.to_f / executions.length
|
30
|
+
|
31
|
+
# Thread (worker) info.
|
32
|
+
book[:threads] = {}
|
33
|
+
book[:threads][:busy] = storm.busy_workers.length
|
34
|
+
book[:threads][:idle] = storm.size - book[:threads][:busy]
|
35
|
+
book[:threads][:saturation] = book[:threads][:busy] / storm.size.to_f * 100
|
36
|
+
|
37
|
+
# Memory, Object, GC info.
|
38
|
+
book[:memory] = {}
|
39
|
+
book[:memory][:live_objects] = ObjectSpace.live_objects rescue nil
|
40
|
+
book[:memory][:resident] = get_memory(:resident)
|
41
|
+
book[:memory][:virtual] = get_memory(:virtual)
|
42
|
+
|
43
|
+
# Open file counts.
|
44
|
+
book[:files] = {}
|
45
|
+
book[:files][:total_count] = get_files(:total)
|
46
|
+
book[:files][:tcp_count] = get_files(:tcp)
|
47
|
+
|
48
|
+
@book = book
|
49
|
+
end
|
50
|
+
|
51
|
+
# executions passed in are *finished*.
|
52
|
+
def tasks_per_thread(threads, executions)
|
53
|
+
counts_by_thread = threads.inject({}) do |memo, thread|
|
54
|
+
memo[thread] = 0
|
55
|
+
memo
|
56
|
+
end
|
57
|
+
executions.each do |e|
|
58
|
+
counts_by_thread[e.thread] += 1
|
59
|
+
end
|
60
|
+
counts_by_thread
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_memory(which)
|
64
|
+
@memory ||= `ps -o rss= -o vsz= -p #{Process.pid}`.split.collect{ |s| s.strip } rescue [nil, nil]
|
65
|
+
case which
|
66
|
+
when :resident
|
67
|
+
@memory[0].to_i
|
68
|
+
when :virtual
|
69
|
+
@memory[1].to_i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_files(which)
|
74
|
+
@files ||= begin
|
75
|
+
output = `lsof -p #{Process.pid}` rescue ""
|
76
|
+
output.split("\n")
|
77
|
+
end
|
78
|
+
case which
|
79
|
+
when :total
|
80
|
+
@files.length
|
81
|
+
when :tcp
|
82
|
+
@files.inject(0){ |memo, line| memo += 1 if line.downcase =~ /tcp/; memo }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
module Bootstrap
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.send(:extend, ClassMethods)
|
6
|
+
mod.send(:include, InstanceMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def bootstrap
|
17
|
+
init_logging
|
18
|
+
with_error_handling(:halt_on_error) do
|
19
|
+
init_thread_pool
|
20
|
+
before_initialize
|
21
|
+
init_tasks
|
22
|
+
init_queue
|
23
|
+
after_initialize
|
24
|
+
init_require
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def init_logging
|
29
|
+
@logger = Logger.new("#{settings.log_dir}/#{settings.process_name}.log")
|
30
|
+
@logger.formatter = LogFormatter
|
31
|
+
@logger.level = settings.log_level
|
32
|
+
logger.info "starting up"
|
33
|
+
|
34
|
+
@task_logger = Logger.new("#{settings.log_dir}/#{settings.process_name}.task.log")
|
35
|
+
@task_logger.formatter = LogFormatter
|
36
|
+
@task_logger.level = settings.log_level
|
37
|
+
end
|
38
|
+
|
39
|
+
def init_tasks
|
40
|
+
logger.info "initializing tasks"
|
41
|
+
Dir.glob("#{settings.task_dir}/*.rb").each do |file_path|
|
42
|
+
logger.info file_path
|
43
|
+
require file_path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def init_queue
|
48
|
+
logger.info "initializing queue"
|
49
|
+
@queue = settings.queue.call(logger)
|
50
|
+
end
|
51
|
+
|
52
|
+
def init_thread_pool
|
53
|
+
logger.info "initializing thread pool"
|
54
|
+
@storm = ThreadStorm.new :size => settings.threads,
|
55
|
+
:reraise => false,
|
56
|
+
:timeout_method => settings.timeout_method,
|
57
|
+
:timeout => settings.task_timeout
|
58
|
+
end
|
59
|
+
|
60
|
+
def before_initialize
|
61
|
+
logger.info "calling before_initialize"
|
62
|
+
settings.before_initialize.call(logger)
|
63
|
+
end
|
64
|
+
|
65
|
+
def after_initialize
|
66
|
+
logger.info "calling after_initialize"
|
67
|
+
settings.after_initialize.call(logger)
|
68
|
+
end
|
69
|
+
|
70
|
+
def init_require
|
71
|
+
require "task_tempest/require"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
module Callbacks
|
3
|
+
|
4
|
+
def on_bookkeeping(book)
|
5
|
+
settings.on_bookkeeping.call(book, logger) if settings.on_bookkeeping
|
6
|
+
end
|
7
|
+
|
8
|
+
def on_require(task, files)
|
9
|
+
return if files.empty?
|
10
|
+
logger.warn task.format_log "Kernel.require called on #{files.inspect}"
|
11
|
+
settings.on_require.call(task, files, logger)
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_internal_exception(e)
|
15
|
+
settings.on_internal_exception.call(e, logger)
|
16
|
+
rescue Exception => e
|
17
|
+
logger.error format_exception(e) rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_task_exception(task, e)
|
21
|
+
settings.on_task_exception.call(task, e, logger)
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_task_timeout(task)
|
25
|
+
settings.on_task_timeout.call(task, logger)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require "thread_storm"
|
2
|
+
|
3
|
+
require "task_tempest/bookkeeper"
|
4
|
+
require "task_tempest/bootstrap"
|
5
|
+
require "task_tempest/callbacks"
|
6
|
+
require "task_tempest/error_handling"
|
7
|
+
require "task_tempest/settings"
|
8
|
+
|
9
|
+
module TaskTempest
|
10
|
+
class Engine
|
11
|
+
attr_reader :logger, :task_logger, :queue, :storm, :message, :tasks
|
12
|
+
|
13
|
+
include Bootstrap
|
14
|
+
include Callbacks
|
15
|
+
include ErrorHandling
|
16
|
+
include Settings
|
17
|
+
|
18
|
+
def self.inherited(derived)
|
19
|
+
derived.settings = settings.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.submit_message(message, *args)
|
23
|
+
logger = Logger.new(STDOUT)
|
24
|
+
queue = settings.queue.call(logger)
|
25
|
+
settings.enqueue.call(queue, message, logger, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.submit_task(task, *args)
|
29
|
+
submit_message(task.to_message, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.submit(task_or_message, *args)
|
33
|
+
if task_or_message.kind_of?(TaskTempest::Task)
|
34
|
+
submit_task(task_or_message, *args)
|
35
|
+
else
|
36
|
+
submit_message(task_or_message, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@tasks = []
|
42
|
+
@bookkeeping_timer = Time.now
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
bootstrap
|
47
|
+
logger.info "starting run loop"
|
48
|
+
with_shutdown_handling{ heartbeat while true }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def heartbeat
|
54
|
+
with_error_handling{ receive_message }
|
55
|
+
with_error_handling{ dispatch_message }
|
56
|
+
with_error_handling{ finish_tasks }
|
57
|
+
with_error_handling{ bookkeeping }
|
58
|
+
end
|
59
|
+
|
60
|
+
def receive_message
|
61
|
+
logger.debug "receiving message"
|
62
|
+
|
63
|
+
if message
|
64
|
+
logger.debug "already have message"
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
# Why do we do it this way? Because of badly behaved dequeue
|
69
|
+
# definitions. For example, right_aws rescues any exception
|
70
|
+
# when making a request to Amazon. Thus if we try to shutdown
|
71
|
+
# our tempest, right_aws could potentially swallow that exception.
|
72
|
+
|
73
|
+
@receive_storm ||= ThreadStorm.new :size => 1,
|
74
|
+
:timeout_method => settings.timeout_method,
|
75
|
+
:timeout => settings.dequeue_timeout
|
76
|
+
|
77
|
+
execution = @receive_storm.execute{ settings.dequeue.call(queue, logger) }
|
78
|
+
with_error_handling do
|
79
|
+
@message = execution.value
|
80
|
+
logger.warn "dequeue timed out" if execution.timed_out?
|
81
|
+
end
|
82
|
+
@receive_storm.clear_executions # Prevent memory leak.
|
83
|
+
|
84
|
+
if message.nil?
|
85
|
+
logger.debug "no available messages, sleeping for #{settings.no_message_sleep}"
|
86
|
+
sleep(settings.no_message_sleep)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def dispatch_message
|
91
|
+
if storm.busy_workers.length == storm.size
|
92
|
+
logger.debug "no available threads, sleeping for #{settings.no_thread_sleep}"
|
93
|
+
sleep(settings.no_thread_sleep)
|
94
|
+
elsif message
|
95
|
+
dispatch_task
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def dispatch_task
|
100
|
+
id, name, *args = message
|
101
|
+
task = TaskTempest::Task.const_get(name).new(*args)
|
102
|
+
task.override :id => id, :logger => task_logger
|
103
|
+
task.spawn(storm)
|
104
|
+
tasks << task
|
105
|
+
logger.info task.format_log("started", true)
|
106
|
+
task.logger.info "arguments #{args.inspect}"
|
107
|
+
rescue Exception => e
|
108
|
+
raise
|
109
|
+
ensure
|
110
|
+
@message = nil # Ensure we pop a new message off the queue on next loop iteration.
|
111
|
+
end
|
112
|
+
|
113
|
+
def finish_tasks
|
114
|
+
finished, @tasks = tasks.separate{ |task| task.execution.finished? }
|
115
|
+
finished.each{ |task| handle_finished_task(task) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_finished_task(task)
|
119
|
+
if (e = task.execution.exception)
|
120
|
+
logger.info task.format_log("failed", true)
|
121
|
+
task.logger.fatal format_exception(e)
|
122
|
+
on_task_exception(task, e)
|
123
|
+
elsif task.execution.timed_out?
|
124
|
+
logger.info task.format_log("timed out", true)
|
125
|
+
on_task_timeout(task)
|
126
|
+
else
|
127
|
+
logger.info task.format_log("finished", true)
|
128
|
+
on_require(task, task.execution.value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def bookkeeping
|
133
|
+
# Return unless it's time to do bookkeeping.
|
134
|
+
if Time.now - @bookkeeping_timer > settings.bookkeeping_interval
|
135
|
+
@bookkeeping_timer = Time.now # Reset the timer.
|
136
|
+
else
|
137
|
+
return
|
138
|
+
end
|
139
|
+
|
140
|
+
keeper = Bookkeeper.new(storm)
|
141
|
+
logger.info "[BOOKKEEPING] " + keeper.book.inspect
|
142
|
+
on_bookkeeping(keeper.book)
|
143
|
+
end
|
144
|
+
|
145
|
+
def clean_shutdown
|
146
|
+
logger.info "shutting down"
|
147
|
+
begin
|
148
|
+
timeout(settings.shutdown_timeout) do
|
149
|
+
storm.join
|
150
|
+
storm.shutdown
|
151
|
+
end
|
152
|
+
rescue Timeout::Error => e
|
153
|
+
logger.warn "shutdown timeout exceeded"
|
154
|
+
end
|
155
|
+
finish_tasks
|
156
|
+
exit(0)
|
157
|
+
end
|
158
|
+
|
159
|
+
def dirty_shutdown
|
160
|
+
exit(-1)
|
161
|
+
end
|
162
|
+
|
163
|
+
def timeout(timeout, &block)
|
164
|
+
settings.timeout_method.call(timeout, &block)
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
module ErrorHandling
|
3
|
+
|
4
|
+
SHUTDOWN_EXCEPTIONS = [
|
5
|
+
Interrupt,
|
6
|
+
SystemExit,
|
7
|
+
SignalException
|
8
|
+
]
|
9
|
+
|
10
|
+
def with_error_handling(halt_on_error = false)
|
11
|
+
yield
|
12
|
+
rescue *SHUTDOWN_EXCEPTIONS => e
|
13
|
+
raise
|
14
|
+
rescue Exception => e
|
15
|
+
on_internal_exception(e)
|
16
|
+
if halt_on_error
|
17
|
+
logger.fatal format_exception(e)
|
18
|
+
exit(-1)
|
19
|
+
else
|
20
|
+
logger.error format_exception(e)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def with_shutdown_handling
|
25
|
+
yield
|
26
|
+
rescue *SHUTDOWN_EXCEPTIONS => e
|
27
|
+
if e.class == SignalException
|
28
|
+
handle_shutdown_signal(e) or raise
|
29
|
+
else
|
30
|
+
clean_shutdown
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_shutdown_signal(e)
|
35
|
+
case e.message
|
36
|
+
when "SIGTERM"
|
37
|
+
logger.info "SIGTERM detected"
|
38
|
+
dirty_shutdown
|
39
|
+
when "SIGUSR2"
|
40
|
+
logger.info "SIGUSR2 detected"
|
41
|
+
clean_shutdown
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def format_exception(e)
|
48
|
+
"#{e.class} #{e.message}\n" + e.backtrace.join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
alias_method :original_require, :require
|
5
|
+
|
6
|
+
def require(file)
|
7
|
+
without_ext = file.sub /(\.rb$)|(\.bundle$)/, ""
|
8
|
+
files = %w[.rb .bundle].collect{ |ext| without_ext + ext }
|
9
|
+
already_required = !($".to_set & files.to_set).empty?
|
10
|
+
required_files = Thread.current[:required_files]
|
11
|
+
required_files << file if required_files and not already_required
|
12
|
+
original_require(file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.record_requires!
|
16
|
+
if Thread.current[:required_files] == nil
|
17
|
+
Thread.current[:required_files] = []
|
18
|
+
yield
|
19
|
+
required_files = Thread.current[:required_files]
|
20
|
+
Thread.current[:required_files] = nil
|
21
|
+
required_files
|
22
|
+
else # Reentrant case.
|
23
|
+
yield
|
24
|
+
Thread.current[:required_files]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "timeout"
|
3
|
+
|
4
|
+
module TaskTempest
|
5
|
+
module Settings
|
6
|
+
|
7
|
+
DEFAULTS = {
|
8
|
+
:process_name => "task_tempest",
|
9
|
+
:log_level => Logger::DEBUG,
|
10
|
+
:threads => 10,
|
11
|
+
:no_thread_sleep => 1,
|
12
|
+
:no_message_sleep => 1,
|
13
|
+
:task_timeout => nil,
|
14
|
+
:shutdown_timeout => 5, # 5 seconds
|
15
|
+
:dequeue_timeout => 2, # 2 seconds
|
16
|
+
:timeout_method => Timeout.method(:timeout),
|
17
|
+
:root_dir => File.expand_path(Dir.pwd),
|
18
|
+
:log_dir => File.expand_path(Dir.pwd),
|
19
|
+
:task_dir => File.expand_path(Dir.pwd),
|
20
|
+
:queue => nil,
|
21
|
+
:enqueue => Proc.new{ |queue, message| raise "not implemented" },
|
22
|
+
:dequeue => Proc.new{ |queue, logger| logger.error("dequeue not defined"); sleep(1); nil },
|
23
|
+
:bookkeeping_interval => 10*60, # 10 minutes
|
24
|
+
:before_initialize => Proc.new{ |logger| },
|
25
|
+
:after_initialize => Proc.new{ |logger| },
|
26
|
+
:on_internal_exception => Proc.new{ |e, logger| },
|
27
|
+
:on_task_exception => Proc.new{ |e, logger| },
|
28
|
+
:on_require => Proc.new{ |files, logger| },
|
29
|
+
:on_bookkeeping => Proc.new{ |book, logger| },
|
30
|
+
:on_task_timeout => Proc.new{ |task, logger| }
|
31
|
+
}
|
32
|
+
|
33
|
+
def self.included(mod)
|
34
|
+
mod.metaclass.class_eval{ attr_accessor :settings }
|
35
|
+
mod.settings = Struct.new(*DEFAULTS.keys).new(*DEFAULTS.values)
|
36
|
+
mod.send(:include, InstanceMethods)
|
37
|
+
mod.send(:extend, ClassMethods)
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
|
42
|
+
def settings
|
43
|
+
self.class.settings
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
|
50
|
+
def process_name(value)
|
51
|
+
settings.process_name = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_level(value)
|
55
|
+
settings.log_level = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def threads(value)
|
59
|
+
settings.threads = value
|
60
|
+
end
|
61
|
+
|
62
|
+
def no_message_sleep(value)
|
63
|
+
settings.no_message_sleep = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def no_thread_sleep(value)
|
67
|
+
settings.no_thread_sleep = value
|
68
|
+
end
|
69
|
+
|
70
|
+
def root_dir(path)
|
71
|
+
settings.root_dir = File.expand_path(path)
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_dir(value)
|
75
|
+
settings.log_dir = File.expand_path(value)
|
76
|
+
end
|
77
|
+
|
78
|
+
def task_dir(value)
|
79
|
+
settings.task_dir = File.expand_path(value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def timeout_method(value)
|
83
|
+
settings.timeout_method = value
|
84
|
+
end
|
85
|
+
|
86
|
+
def dequeue_timeout(seconds)
|
87
|
+
settings.dequeue_timeout = value.to_f
|
88
|
+
end
|
89
|
+
|
90
|
+
def task_timeout(value)
|
91
|
+
settings.task_timeout = value.to_f
|
92
|
+
end
|
93
|
+
|
94
|
+
def shutdown_timeout(value)
|
95
|
+
settings.shutdown_timeout = value.to_f
|
96
|
+
end
|
97
|
+
|
98
|
+
def queue(&block)
|
99
|
+
settings.queue = block
|
100
|
+
end
|
101
|
+
|
102
|
+
def enqueue(&block)
|
103
|
+
settings.enqueue = block
|
104
|
+
end
|
105
|
+
|
106
|
+
def dequeue(&block)
|
107
|
+
settings.dequeue = block
|
108
|
+
end
|
109
|
+
|
110
|
+
def bookkeeping_interval(value)
|
111
|
+
settings.bookkeeping_interval = value
|
112
|
+
end
|
113
|
+
|
114
|
+
def before_initialize(&block)
|
115
|
+
settings.before_initialize = block
|
116
|
+
end
|
117
|
+
|
118
|
+
def after_initialize(&block)
|
119
|
+
settings.after_initialize = block
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_internal_exception(&block)
|
123
|
+
settings.on_internal_exception = block
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_task_exception(&block)
|
127
|
+
settings.on_task_exception = block
|
128
|
+
end
|
129
|
+
|
130
|
+
def on_task_timeout(&block)
|
131
|
+
settings.on_task_timeout = block
|
132
|
+
end
|
133
|
+
|
134
|
+
def on_require(&block)
|
135
|
+
settings.on_require = block
|
136
|
+
end
|
137
|
+
|
138
|
+
def on_bookkeeping(&block)
|
139
|
+
settings.on_bookkeeping = block
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
require "task_tempest/task_logger"
|
5
|
+
require "task_tempest/require"
|
6
|
+
|
7
|
+
module TaskTempest
|
8
|
+
class Task
|
9
|
+
attr_reader :id, :args, :execution
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@id = generate_id
|
13
|
+
@args = args
|
14
|
+
end
|
15
|
+
|
16
|
+
def override(options = {})
|
17
|
+
@id = options[:id] if options[:id]
|
18
|
+
@logger = TaskLogger.new(options[:logger], self) if options[:logger]
|
19
|
+
end
|
20
|
+
|
21
|
+
def spawn(storm)
|
22
|
+
@execution = storm.execute{ run }
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
Kernel.record_requires!{ start(*args) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def start(*args)
|
30
|
+
raise "not implemented"
|
31
|
+
end
|
32
|
+
|
33
|
+
def logger
|
34
|
+
@logger ||= TaskLogger.new(Logger.new(STDOUT), self)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_message
|
38
|
+
[id, self.class.name, *args]
|
39
|
+
end
|
40
|
+
|
41
|
+
def format_log(message, duration = false)
|
42
|
+
s = "{#{id}} <#{self.class}> #{message}"
|
43
|
+
s += " #{execution.duration}" if duration and execution.finished?
|
44
|
+
s
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def generate_id
|
50
|
+
Digest::SHA1.hexdigest(Time.now.to_s + rand.to_s)[0,5]
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TaskTempest
|
2
|
+
class TaskLogger
|
3
|
+
|
4
|
+
def initialize(logger, task)
|
5
|
+
@logger = logger
|
6
|
+
@task = task
|
7
|
+
end
|
8
|
+
|
9
|
+
%w[debug info warn error fatal].each do |level|
|
10
|
+
class_eval <<-STR
|
11
|
+
def #{level}(msg)
|
12
|
+
@logger.#{level} @task.format_log(msg)
|
13
|
+
end
|
14
|
+
STR
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/task_tempest.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "task_tempest/active_support"
|
2
|
+
require "task_tempest/engine"
|
3
|
+
require "task_tempest/task"
|
4
|
+
|
5
|
+
module TaskTempest
|
6
|
+
|
7
|
+
LogFormatter = Proc.new do |severity, time, progname, message|
|
8
|
+
message = message.call if message.respond_to?(:call)
|
9
|
+
time = time.strftime("%Y/%m/%d %H:%M:%S")
|
10
|
+
sprintf("%s [%s] %s\n", time, severity, message)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,68 @@
|
|
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{task_tempest}
|
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 = ["Christopher J. Bottaro"]
|
12
|
+
s.date = %q{2010-06-24}
|
13
|
+
s.description = %q{Framework for creating queue based, threaded asychronous job processors.}
|
14
|
+
s.email = %q{cjbottaro@alumni.cs.utexas.edu}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"CHANGELOG",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"examples/my_tempest.rb",
|
28
|
+
"examples/tasks/evaler.rb",
|
29
|
+
"examples/tasks/greeter.rb",
|
30
|
+
"lib/task_tempest.rb",
|
31
|
+
"lib/task_tempest/active_support.rb",
|
32
|
+
"lib/task_tempest/bookkeeper.rb",
|
33
|
+
"lib/task_tempest/bootstrap.rb",
|
34
|
+
"lib/task_tempest/callbacks.rb",
|
35
|
+
"lib/task_tempest/engine.rb",
|
36
|
+
"lib/task_tempest/error_handling.rb",
|
37
|
+
"lib/task_tempest/require.rb",
|
38
|
+
"lib/task_tempest/settings.rb",
|
39
|
+
"lib/task_tempest/task.rb",
|
40
|
+
"lib/task_tempest/task_logger.rb",
|
41
|
+
"task_tempest.gemspec",
|
42
|
+
"test/helper.rb",
|
43
|
+
"test/test_task_tempest.rb"
|
44
|
+
]
|
45
|
+
s.homepage = %q{http://github.com/cjbottaro/task_tempest}
|
46
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
47
|
+
s.require_paths = ["lib"]
|
48
|
+
s.rubygems_version = %q{1.3.7}
|
49
|
+
s.summary = %q{Framework for creating asychronous job processors.}
|
50
|
+
s.test_files = [
|
51
|
+
"test/helper.rb",
|
52
|
+
"test/test_task_tempest.rb",
|
53
|
+
"examples/my_tempest.rb",
|
54
|
+
"examples/tasks/evaler.rb",
|
55
|
+
"examples/tasks/greeter.rb"
|
56
|
+
]
|
57
|
+
|
58
|
+
if s.respond_to? :specification_version then
|
59
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
60
|
+
s.specification_version = 3
|
61
|
+
|
62
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
63
|
+
else
|
64
|
+
end
|
65
|
+
else
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: task_tempest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Christopher J. Bottaro
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-06-24 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Framework for creating queue based, threaded asychronous job processors.
|
23
|
+
email: cjbottaro@alumni.cs.utexas.edu
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- LICENSE
|
30
|
+
- README.rdoc
|
31
|
+
files:
|
32
|
+
- .document
|
33
|
+
- .gitignore
|
34
|
+
- CHANGELOG
|
35
|
+
- LICENSE
|
36
|
+
- README.rdoc
|
37
|
+
- Rakefile
|
38
|
+
- VERSION
|
39
|
+
- examples/my_tempest.rb
|
40
|
+
- examples/tasks/evaler.rb
|
41
|
+
- examples/tasks/greeter.rb
|
42
|
+
- lib/task_tempest.rb
|
43
|
+
- lib/task_tempest/active_support.rb
|
44
|
+
- lib/task_tempest/bookkeeper.rb
|
45
|
+
- lib/task_tempest/bootstrap.rb
|
46
|
+
- lib/task_tempest/callbacks.rb
|
47
|
+
- lib/task_tempest/engine.rb
|
48
|
+
- lib/task_tempest/error_handling.rb
|
49
|
+
- lib/task_tempest/require.rb
|
50
|
+
- lib/task_tempest/settings.rb
|
51
|
+
- lib/task_tempest/task.rb
|
52
|
+
- lib/task_tempest/task_logger.rb
|
53
|
+
- task_tempest.gemspec
|
54
|
+
- test/helper.rb
|
55
|
+
- test/test_task_tempest.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/cjbottaro/task_tempest
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
hash: 3
|
80
|
+
segments:
|
81
|
+
- 0
|
82
|
+
version: "0"
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.3.7
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: Framework for creating asychronous job processors.
|
90
|
+
test_files:
|
91
|
+
- test/helper.rb
|
92
|
+
- test/test_task_tempest.rb
|
93
|
+
- examples/my_tempest.rb
|
94
|
+
- examples/tasks/evaler.rb
|
95
|
+
- examples/tasks/greeter.rb
|