task_tempest 0.1.0 → 0.2.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/.gitignore CHANGED
@@ -19,3 +19,4 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+ example/log
data/CHANGELOG CHANGED
@@ -1,2 +1,4 @@
1
+ 0.2.0
2
+ - Refactored
1
3
  0.1.0
2
4
  - Initial version
data/Rakefile CHANGED
@@ -51,3 +51,22 @@ Rake::RDocTask.new do |rdoc|
51
51
  rdoc.rdoc_files.include('README*')
52
52
  rdoc.rdoc_files.include('lib/**/*.rb')
53
53
  end
54
+
55
+ namespace :example do
56
+
57
+ desc "Run the example."
58
+ task :run do
59
+ `ruby example/my_tempest.rb run`
60
+ end
61
+
62
+ desc "Fill the example queue."
63
+ task :fill do
64
+ require "example/my_tempest"
65
+ while true
66
+ r = rand
67
+ sleep(r)
68
+ MyTempest.submit([nil, "Evaler", %{sleep(#{r})}])
69
+ end
70
+ end
71
+
72
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -1,3 +1,4 @@
1
+ Dir.chdir(File.dirname(__FILE__))
1
2
  $LOAD_PATH << "../lib"
2
3
 
3
4
  require "rubygems"
@@ -9,25 +10,44 @@ require "tasks/evaler"
9
10
  require "tasks/greeter"
10
11
 
11
12
  class MemcachedQueue
13
+ attr_reader :logger
12
14
 
13
- def initialize(name)
15
+ def initialize(name, logger = nil)
14
16
  @name = name
17
+ @logger = logger || Logger.new(File.open("/dev/null", "w"))
15
18
  @cache = MemCache.new "localhost:11211"
16
19
  end
17
20
 
18
21
  def push(item)
22
+ logger.debug "MemcachedQueue#push #{item.inspect}"
19
23
  queue = @cache.fetch(@name){ [] }
20
24
  queue.push(item)
21
25
  @cache.set(@name, queue)
22
26
  end
23
27
 
28
+ alias_method :enqueue, :push
29
+
24
30
  def pop
31
+ # @count ||= 0
32
+ # @count += 1
33
+ # if @count % 10 == 0
34
+ # @count = 0
35
+ # raise "pop failed"
36
+ # end
37
+
25
38
  queue = @cache.fetch(@name){ [] }
26
39
  item = queue.pop
40
+ logger.debug "MemcachedQueue#pop #{item.inspect}" if item
27
41
  @cache.set(@name, queue)
28
42
  item
29
43
  end
30
44
 
45
+ def size
46
+ @cache.fetch(@name){ [] }.size
47
+ end
48
+
49
+ alias_method :dequeue, :pop
50
+
31
51
  end
32
52
 
33
53
  # To run this example, open two shells and navagate to the examples directory (i.e. the
@@ -41,20 +61,20 @@ end
41
61
  # Note this example requires the SystemTimer, daemons and memcache-client gems.
42
62
  class MyTempest < TaskTempest::Engine
43
63
 
44
- # This dictates what the logs will be named.
45
- process_name "my_tempest"
46
-
47
64
  # How many threads.
48
- threads 5
65
+ threads 3
49
66
 
50
67
  # Where to write the log files.
51
68
  log_dir "log"
52
69
 
70
+ # What to name the log files.
71
+ log_name "my_tempest"
72
+
53
73
  # Where to look for task classes. Will require each .rb file in this directory.
54
74
  task_dir "tasks"
55
75
 
56
76
  # Time in seconds between each bookkeeping event.
57
- bookkeeping_interval 15
77
+ bookkeeping_interval 10
58
78
 
59
79
  # Don't display log messages below this level.
60
80
  log_level Logger::INFO
@@ -70,25 +90,6 @@ class MyTempest < TaskTempest::Engine
70
90
  MemcachedQueue.new("my_tempest_queue")
71
91
  end
72
92
 
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
93
  # Callback that happens after #init_logging, but before #bootstrap.
93
94
  before_initialize do |logger|
94
95
  end
@@ -104,7 +105,7 @@ class MyTempest < TaskTempest::Engine
104
105
 
105
106
  # Callback that happens when an exception occurs in a task.
106
107
  on_task_exception do |task, e, logger|
107
- puts "(T:#{task_id}) #{e.class}: #{e.message}"
108
+ puts "(T:#{task.id}) #{e.class}: #{e.message}"
108
109
  end
109
110
 
110
111
  # Callback that happens when a task exceeds the task_timeout setting.
@@ -128,7 +129,7 @@ end
128
129
 
129
130
  if $0 == __FILE__
130
131
  require "daemons"
131
- Daemons.run_proc(MyTempest.settings.process_name, :log_output => true) do
132
+ Daemons.run_proc(MyTempest.name, :log_output => true) do
132
133
  MyTempest.new.run
133
134
  end
134
135
  end
File without changes
File without changes
@@ -10,6 +10,30 @@ class Array
10
10
  end
11
11
  [passed, failed]
12
12
  end unless method_defined?(:separate)
13
+
14
+ def avg(&block)
15
+ sum(&block).to_f / length
16
+ end
17
+ end
18
+
19
+ module Enumerable
20
+ def sum(identity = 0, &block)
21
+ return identity unless size > 0
22
+
23
+ if block_given?
24
+ map(&block).sum
25
+ else
26
+ inject { |sum, element| sum + element }
27
+ end
28
+ end unless method_defined?(:sum)
29
+ end
30
+
31
+ class Float
32
+ def round_with_task_tempest(precision = nil)
33
+ precision.nil? ? round_without_task_tempest : (self * (10 ** precision)).round / (10 ** precision).to_f
34
+ end
35
+ alias_method :round_without_task_tempest, :round
36
+ alias_method :round, :round_with_task_tempest
13
37
  end
14
38
 
15
39
  class Object
@@ -1,63 +1,103 @@
1
1
  module TaskTempest
2
2
  class Bookkeeper
3
- attr_reader :storm
3
+ attr_reader :logger
4
4
 
5
- def initialize(storm)
6
- @storm = storm
5
+ def initialize(options)
6
+ options.each{ |k, v| instance_variable_set("@#{k}", v) }
7
7
  end
8
8
 
9
- def book
10
- return @book if @book
9
+ def report(executions)
10
+ @timer ||= Time.now
11
+ @executions ||= []
12
+ @executions += executions
11
13
 
12
- book = {}
14
+ if Time.now - @timer > @interval
15
+ logger.info "[STATS] " + make_book.inspect
16
+ @executions.clear
17
+ @timer = Time.now
18
+ end
19
+ end
20
+
21
+ def make_book
13
22
 
14
- executions = storm.clear_executions(:finished?)
23
+ # Do some setup.
15
24
  ObjectSpace.garbage_collect
25
+ queue = @queue_factory.call
26
+ book = {}
27
+
28
+ # Reset memoized objects.
29
+ @memory = nil
30
+ @files = nil
16
31
 
17
32
  # Task success/error counts.
18
33
  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
34
+ book[:tasks][:counts] = task_counts
35
+ book[:tasks][:per_thread] = tasks_per_thread
36
+ book[:tasks][:durations] = task_durations
37
+ book[:tasks][:throughput] = task_throughput
30
38
 
31
39
  # Thread (worker) info.
32
40
  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
41
+ book[:threads][:busy] = @storm.busy_workers.length
42
+ book[:threads][:idle] = @storm.size - book[:threads][:busy]
43
+ book[:threads][:saturation] = (book[:threads][:busy] / @storm.size.to_f * 100).round(2)
36
44
 
37
45
  # Memory, Object, GC info.
38
46
  book[:memory] = {}
39
47
  book[:memory][:live_objects] = ObjectSpace.live_objects rescue nil
40
- book[:memory][:resident] = get_memory(:resident)
41
- book[:memory][:virtual] = get_memory(:virtual)
48
+ book[:memory][:resident] = format_memory(get_memory(:resident))
49
+ book[:memory][:virtual] = format_memory(get_memory(:virtual))
42
50
 
43
51
  # Open file counts.
44
52
  book[:files] = {}
45
53
  book[:files][:total_count] = get_files(:total)
46
54
  book[:files][:tcp_count] = get_files(:tcp)
47
55
 
48
- @book = book
56
+ # Queue info.
57
+ book[:queue] = {}
58
+ book[:queue][:size] = queue.size if queue.respond_to?(:size)
59
+ book[:queue][:backlog] = @storm.executions.inject(0){ |memo, e| memo += 1 unless e.started?; memo }
60
+
61
+ book
62
+ end
63
+
64
+ def task_counts
65
+ tot = @executions.length
66
+ err = @executions.sum{ |e| e.exception ? 1 : 0 }
67
+ pct = begin
68
+ if tot > 0
69
+ (err.to_f / tot)
70
+ else
71
+ 0.0
72
+ end
73
+ end
74
+ { :tot => tot, :err => err, :pct => pct.round(3) }
75
+ end
76
+
77
+ def task_throughput
78
+ duration = Time.now - @timer
79
+ per_sec = @executions.length.to_f / duration
80
+ per_min = (per_sec * 60).round(2)
81
+ "#{per_min}/m"
49
82
  end
50
83
 
51
- # executions passed in are *finished*.
52
- def tasks_per_thread(threads, executions)
53
- counts_by_thread = threads.inject({}) do |memo, thread|
84
+ def tasks_per_thread
85
+ counts_by_thread = @storm.threads.inject({}) do |memo, thread|
54
86
  memo[thread] = 0
55
87
  memo
56
88
  end
57
- executions.each do |e|
58
- counts_by_thread[e.thread] += 1
89
+ @executions.each{ |e| counts_by_thread[e.thread] += 1 }
90
+ counts = counts_by_thread.values
91
+ { :min => counts.min, :max => counts.max, :avg => counts.avg.round(2) }
92
+ end
93
+
94
+ def task_durations
95
+ durations = @executions.collect{ |execution| execution.duration }
96
+ if durations.length > 0
97
+ { :min => durations.min.round(3), :max => durations.max.round(3), :avg => durations.avg.round(3) }
98
+ else
99
+ "n/a"
59
100
  end
60
- counts_by_thread
61
101
  end
62
102
 
63
103
  def get_memory(which)
@@ -83,5 +123,15 @@ module TaskTempest
83
123
  end
84
124
  end
85
125
 
126
+ KB = 1024
127
+ MB = KB**2
128
+ def format_memory(memory)
129
+ if memory > MB
130
+ (memory / MB).to_s + "M"
131
+ else
132
+ (memory / KB).to_s + "K"
133
+ end
134
+ end
135
+
86
136
  end
87
137
  end
@@ -1,76 +1,152 @@
1
+ require "thread_storm"
2
+ require "task_tempest/bookkeeper"
3
+ require "task_tempest/dispatcher"
4
+
1
5
  module TaskTempest
2
6
  module Bootstrap
3
7
 
4
- def self.included(mod)
5
- mod.send(:extend, ClassMethods)
6
- mod.send(:include, InstanceMethods)
8
+ def logger
9
+ @logger ||= begin
10
+ log_name = settings.log_name || self.class.name
11
+ path = "#{settings.log_dir}/#{log_name}.log"
12
+ Logger.new(path).tap do |logger|
13
+ logger.formatter = LogFormatter
14
+ logger.level = settings.log_level
15
+ end
16
+ end
7
17
  end
8
18
 
9
- module ClassMethods
19
+ def task_logger
20
+ @task_logger ||= begin
21
+ log_name = settings.log_name || self.class.name
22
+ path = "#{settings.log_dir}/#{log_name}.task.log"
23
+ Logger.new(path).tap do |logger|
24
+ logger.formatter = LogFormatter
25
+ logger.level = settings.log_level
26
+ end
27
+ end
10
28
  end
11
29
 
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
30
+ def queue
31
+ @queue ||= begin
32
+ case settings.queue
33
+ when Proc
34
+ settings.queue.call(logger)
35
+ else
36
+ settings.queue
25
37
  end
26
38
  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
39
+ end
40
+
41
+ def storm
42
+ @storm ||= begin
43
+ ThreadStorm.new :size => settings.threads,
44
+ :reraise => false,
45
+ :execute_blocks => true,
46
+ :timeout_method => settings.timeout_method,
47
+ :timeout => settings.task_timeout
37
48
  end
38
-
39
- def init_tasks
49
+ end
50
+
51
+ def dispatcher
52
+ @dispatcher ||= begin
53
+ Dispatcher.new :logger => logger,
54
+ :task_logger => task_logger,
55
+ :queue_factory => Proc.new{ settings.queue.call(logger) },
56
+ :storm => storm,
57
+ :no_message_sleep => settings.no_message_sleep
58
+ end
59
+ end
60
+
61
+ def bookkeeper
62
+ @bookkeeper ||= begin
63
+ Bookkeeper.new :storm => storm,
64
+ :queue_factory => Proc.new{ settings.queue.call(logger) },
65
+ :interval => settings.bookkeeping_interval,
66
+ :logger => logger
67
+ end
68
+ end
69
+
70
+ def bootstrap(error_action)
71
+ init_logging
72
+ with_error_handling(error_action) do
73
+ before_initialize
74
+ init_require
75
+ init_tasks
76
+ init_thread_pool
77
+ init_queue
78
+ init_bookkeeper
79
+ init_task_logging
80
+ init_dispatcher
81
+ after_initialize
82
+ end
83
+ end
84
+
85
+ def init_logging
86
+ @logger and return
87
+ logger
88
+ logger.info "logger initialized"
89
+ end
90
+
91
+ def init_task_logging
92
+ @task_logger and return
93
+ task_logger
94
+ logger.info "task logger initialized"
95
+ end
96
+
97
+ def init_thread_pool
98
+ @storm and return
99
+ logger.info "initializing thread pool"
100
+ storm
101
+ end
102
+
103
+ def init_tasks
104
+ @init_tasks ||= begin
40
105
  logger.info "initializing tasks"
41
106
  Dir.glob("#{settings.task_dir}/*.rb").each do |file_path|
42
- logger.info file_path
107
+ logger.debug file_path
43
108
  require file_path
44
109
  end
110
+ true
45
111
  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"
112
+ end
113
+
114
+ def init_queue
115
+ @queue and return
116
+ logger.info "initializing queue"
117
+ queue
118
+ end
119
+
120
+ def init_require
121
+ require "task_tempest/require"
122
+ end
123
+
124
+ def init_bookkeeper
125
+ @bookkeeper and return
126
+ logger.info "initializing bookkeeper"
127
+ bookkeeper
128
+ end
129
+
130
+ def init_dispatcher
131
+ @dispatcher and return
132
+ logger.info "initializing dispatcher"
133
+ dispatcher
134
+ end
135
+
136
+ def before_initialize
137
+ @before_initialize ||= begin
138
+ logger.info "before_initialize called"
62
139
  settings.before_initialize.call(logger)
140
+ true
63
141
  end
64
-
65
- def after_initialize
66
- logger.info "calling after_initialize"
142
+ end
143
+
144
+ def after_initialize
145
+ @after_initialize ||= begin
67
146
  settings.after_initialize.call(logger)
147
+ logger.info "after_initialize called"
148
+ true
68
149
  end
69
-
70
- def init_require
71
- require "task_tempest/require"
72
- end
73
-
74
150
  end
75
151
 
76
152
  end
@@ -0,0 +1,64 @@
1
+ module TaskTempest
2
+ class Dispatcher
3
+ attr_reader :logger
4
+
5
+ def initialize(options)
6
+ options.each{ |k, v| instance_variable_set("@#{k}", v) }
7
+ start
8
+ end
9
+
10
+ def start
11
+ if dead?
12
+ @queue = @queue_factory.call
13
+ @thread = Thread.new{ run }
14
+ end
15
+ end
16
+
17
+ alias_method :restart, :start
18
+
19
+ def alive?
20
+ @thread and @thread.alive?
21
+ end
22
+
23
+ def dead?
24
+ not alive?
25
+ end
26
+
27
+ def exception
28
+ dead? and @thread.value
29
+ end
30
+
31
+ def run
32
+ run_loop while true
33
+ end
34
+
35
+ def run_loop
36
+ consume and dispatch
37
+ end
38
+
39
+ def consume
40
+ @message = @queue.dequeue
41
+ if @message
42
+ true
43
+ else
44
+ logger.debug "queue empty, sleeping for #{@no_message_sleep}"
45
+ sleep(@no_message_sleep)
46
+ false
47
+ end
48
+ end
49
+
50
+ def dispatch
51
+ task_id, task_class_name, *task_args = @message
52
+ task_class = TaskTempest::Task.const_get(task_class_name)
53
+ task = task_class.new(*task_args).init(:id => task_id, :logger => @task_logger)
54
+ task.execution = @storm.execute(task){ task.run }
55
+ logger.info task.format_log("started")
56
+ end
57
+
58
+ def shutdown
59
+ @thread and @thread.kill
60
+ @thread.join
61
+ end
62
+
63
+ end
64
+ end
@@ -1,6 +1,6 @@
1
+ require "benchmark"
1
2
  require "thread_storm"
2
3
 
3
- require "task_tempest/bookkeeper"
4
4
  require "task_tempest/bootstrap"
5
5
  require "task_tempest/callbacks"
6
6
  require "task_tempest/error_handling"
@@ -8,7 +8,6 @@ require "task_tempest/settings"
8
8
 
9
9
  module TaskTempest
10
10
  class Engine
11
- attr_reader :logger, :task_logger, :queue, :storm, :message, :tasks
12
11
 
13
12
  include Bootstrap
14
13
  include Callbacks
@@ -20,9 +19,7 @@ module TaskTempest
20
19
  end
21
20
 
22
21
  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)
22
+ new.queue.enqueue(message, *args)
26
23
  end
27
24
 
28
25
  def self.submit_task(task, *args)
@@ -37,115 +34,61 @@ module TaskTempest
37
34
  end
38
35
  end
39
36
 
40
- def initialize
41
- @tasks = []
42
- @bookkeeping_timer = Time.now
37
+ def self.run
38
+ new.run
43
39
  end
44
40
 
45
41
  def run
46
- bootstrap
47
- logger.info "starting run loop"
42
+ bootstrap(:halt)
48
43
  with_shutdown_handling{ heartbeat while true }
49
44
  end
50
45
 
51
46
  private
52
47
 
53
48
  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
49
+ time = Benchmark.realtime do
50
+ with_error_handling{ finish_tasks }
51
+ with_error_handling{ health_check }
52
+ with_error_handling{ bookkeeping }
96
53
  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.
54
+ logger.debug "heartbeat complete in #{time} seconds"
55
+ sleep(settings.pulse_delay)
111
56
  end
112
57
 
113
58
  def finish_tasks
114
- finished, @tasks = tasks.separate{ |task| task.execution.finished? }
115
- finished.each{ |task| handle_finished_task(task) }
59
+ @executions = storm.clear_executions(:finished?).each do |execution|
60
+ task = execution.args.first
61
+ if (e = execution.exception)
62
+ logger.info task.format_log("failed", true)
63
+ task.logger.fatal format_exception(e)
64
+ on_task_exception(task, e)
65
+ elsif execution.timed_out?
66
+ logger.info task.format_log("timed out", true)
67
+ on_task_timeout(task)
68
+ else
69
+ logger.info task.format_log("finished", true)
70
+ on_require(task, execution.value)
71
+ end
72
+ end
116
73
  end
117
74
 
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)
75
+ def health_check
76
+ if dispatcher.dead?
77
+ with_error_handling{ dispatcher.exception }
78
+ logger.error "dispatcher thread died, restarting"
79
+ dispatcher.restart
129
80
  end
130
81
  end
131
82
 
132
83
  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)
84
+ bookkeeper.report(@executions)
143
85
  end
144
86
 
145
87
  def clean_shutdown
146
- logger.info "shutting down"
88
+ logger.info "shutting down..."
147
89
  begin
148
90
  timeout(settings.shutdown_timeout) do
91
+ dispatcher.shutdown
149
92
  storm.join
150
93
  storm.shutdown
151
94
  end
@@ -153,6 +96,7 @@ module TaskTempest
153
96
  logger.warn "shutdown timeout exceeded"
154
97
  end
155
98
  finish_tasks
99
+ logger.info "shutdown"
156
100
  exit(0)
157
101
  end
158
102
 
@@ -7,17 +7,23 @@ module TaskTempest
7
7
  SignalException
8
8
  ]
9
9
 
10
- def with_error_handling(halt_on_error = false)
10
+ def with_error_handling(error_action = :continue)
11
11
  yield
12
12
  rescue *SHUTDOWN_EXCEPTIONS => e
13
13
  raise
14
14
  rescue Exception => e
15
15
  on_internal_exception(e)
16
- if halt_on_error
16
+ case error_action
17
+ when :halt
17
18
  logger.fatal format_exception(e)
18
19
  exit(-1)
19
- else
20
+ when :reraise
21
+ logger.fatal format_exception(e)
22
+ raise
23
+ when :continue
20
24
  logger.error format_exception(e)
25
+ else
26
+ raise "Wtf man, typo."
21
27
  end
22
28
  end
23
29
 
@@ -45,7 +51,7 @@ module TaskTempest
45
51
  end
46
52
 
47
53
  def format_exception(e)
48
- "#{e.class} #{e.message}\n" + e.backtrace.join("\n")
54
+ "#{e.class}: #{e.message}\n" + e.backtrace.join("\n")
49
55
  end
50
56
 
51
57
  end
@@ -0,0 +1,58 @@
1
+ require "thread"
2
+
3
+ require "task_tempest/error_handling"
4
+
5
+ module TaskTempest
6
+ class Producer
7
+ attr_reader :logger
8
+
9
+ DEFAULTS = {
10
+ :delay => 1,
11
+ :buffer_size => 1
12
+ }
13
+
14
+ def initialize(queue, logger, options = {})
15
+ options = DEFAULTS.merge(options)
16
+ @queue = queue
17
+ @logger = logger
18
+ @delay = options[:delay]
19
+ @buffer = SizedQueue.new(options[:buffer_size])
20
+ Thread.new{ run }
21
+ end
22
+
23
+ def consume
24
+ if @buffer.size > 0
25
+ @buffer.pop
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def run
34
+ run_loop while true
35
+ end
36
+
37
+ def run_loop
38
+ (dequeue and buffer) or delay
39
+ end
40
+
41
+ def dequeue
42
+ @message = @queue.dequeue
43
+ rescue Exception => e
44
+ @message = e
45
+ nil
46
+ end
47
+
48
+ def buffer
49
+ @buffer.push(@message)
50
+ end
51
+
52
+ def delay
53
+ logger.debug "producer sleeping (queue empty) for #{@delay.inspect}"
54
+ sleep(@delay)
55
+ end
56
+
57
+ end
58
+ end
@@ -5,22 +5,28 @@ module TaskTempest
5
5
  module Settings
6
6
 
7
7
  DEFAULTS = {
8
- :process_name => "task_tempest",
9
- :log_level => Logger::DEBUG,
8
+ # Basic settings
10
9
  :threads => 10,
11
- :no_thread_sleep => 1,
10
+ :log_level => Logger::INFO,
11
+ :queue => nil,
12
+ :bookkeeping_interval => 10*60, # 10 minutes
13
+ :log_name => nil,
14
+
15
+ # Delay settings
12
16
  :no_message_sleep => 1,
17
+ :pulse_delay => 0.25,
18
+
19
+ # Timeout settings
20
+ :timeout_method => Timeout.method(:timeout),
13
21
  :task_timeout => nil,
14
22
  :shutdown_timeout => 5, # 5 seconds
15
- :dequeue_timeout => 2, # 2 seconds
16
- :timeout_method => Timeout.method(:timeout),
23
+
24
+ # Directory settings
17
25
  :root_dir => File.expand_path(Dir.pwd),
18
26
  :log_dir => File.expand_path(Dir.pwd),
19
27
  :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
28
+
29
+ # Callback settings
24
30
  :before_initialize => Proc.new{ |logger| },
25
31
  :after_initialize => Proc.new{ |logger| },
26
32
  :on_internal_exception => Proc.new{ |e, logger| },
@@ -47,10 +53,6 @@ module TaskTempest
47
53
 
48
54
  module ClassMethods
49
55
 
50
- def process_name(value)
51
- settings.process_name = value
52
- end
53
-
54
56
  def log_level(value)
55
57
  settings.log_level = value
56
58
  end
@@ -59,12 +61,16 @@ module TaskTempest
59
61
  settings.threads = value
60
62
  end
61
63
 
64
+ def log_name(value)
65
+ settings.log_name = value
66
+ end
67
+
62
68
  def no_message_sleep(value)
63
69
  settings.no_message_sleep = value
64
70
  end
65
71
 
66
- def no_thread_sleep(value)
67
- settings.no_thread_sleep = value
72
+ def pulse_delay(value)
73
+ settings.pulse_delay = value
68
74
  end
69
75
 
70
76
  def root_dir(path)
@@ -95,16 +101,12 @@ module TaskTempest
95
101
  settings.shutdown_timeout = value.to_f
96
102
  end
97
103
 
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
104
+ def queue(queue = nil, &block)
105
+ if block_given?
106
+ settings.queue = block
107
+ else
108
+ settings.queue = queue
109
+ end
108
110
  end
109
111
 
110
112
  def bookkeeping_interval(value)
@@ -6,20 +6,18 @@ require "task_tempest/require"
6
6
 
7
7
  module TaskTempest
8
8
  class Task
9
- attr_reader :id, :args, :execution
9
+ attr_reader :id, :args
10
+ attr_accessor :execution
10
11
 
11
12
  def initialize(*args)
12
13
  @id = generate_id
13
14
  @args = args
14
15
  end
15
16
 
16
- def override(options = {})
17
+ def init(options = {})
17
18
  @id = options[:id] if options[:id]
18
19
  @logger = TaskLogger.new(options[:logger], self) if options[:logger]
19
- end
20
-
21
- def spawn(storm)
22
- @execution = storm.execute{ run }
20
+ self
23
21
  end
24
22
 
25
23
  def run
@@ -38,9 +36,9 @@ module TaskTempest
38
36
  [id, self.class.name, *args]
39
37
  end
40
38
 
41
- def format_log(message, duration = false)
39
+ def format_log(message, show_duration = false)
42
40
  s = "{#{id}} <#{self.class}> #{message}"
43
- s += " #{execution.duration}" if duration and execution.finished?
41
+ s += " #{execution.duration.round(3)}" if show_duration and execution.finished?
44
42
  s
45
43
  end
46
44
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{task_tempest}
8
- s.version = "0.1.0"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Christopher J. Bottaro"]
12
- s.date = %q{2010-06-24}
12
+ s.date = %q{2010-07-07}
13
13
  s.description = %q{Framework for creating queue based, threaded asychronous job processors.}
14
14
  s.email = %q{cjbottaro@alumni.cs.utexas.edu}
15
15
  s.extra_rdoc_files = [
@@ -24,16 +24,18 @@ Gem::Specification.new do |s|
24
24
  "README.rdoc",
25
25
  "Rakefile",
26
26
  "VERSION",
27
- "examples/my_tempest.rb",
28
- "examples/tasks/evaler.rb",
29
- "examples/tasks/greeter.rb",
27
+ "example/my_tempest.rb",
28
+ "example/tasks/evaler.rb",
29
+ "example/tasks/greeter.rb",
30
30
  "lib/task_tempest.rb",
31
31
  "lib/task_tempest/active_support.rb",
32
32
  "lib/task_tempest/bookkeeper.rb",
33
33
  "lib/task_tempest/bootstrap.rb",
34
34
  "lib/task_tempest/callbacks.rb",
35
+ "lib/task_tempest/dispatcher.rb",
35
36
  "lib/task_tempest/engine.rb",
36
37
  "lib/task_tempest/error_handling.rb",
38
+ "lib/task_tempest/producer.rb",
37
39
  "lib/task_tempest/require.rb",
38
40
  "lib/task_tempest/settings.rb",
39
41
  "lib/task_tempest/task.rb",
@@ -45,21 +47,18 @@ Gem::Specification.new do |s|
45
47
  s.homepage = %q{http://github.com/cjbottaro/task_tempest}
46
48
  s.rdoc_options = ["--charset=UTF-8"]
47
49
  s.require_paths = ["lib"]
48
- s.rubygems_version = %q{1.3.7}
50
+ s.rubygems_version = %q{1.3.6}
49
51
  s.summary = %q{Framework for creating asychronous job processors.}
50
52
  s.test_files = [
51
53
  "test/helper.rb",
52
- "test/test_task_tempest.rb",
53
- "examples/my_tempest.rb",
54
- "examples/tasks/evaler.rb",
55
- "examples/tasks/greeter.rb"
54
+ "test/test_task_tempest.rb"
56
55
  ]
57
56
 
58
57
  if s.respond_to? :specification_version then
59
58
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
60
59
  s.specification_version = 3
61
60
 
62
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
63
62
  else
64
63
  end
65
64
  else
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: task_tempest
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
5
4
  prerelease: false
6
5
  segments:
7
6
  - 0
8
- - 1
7
+ - 2
9
8
  - 0
10
- version: 0.1.0
9
+ version: 0.2.0
11
10
  platform: ruby
12
11
  authors:
13
12
  - Christopher J. Bottaro
@@ -15,7 +14,7 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2010-06-24 00:00:00 -05:00
17
+ date: 2010-07-07 00:00:00 -05:00
19
18
  default_executable:
20
19
  dependencies: []
21
20
 
@@ -36,16 +35,18 @@ files:
36
35
  - README.rdoc
37
36
  - Rakefile
38
37
  - VERSION
39
- - examples/my_tempest.rb
40
- - examples/tasks/evaler.rb
41
- - examples/tasks/greeter.rb
38
+ - example/my_tempest.rb
39
+ - example/tasks/evaler.rb
40
+ - example/tasks/greeter.rb
42
41
  - lib/task_tempest.rb
43
42
  - lib/task_tempest/active_support.rb
44
43
  - lib/task_tempest/bookkeeper.rb
45
44
  - lib/task_tempest/bootstrap.rb
46
45
  - lib/task_tempest/callbacks.rb
46
+ - lib/task_tempest/dispatcher.rb
47
47
  - lib/task_tempest/engine.rb
48
48
  - lib/task_tempest/error_handling.rb
49
+ - lib/task_tempest/producer.rb
49
50
  - lib/task_tempest/require.rb
50
51
  - lib/task_tempest/settings.rb
51
52
  - lib/task_tempest/task.rb
@@ -63,33 +64,26 @@ rdoc_options:
63
64
  require_paths:
64
65
  - lib
65
66
  required_ruby_version: !ruby/object:Gem::Requirement
66
- none: false
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- hash: 3
71
70
  segments:
72
71
  - 0
73
72
  version: "0"
74
73
  required_rubygems_version: !ruby/object:Gem::Requirement
75
- none: false
76
74
  requirements:
77
75
  - - ">="
78
76
  - !ruby/object:Gem::Version
79
- hash: 3
80
77
  segments:
81
78
  - 0
82
79
  version: "0"
83
80
  requirements: []
84
81
 
85
82
  rubyforge_project:
86
- rubygems_version: 1.3.7
83
+ rubygems_version: 1.3.6
87
84
  signing_key:
88
85
  specification_version: 3
89
86
  summary: Framework for creating asychronous job processors.
90
87
  test_files:
91
88
  - test/helper.rb
92
89
  - test/test_task_tempest.rb
93
- - examples/my_tempest.rb
94
- - examples/tasks/evaler.rb
95
- - examples/tasks/greeter.rb