toro 0.0.1 → 0.0.2

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.
@@ -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