task_tempest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 0.1.0
2
+ - Initial version
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,7 @@
1
+ class Evaler < TaskTempest::Task
2
+
3
+ def start(code)
4
+ eval(code)
5
+ end
6
+
7
+ end
@@ -0,0 +1,8 @@
1
+ class Greeter < TaskTempest::Task
2
+
3
+ def start(person, greeting)
4
+ logger.info "#{person} has been greeted"
5
+ puts "#{greeting}, #{person}!"
6
+ end
7
+
8
+ 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
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'task_tempest'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,5 @@
1
+ require 'helper'
2
+
3
+ class TestTaskTempest < Test::Unit::TestCase
4
+
5
+ end
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