toro 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,143 @@
1
+ module Toro
2
+ class Shutdown < Interrupt; end
3
+
4
+ class Manager
5
+ include Actor
6
+ include ActorManager
7
+
8
+ attr_reader :busy, :ready
9
+
10
+ def initialize(options={})
11
+ defaults = {
12
+ concurrency: 1,
13
+ queues: [Toro.options[:default_queue]],
14
+ }
15
+ options = defaults.merge(options)
16
+ @queues = options[:queues]
17
+ @threads = {}
18
+ @ready = options[:concurrency].times.map do
19
+ processor = Processor.new_link(current_actor)
20
+ processor.proxy_id = processor.object_id
21
+ processor
22
+ end
23
+ @busy = []
24
+ @is_done = false
25
+ @fetcher = Fetcher.new({ manager: current_actor, queues: options[:queues] })
26
+ @listener = Listener.new({ queues: @queues, fetcher: @fetcher, manager: current_actor })
27
+ end
28
+
29
+ def start
30
+ @is_done = false
31
+ @listener.async.start
32
+ @ready.each { dispatch }
33
+ heartbeat
34
+ end
35
+
36
+ def stop
37
+ @is_done = true
38
+
39
+ Toro.logger.debug "Shutting down #{@ready.size} quiet workers"
40
+ @ready.each { |processor| processor.terminate if processor.alive? }
41
+ @ready.clear
42
+ @fetcher.terminate if @fetcher.alive?
43
+ if @listener.alive?
44
+ actors[:listener].stop if actors[:listener]
45
+ @listener.terminate
46
+ end
47
+ return if clean_up_for_graceful_shutdown
48
+
49
+ hard_shutdown_in(Toro.options[:hard_shutdown_time])
50
+ end
51
+
52
+ def assign(job)
53
+ raise 'No processors ready' if !is_ready?
54
+ processor = @ready.pop
55
+ @busy << processor
56
+ processor.async.process(job)
57
+ end
58
+
59
+ def is_ready?
60
+ !@ready.empty?
61
+ end
62
+
63
+ def dispatch
64
+ raise "No processors, cannot continue!" if @ready.empty? && @busy.empty?
65
+ raise "No ready processor!?" if @ready.empty?
66
+ @fetcher.async.fetch
67
+ end
68
+
69
+ def clean_up_for_graceful_shutdown
70
+ if @busy.empty?
71
+ shutdown
72
+ return true
73
+ end
74
+
75
+ after(Toro.options[:graceful_shutdown_time]) { clean_up_for_graceful_shutdown }
76
+ false
77
+ end
78
+
79
+ def hard_shutdown_in(delay)
80
+ Toro.logger.info "Pausing up to #{delay} seconds to allow workers to finish..."
81
+
82
+ after(delay) do
83
+ # We've reached the timeout and we still have busy processors.
84
+ # They must die but their messages shall live on.
85
+ Toro.logger.warn "Terminating #{@busy.size} busy worker threads"
86
+
87
+ requeue
88
+
89
+ @busy.each do |processor|
90
+ if processor.alive? && thread = @threads.delete(processor.object_id)
91
+ thread.raise Shutdown
92
+ end
93
+ end
94
+
95
+ signal_shutdown
96
+ end
97
+ end
98
+
99
+ def shutdown
100
+ requeue
101
+ signal_shutdown
102
+ end
103
+
104
+ def requeue
105
+ Toro::Database.with_connection do
106
+ Job.where(status: 'running', started_by: Toro.process_identity).
107
+ update_all(status: 'queued', started_by: nil, started_at: nil)
108
+ end
109
+ end
110
+
111
+ def signal_shutdown
112
+ after(0) { signal(:shutdown) }
113
+ end
114
+
115
+ def set_thread(proxy_id, thread)
116
+ @threads[proxy_id] = thread
117
+ end
118
+
119
+ def heartbeat
120
+ return if stopped?
121
+
122
+ after(5) do
123
+ heartbeat
124
+ end
125
+ end
126
+
127
+ def processor_complete(processor)
128
+ @threads.delete(processor.object_id)
129
+ @busy.delete(processor)
130
+ if stopped?
131
+ processor.terminate if processor.alive?
132
+ shutdown if @busy.empty?
133
+ else
134
+ @ready << processor if processor.alive?
135
+ dispatch
136
+ end
137
+ end
138
+
139
+ def stopped?
140
+ @is_done
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,81 @@
1
+ module Toro
2
+ module Middleware
3
+ class Chain
4
+ include Enumerable
5
+ attr_reader :entries
6
+
7
+ def initialize_copy(copy)
8
+ copy.instance_variable_set(:@entries, entries.dup)
9
+ end
10
+
11
+ def each(&block)
12
+ entries.each(&block)
13
+ end
14
+
15
+ def initialize
16
+ @entries = []
17
+ yield self if block_given?
18
+ end
19
+
20
+ def remove(klass)
21
+ entries.delete_if { |entry| entry.klass == klass }
22
+ end
23
+
24
+ def add(klass, *args)
25
+ remove(klass) if exists?(klass)
26
+ entries << Entry.new(klass, *args)
27
+ end
28
+
29
+ def insert_before(oldklass, newklass, *args)
30
+ i = entries.index { |entry| entry.klass == newklass }
31
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
32
+ i = entries.index { |entry| entry.klass == oldklass } || 0
33
+ entries.insert(i, new_entry)
34
+ end
35
+
36
+ def insert_after(oldklass, newklass, *args)
37
+ i = entries.index { |entry| entry.klass == newklass }
38
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
39
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
40
+ entries.insert(i+1, new_entry)
41
+ end
42
+
43
+ def exists?(klass)
44
+ any? { |entry| entry.klass == klass }
45
+ end
46
+
47
+ def retrieve
48
+ map(&:make_new)
49
+ end
50
+
51
+ def clear
52
+ entries.clear
53
+ end
54
+
55
+ def invoke(*args, &final_action)
56
+ chain = retrieve.dup
57
+ traverse_chain = lambda do
58
+ if chain.empty?
59
+ final_action.call
60
+ else
61
+ chain.shift.call(*args, &traverse_chain)
62
+ end
63
+ end
64
+ traverse_chain.call
65
+ end
66
+ end
67
+
68
+ class Entry
69
+ attr_reader :klass
70
+
71
+ def initialize(klass, *args)
72
+ @klass = klass
73
+ @args = args
74
+ end
75
+
76
+ def make_new
77
+ @klass.new(*@args)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ module Toro
2
+ module Middleware
3
+ module Server
4
+ class Error
5
+ def call(job, worker)
6
+ begin
7
+ yield
8
+ rescue Exception => exception
9
+ job.update_attributes(
10
+ status: 'failed',
11
+ finished_at: Time.now
12
+ )
13
+ raise exception
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module Toro
2
+ module Middleware
3
+ module Server
4
+ class ErrorStorage
5
+ def call(job, worker)
6
+ begin
7
+ yield
8
+ rescue Exception => exception
9
+ job.reload
10
+ job.set_properties(
11
+ 'error:class' => exception.class.name,
12
+ 'error:message' => exception.message,
13
+ 'error:backtrace' => exception.backtrace
14
+ )
15
+ job.save
16
+ raise exception
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Toro
2
+ module Middleware
3
+ module Server
4
+ class Properties
5
+ def call(job, worker)
6
+ result = yield
7
+ if result.is_a?(Hash) && result[:job_properties].is_a?(Hash)
8
+ job.set_properties(result[:job_properties])
9
+ job.save
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module Toro
2
+ module Middleware
3
+ module Server
4
+ class Retry
5
+ def call(job, worker)
6
+ begin
7
+ yield
8
+ rescue Exception => exception
9
+ if worker.toro_options[:retry_interval]
10
+ interval = worker.toro_options[:retry_interval]
11
+ job.reload
12
+ job.properties ||= {}
13
+ job.properties['retry:errors'] ||= []
14
+ job.properties['retry:errors'] << "#{exception.class.name} -- #{exception.message} -- #{Time.now}"
15
+ job.status = 'scheduled'
16
+ job.scheduled_at = Time.now + interval
17
+ job.save
18
+ end
19
+ raise exception
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ require 'slim'
2
+ require 'jquery-datatables-rails'
3
+ require 'rails-datatables'
4
+ require 'action_view'
5
+
6
+ directory = File.dirname(File.absolute_path(__FILE__))
7
+ require "#{directory}/monitor/custom_views.rb"
8
+ require "#{directory}/monitor/time_formatter.rb"
9
+ require "#{directory}/monitor/engine.rb" if defined?(Rails)
10
+
11
+ module Toro
12
+ module Monitor
13
+ DEFAULTS = {
14
+ :charts => nil,
15
+ :javascripts => [],
16
+ :poll_interval => 3000
17
+ }
18
+
19
+ class << self
20
+ def options
21
+ @options ||= DEFAULTS.dup
22
+ end
23
+
24
+ def options=(options)
25
+ @options = options
26
+ end
27
+
28
+ def root_path
29
+ toro_monitor_path = Toro::Monitor::Engine.routes.url_helpers.toro_monitor_path
30
+ "#{::Rails.application.config.relative_url_root}#{toro_monitor_path}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ module Toro
2
+ module Monitor
3
+ class CustomViews
4
+ @views = []
5
+
6
+ class << self
7
+ def add(name, path, &block)
8
+ @views << {
9
+ name: name,
10
+ path: path,
11
+ filter: block
12
+ }
13
+ end
14
+
15
+ def for_job(job)
16
+ views = []
17
+ @views.each do |view|
18
+ is_valid = view[:filter].call(job)
19
+ views << view.dup if is_valid
20
+ end
21
+ views
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Toro
2
+ module Monitor
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Monitor
5
+
6
+ initializer "toro.asset_pipeline" do |app|
7
+ app.config.assets.precompile << 'toro/monitor/application.js'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ module Toro
2
+ module Monitor
3
+ class TimeFormatter
4
+ class << self
5
+ include ActionView::Helpers::DateHelper
6
+
7
+ def distance_of_time(from_time, to_time)
8
+ replacements = {
9
+ 'less than ' => '',
10
+ 'about ' => '',
11
+ ' days' => 'd',
12
+ ' day' => 'd',
13
+ ' hours' => 'h',
14
+ ' hour' => 'h',
15
+ ' minutes' => 'm',
16
+ ' minute' => 'm',
17
+ ' seconds' => 's',
18
+ ' second' => 's'
19
+ }
20
+ phrase = distance_of_time_in_words(from_time, to_time, include_seconds: true)
21
+ replacements.each { |from, to| phrase.gsub!(from, to) }
22
+ phrase
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ module Toro
2
+ class Processor
3
+ include Actor
4
+
5
+ attr_accessor :proxy_id
6
+
7
+ class << self
8
+ def default_middleware
9
+ Middleware::Chain.new do |middleware|
10
+ middleware.add Middleware::Server::Properties
11
+ middleware.add Middleware::Server::Retry
12
+ middleware.add Middleware::Server::ErrorStorage
13
+ middleware.add Middleware::Server::Error
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(manager)
19
+ @manager = manager
20
+ end
21
+
22
+ def process(job)
23
+ @manager.set_thread(proxy_id, Thread.current)
24
+
25
+ Toro.logger.info "Processing #{job}"
26
+ worker = job.class_name.constantize
27
+
28
+ Toro::Database.with_connection do
29
+ begin
30
+ Toro.server_middleware.invoke(job, worker) do
31
+ worker.new.perform(*job.args)
32
+ end
33
+ rescue Exception => exception
34
+ Toro.logger.error "#{exception.class}: #{exception.message}"
35
+ Toro.logger.error exception.backtrace.join("\n")
36
+ else
37
+ Toro.logger.info "Processed #{job}"
38
+ job.update_attributes(
39
+ status: 'complete',
40
+ finished_at: Time.now
41
+ )
42
+ end
43
+ end
44
+
45
+ @manager.processor_complete(current_actor) if @manager.alive?
46
+ end
47
+ end
48
+ end