exekutor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/LICENSE.txt +21 -0
- data/exe/exekutor +7 -0
- data/lib/active_job/queue_adapters/exekutor_adapter.rb +14 -0
- data/lib/exekutor/asynchronous.rb +188 -0
- data/lib/exekutor/cleanup.rb +56 -0
- data/lib/exekutor/configuration.rb +373 -0
- data/lib/exekutor/hook.rb +172 -0
- data/lib/exekutor/info/worker.rb +20 -0
- data/lib/exekutor/internal/base_record.rb +11 -0
- data/lib/exekutor/internal/callbacks.rb +138 -0
- data/lib/exekutor/internal/cli/app.rb +173 -0
- data/lib/exekutor/internal/cli/application_loader.rb +36 -0
- data/lib/exekutor/internal/cli/cleanup.rb +96 -0
- data/lib/exekutor/internal/cli/daemon.rb +108 -0
- data/lib/exekutor/internal/cli/default_option_value.rb +29 -0
- data/lib/exekutor/internal/cli/info.rb +126 -0
- data/lib/exekutor/internal/cli/manager.rb +260 -0
- data/lib/exekutor/internal/configuration_builder.rb +113 -0
- data/lib/exekutor/internal/database_connection.rb +21 -0
- data/lib/exekutor/internal/executable.rb +75 -0
- data/lib/exekutor/internal/executor.rb +242 -0
- data/lib/exekutor/internal/hooks.rb +87 -0
- data/lib/exekutor/internal/listener.rb +176 -0
- data/lib/exekutor/internal/logger.rb +74 -0
- data/lib/exekutor/internal/provider.rb +308 -0
- data/lib/exekutor/internal/reserver.rb +95 -0
- data/lib/exekutor/internal/status_server.rb +132 -0
- data/lib/exekutor/job.rb +31 -0
- data/lib/exekutor/job_error.rb +11 -0
- data/lib/exekutor/job_options.rb +95 -0
- data/lib/exekutor/plugins/appsignal.rb +46 -0
- data/lib/exekutor/plugins.rb +13 -0
- data/lib/exekutor/queue.rb +141 -0
- data/lib/exekutor/version.rb +6 -0
- data/lib/exekutor/worker.rb +219 -0
- data/lib/exekutor.rb +49 -0
- data/lib/generators/exekutor/configuration_generator.rb +18 -0
- data/lib/generators/exekutor/install_generator.rb +43 -0
- data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +7 -0
- data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +7 -0
- data/lib/generators/exekutor/templates/install/initializers/exekutor.rb.erb +14 -0
- data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +83 -0
- data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +6 -0
- data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +5 -0
- data.tar.gz.sig +0 -0
- metadata +403 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exekutor
|
4
|
+
# Defines hooks for Exekutor.
|
5
|
+
#
|
6
|
+
# @example Define and register hooks
|
7
|
+
# class ExekutorHooks
|
8
|
+
# include Exekutor::Hook
|
9
|
+
# around_job_execution :instrument
|
10
|
+
# after_job_failure {|_job, error| report_error error }
|
11
|
+
# after_fatal_error :report_error
|
12
|
+
#
|
13
|
+
# def instrument(job)
|
14
|
+
# ErrorMonitoring.monitor_transaction { yield }
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def report_error(error)
|
18
|
+
# ErrorMonitoring.report error
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Exekutor.hooks.register ExekutorHooks
|
23
|
+
module Hook
|
24
|
+
extend ActiveSupport::Concern
|
25
|
+
|
26
|
+
CALLBACK_NAMES = %i[
|
27
|
+
before_enqueue around_enqueue after_enqueue before_job_execution around_job_execution after_job_execution
|
28
|
+
on_job_failure on_fatal_error before_startup after_startup before_shutdown after_shutdown
|
29
|
+
].freeze
|
30
|
+
private_constant "CALLBACK_NAMES"
|
31
|
+
|
32
|
+
included do
|
33
|
+
class_attribute :__callbacks, default: Hash.new { |h, k| h[k] = [] }
|
34
|
+
|
35
|
+
private_class_method :add_callback!
|
36
|
+
end
|
37
|
+
|
38
|
+
# Gets the registered callbacks
|
39
|
+
# @return [Hash<Symbol,Array<Proc>>] the callbacks
|
40
|
+
def callbacks
|
41
|
+
instance = self
|
42
|
+
__callbacks.transform_values do |callbacks|
|
43
|
+
callbacks.map do |method, callback|
|
44
|
+
if method
|
45
|
+
method(method)
|
46
|
+
elsif callback.arity.zero?
|
47
|
+
-> { instance.instance_exec(&callback) }
|
48
|
+
else
|
49
|
+
->(*args) { instance.instance_exec(*args, &callback) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class_methods do
|
56
|
+
|
57
|
+
# @!method before_enqueue
|
58
|
+
# Registers a callback to be called before a job is enqueued.
|
59
|
+
# @param methods [Symbol] the method(s) to call
|
60
|
+
# @yield the block to call
|
61
|
+
# @yieldparam job [ActiveJob::Base] the job to enqueue
|
62
|
+
# @return [void]
|
63
|
+
|
64
|
+
# @!method after_enqueue
|
65
|
+
# Registers a callback to be called after a job is enqueued.
|
66
|
+
# @param methods [Symbol] the method(s) to call
|
67
|
+
# @yield the block to call
|
68
|
+
# @yieldparam job [ActiveJob::Base] the enqueued job
|
69
|
+
# @return [void]
|
70
|
+
|
71
|
+
# @!method around_enqueue
|
72
|
+
# Registers a callback to be called when a job is enqueued. You must call +yield+ from the callback.
|
73
|
+
# @param methods [Symbol] the method(s) to call
|
74
|
+
# @yield the block to call
|
75
|
+
# @yieldparam job [ActiveJob::Base] the job to enqueue
|
76
|
+
# @return [void]
|
77
|
+
|
78
|
+
# @!method before_job_execution
|
79
|
+
# Registers a callback to be called before a job is executed.
|
80
|
+
# @param methods [Symbol] the method(s) to call
|
81
|
+
# @yield the block to call
|
82
|
+
# @yieldparam job [Hash] the job to execute
|
83
|
+
# @return [void]
|
84
|
+
|
85
|
+
# @!method after_job_execution
|
86
|
+
# Registers a callback to be called after a job is executed.
|
87
|
+
# @param methods [Symbol] the method(s) to call
|
88
|
+
# @yield the block to call
|
89
|
+
# @yieldparam job [Hash] the executed job
|
90
|
+
# @return [void]
|
91
|
+
|
92
|
+
# @!method around_job_execution
|
93
|
+
# Registers a callback to be called when a job is executed. You must call +yield+ from the callback.
|
94
|
+
# @param methods [Symbol] the method(s) to call
|
95
|
+
# @yield the block to call
|
96
|
+
# @yieldparam job [Hash] the job to execute
|
97
|
+
# @return [void]
|
98
|
+
|
99
|
+
# @!method on_job_failure
|
100
|
+
# Registers a callback to be called when a job raises an error.
|
101
|
+
# @param methods [Symbol] the method(s) to call
|
102
|
+
# @yield the block to call
|
103
|
+
# @yieldparam job [Hash] the job that was executed
|
104
|
+
# @yieldparam error [StandardError] the error that was raised
|
105
|
+
# @return [void]
|
106
|
+
|
107
|
+
# @!method on_fatal_error
|
108
|
+
# Registers a callback to be called when an error is raised from a worker outside a job.
|
109
|
+
# @param methods [Symbol] the method(s) to call
|
110
|
+
# @yield the block to call
|
111
|
+
# @yieldparam error [StandardError] the error that was raised
|
112
|
+
# @return [void]
|
113
|
+
|
114
|
+
# @!method before_startup
|
115
|
+
# Registers a callback to be called before a worker is starting up.
|
116
|
+
# @param methods [Symbol] the method(s) to call
|
117
|
+
# @yield the block to call
|
118
|
+
# @yieldparam worker [Worker] the worker
|
119
|
+
# @return [void]
|
120
|
+
|
121
|
+
# @!method after_startup
|
122
|
+
# Registers a callback to be called after a worker has started up.
|
123
|
+
# @param methods [Symbol] the method(s) to call
|
124
|
+
# @yield the block to call
|
125
|
+
# @yieldparam worker [Worker] the worker
|
126
|
+
# @return [void]
|
127
|
+
|
128
|
+
# @!method before_shutdown
|
129
|
+
# Registers a callback to be called before a worker is shutting down.
|
130
|
+
# @param methods [Symbol] the method(s) to call
|
131
|
+
# @yield the block to call
|
132
|
+
# @yieldparam worker [Worker] the worker
|
133
|
+
# @return [void]
|
134
|
+
|
135
|
+
# @!method after_shutdown
|
136
|
+
# Registers a callback to be called after a worker has shutdown.
|
137
|
+
# @param methods [Symbol] the method(s) to call
|
138
|
+
# @yield the block to call
|
139
|
+
# @yieldparam worker [Worker] the worker
|
140
|
+
# @return [void]
|
141
|
+
|
142
|
+
CALLBACK_NAMES.each do |name|
|
143
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
144
|
+
def #{name}(*methods, &callback)
|
145
|
+
add_callback! :#{name}, methods, callback
|
146
|
+
end
|
147
|
+
RUBY
|
148
|
+
end
|
149
|
+
|
150
|
+
# Adds a callback.
|
151
|
+
# @param type [Symbol] the callback to register
|
152
|
+
# @param methods [Symbol] the method(s) to call
|
153
|
+
# @yield the block to call
|
154
|
+
def add_callback(type, *methods, &callback)
|
155
|
+
unless CALLBACK_NAMES.include? type
|
156
|
+
raise Error, "Invalid callback type: #{type} (Expected one of: #{CALLBACK_NAMES.map(&:inspect).join(", ")}"
|
157
|
+
end
|
158
|
+
|
159
|
+
add_callback! type, methods, callback
|
160
|
+
true
|
161
|
+
end
|
162
|
+
|
163
|
+
def add_callback!(type, methods, callback)
|
164
|
+
raise Error, "No method or callback block supplied" if methods.blank? && callback.nil?
|
165
|
+
raise Error, "Either a method or a callback block must be supplied" if methods.present? && callback.present?
|
166
|
+
|
167
|
+
methods&.each { |method| __callbacks[type] << [method, nil] }
|
168
|
+
__callbacks[type] << [nil, callback] if callback.present?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../internal/base_record"
|
4
|
+
|
5
|
+
module Exekutor
|
6
|
+
# Module for the Worker active record class
|
7
|
+
module Info
|
8
|
+
# Active record class for a worker instance
|
9
|
+
class Worker < Internal::BaseRecord
|
10
|
+
self.implicit_order_column = :started_at
|
11
|
+
enum status: { initializing: "i", running: "r", shutting_down: "s", crashed: "c" }
|
12
|
+
|
13
|
+
# Registers a heartbeat for this worker, if necessary
|
14
|
+
def heartbeat!
|
15
|
+
now = Time.now.change(sec: 0)
|
16
|
+
touch :last_heartbeat_at, time: now if self.last_heartbeat_at.nil? || now >= self.last_heartbeat_at + 1.minute
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Exekutor
|
3
|
+
# @private
|
4
|
+
module Internal
|
5
|
+
# The base class for Exekutor active record classes
|
6
|
+
class BaseRecord < Exekutor.config.base_record_class
|
7
|
+
self.abstract_class = true
|
8
|
+
self.table_name_prefix = "exekutor_"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Exekutor
|
2
|
+
module Internal
|
3
|
+
# Mixin to define callbacks on a class
|
4
|
+
#
|
5
|
+
# @example Define and call callbacks
|
6
|
+
# class MyClass
|
7
|
+
# include Exekutor::Internal::Callbacks
|
8
|
+
#
|
9
|
+
# define_callbacks :on_event, :before_another_event, :after_another_event
|
10
|
+
#
|
11
|
+
# def emit_event
|
12
|
+
# run_callbacks :on, :event, "Callback arg"
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def emit_another_event
|
16
|
+
# with_callbacks :another_event, self do |self_arg|
|
17
|
+
# puts "another event"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# MyClass.new.on_event(12) {|str, int| puts "event happened: #{str}, #{int}" }
|
22
|
+
module Callbacks
|
23
|
+
extend ActiveSupport::Concern
|
24
|
+
|
25
|
+
included do
|
26
|
+
class_attribute :__callback_names, instance_writer: false, default: []
|
27
|
+
attr_reader :__callbacks
|
28
|
+
protected :__callbacks
|
29
|
+
end
|
30
|
+
|
31
|
+
# Adds a callback.
|
32
|
+
# @param type [Symbol] the type of callback to add
|
33
|
+
# @param args [Any] the args to forward to the callback
|
34
|
+
# @yield the block to call
|
35
|
+
# @yieldparam *args [Any] the callback args, appended by the specified args
|
36
|
+
def add_callback(type, *args, &callback)
|
37
|
+
unless __callback_names.include? type
|
38
|
+
raise Error, "Invalid callback type: #{type} (Expected one of: #{__callback_names.map(&:inspect).join(", ")}"
|
39
|
+
end
|
40
|
+
raise Error, "No callback block supplied" if callback.nil?
|
41
|
+
|
42
|
+
add_callback! type, args, callback
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
# Runs all callbacks for the specified type and action.
|
49
|
+
# @param type [:on, :before, :around, :after] the type of the callback
|
50
|
+
# @param action [Symbol] the name of the callback
|
51
|
+
# @param args [Any] the callback args
|
52
|
+
def run_callbacks(type, action, *args)
|
53
|
+
callbacks = __callbacks && __callbacks[:"#{type}_#{action}"]
|
54
|
+
unless callbacks
|
55
|
+
yield(*args) if block_given?
|
56
|
+
return
|
57
|
+
end
|
58
|
+
if type == :around
|
59
|
+
# Chain all callbacks together, ending with the original given block
|
60
|
+
callbacks.inject(-> { yield(*args) }) do |next_callback, (callback, extra_args)|
|
61
|
+
callback_args = if callback.arity.zero?
|
62
|
+
[]
|
63
|
+
else
|
64
|
+
args + extra_args
|
65
|
+
end
|
66
|
+
lambda do
|
67
|
+
has_yielded = false
|
68
|
+
callback.call(*callback_args) { has_yielded = true; next_callback.call }
|
69
|
+
raise MissingYield, "Callback did not yield!" unless has_yielded
|
70
|
+
rescue StandardError => err
|
71
|
+
raise if err.is_a? MissingYield
|
72
|
+
Exekutor.on_fatal_error err, "[Executor] Callback error!"
|
73
|
+
next_callback.call
|
74
|
+
end
|
75
|
+
end.call
|
76
|
+
return
|
77
|
+
end
|
78
|
+
iterator = type == :after ? :each : :reverse_each
|
79
|
+
callbacks.send(iterator) do |(callback, extra_args)|
|
80
|
+
if callback.arity.zero?
|
81
|
+
callback.call
|
82
|
+
else
|
83
|
+
callback.call(*(args + extra_args))
|
84
|
+
end
|
85
|
+
rescue StandardError => err
|
86
|
+
Exekutor.on_fatal_error err, "[Executor] Callback error!"
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# Runs :before, :around, and :after callbacks for the specified action.
|
92
|
+
def with_callbacks(action, *args)
|
93
|
+
run_callbacks :before, action, *args
|
94
|
+
run_callbacks(:around, action, *args) { |*fargs| yield(*fargs) }
|
95
|
+
run_callbacks :after, action, *args
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def add_callback!(type, args, callback)
|
102
|
+
@__callbacks ||= Concurrent::Hash.new
|
103
|
+
__callbacks[type] ||= Concurrent::Array.new
|
104
|
+
__callbacks[type] << [callback, args]
|
105
|
+
end
|
106
|
+
|
107
|
+
class_methods do
|
108
|
+
# Defines the specified callbacks on this class. Also defines a method with the given name to register the callback.
|
109
|
+
# @param callbacks [Symbol] the callback names to define. Must start with +on_+, +before_+, +after_+, or +around_+.
|
110
|
+
# @param freeze [Boolean] if true, freezes the callbacks so that no other callbacks can be defined
|
111
|
+
# @raise [Error] if a callback name is invalid or if the callbacks are frozen
|
112
|
+
def define_callbacks(*callbacks, freeze: true)
|
113
|
+
raise Error, "Callbacks are frozen, no other callbacks may be defined" if __callback_names.frozen?
|
114
|
+
callbacks.each do |name|
|
115
|
+
unless /^(on_)|(before_)|(after_)|(around_)[a-z]+/.match? name.to_s
|
116
|
+
raise Error, "Callback name must start with `on_`, `before_`, `after_`, or `around_`"
|
117
|
+
end
|
118
|
+
|
119
|
+
__callback_names << name
|
120
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
121
|
+
def #{name}(*args, &callback)
|
122
|
+
add_callback! :#{name}, args, callback
|
123
|
+
end
|
124
|
+
RUBY
|
125
|
+
end
|
126
|
+
|
127
|
+
__callback_names.freeze if freeze
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Raised when registering a callback fails
|
132
|
+
class Error < Exekutor::Error; end
|
133
|
+
|
134
|
+
# Raised when an around callback does not yield
|
135
|
+
class MissingYield < Exekutor::Error; end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require "gli"
|
2
|
+
require "rainbow"
|
3
|
+
require_relative "cleanup"
|
4
|
+
require_relative "info"
|
5
|
+
require_relative "manager"
|
6
|
+
|
7
|
+
module Exekutor
|
8
|
+
# @private
|
9
|
+
module Internal
|
10
|
+
# The internal command line interface for Exekutor
|
11
|
+
# @private
|
12
|
+
module CLI
|
13
|
+
# Converts the command line arguments to Manager calls
|
14
|
+
# @private
|
15
|
+
class App
|
16
|
+
extend GLI::App
|
17
|
+
|
18
|
+
program_desc "Exekutor CLI"
|
19
|
+
version Exekutor::VERSION
|
20
|
+
|
21
|
+
default_command :start
|
22
|
+
|
23
|
+
flag %i[id identifier], default_value: nil,
|
24
|
+
desc: "Descriptor of the worker instance, is used in the pid file and shown in the worker info"
|
25
|
+
flag %i[pid pidfile], default_value: Manager::DEFAULT_PIDFILE,
|
26
|
+
desc: "Path to write daemonized Process ID"
|
27
|
+
|
28
|
+
switch %i[v verbose], negatable: false, desc: "Enable more output"
|
29
|
+
switch %i[quiet], negatable: false, desc: "Enable less output"
|
30
|
+
|
31
|
+
# Defines start command flags
|
32
|
+
def self.define_start_options(c)
|
33
|
+
c.flag %i[env environment], desc: "The Rails environment"
|
34
|
+
c.flag %i[q queue], default_value: Manager::DEFAULT_QUEUE, multiple: true,
|
35
|
+
desc: "Queue to work from"
|
36
|
+
c.flag %i[t threads], type: String, default_value: Manager::DEFAULT_THREADS,
|
37
|
+
desc: "The number of threads for executing jobs, specified as `min:max`"
|
38
|
+
c.flag %i[p poll_interval], type: Integer, default_value: DefaultOptionValue.new( value: 60),
|
39
|
+
desc: "Interval between polls for available jobs (in seconds)"
|
40
|
+
c.flag %i[cfg configfile], type: String, default_value: Manager::DEFAULT_CONFIG_FILES, multiple: true,
|
41
|
+
desc: "The YAML configuration file to load. If specifying multiple files, the last file takes precedence"
|
42
|
+
end
|
43
|
+
private_class_method :define_start_options
|
44
|
+
|
45
|
+
# Defines stop command flags
|
46
|
+
def self.define_stop_options(c)
|
47
|
+
c.flag %i[timeout shutdown_timeout], default_value: Manager::DEFAULT_FOREVER,
|
48
|
+
desc: "Number of seconds to wait for jobs to finish when shutting down before killing the worker. (in seconds)"
|
49
|
+
end
|
50
|
+
private_class_method :define_stop_options
|
51
|
+
|
52
|
+
desc "Starts a worker"
|
53
|
+
long_desc <<~TEXT
|
54
|
+
Starts a new worker to execute jobs from your ActiveJob queue. If the worker is daemonized this command will
|
55
|
+
return immediately.
|
56
|
+
TEXT
|
57
|
+
command :start do |c|
|
58
|
+
c.switch %i[d daemon daemonize], negatable: false,
|
59
|
+
desc: "Run as a background daemon (default: false)"
|
60
|
+
|
61
|
+
define_start_options(c)
|
62
|
+
|
63
|
+
c.action do |global_options, options|
|
64
|
+
Manager.new(global_options).start(options)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Stops a daemonized worker"
|
69
|
+
long_desc <<~TEXT
|
70
|
+
Stops a daemonized worker. This uses the PID file to send a shutdown command to a running worker. If the worker
|
71
|
+
does not exit within the shutdown timeout it will kill the process.
|
72
|
+
TEXT
|
73
|
+
command :stop do |c|
|
74
|
+
c.switch :all, negatable: false, desc: "Stops all workers with default pid files."
|
75
|
+
define_stop_options c
|
76
|
+
|
77
|
+
c.action do |global_options, options|
|
78
|
+
if options[:all]
|
79
|
+
puts "The identifier option is ignored for --all" unless global_options[:identifier].nil? || global_options[:quiet]
|
80
|
+
pidfile_pattern = if options[:pidfile].nil? || options[:pidfile] == Manager::DEFAULT_PIDFILE
|
81
|
+
"tmp/pids/exekutor*.pid"
|
82
|
+
else
|
83
|
+
options[:pidfile]
|
84
|
+
end
|
85
|
+
pidfiles = Dir[pidfile_pattern]
|
86
|
+
if pidfiles.any?
|
87
|
+
pidfiles.each do |pidfile|
|
88
|
+
Manager.new(global_options.merge(pidfile: pidfile)).stop(options)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
puts "There are no running workers (No pidfiles found for `#{pidfile_pattern}`)"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
Manager.new(global_options).stop(options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
desc "Restarts a daemonized worker"
|
100
|
+
long_desc <<~TEXT
|
101
|
+
Restarts a daemonized worker. Will issue the stop command if a worker is running and wait for the active worker
|
102
|
+
to exit before starting a new worker. If no worker is currently running, a new worker will be started.
|
103
|
+
TEXT
|
104
|
+
command :restart do |c|
|
105
|
+
define_stop_options c
|
106
|
+
define_start_options c
|
107
|
+
|
108
|
+
c.action do |global_options, options|
|
109
|
+
Manager.new(global_options).restart(options.slice(:shutdown_timeout),
|
110
|
+
options.reject { |k, _| k == :shutdown_timeout })
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
desc "Prints worker and job info"
|
115
|
+
long_desc <<~TEXT
|
116
|
+
Prints info about workers and pending jobs.
|
117
|
+
TEXT
|
118
|
+
command :info do |c|
|
119
|
+
c.flag %i[env environment], desc: "The Rails environment."
|
120
|
+
|
121
|
+
c.action do |global_options, options|
|
122
|
+
Info.new(global_options).print(options)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
desc "Cleans up workers and jobs"
|
127
|
+
long_desc <<~TEXT
|
128
|
+
Cleans up the finished jobs and stale workers
|
129
|
+
TEXT
|
130
|
+
command :cleanup do |c|
|
131
|
+
c.flag %i[env environment], desc: "The Rails environment."
|
132
|
+
|
133
|
+
c.flag %i[t timeout],
|
134
|
+
desc: "The global timeout in hours. Workers and jobs before the timeout will be purged"
|
135
|
+
c.flag %i[worker_timeout],
|
136
|
+
default_value: 4,
|
137
|
+
desc: "The worker timeout in hours. Workers where the last heartbeat is before the timeout will be deleted."
|
138
|
+
c.flag %i[job_timeout],
|
139
|
+
default_value: 48,
|
140
|
+
desc: "The job timeout in hours. Jobs where scheduled at is before the timeout will be purged."
|
141
|
+
c.flag %i[s job_status],
|
142
|
+
default_value: Cleanup::DEFAULT_STATUSES, multiple: true,
|
143
|
+
desc: "The statuses to purge. Only jobs with this status will be purged."
|
144
|
+
|
145
|
+
c.default_command :all
|
146
|
+
|
147
|
+
c.desc "Cleans up both the workers and the jobs"
|
148
|
+
c.command(:all) do |ac|
|
149
|
+
ac.action do |global_options, options|
|
150
|
+
Cleanup.new(global_options).tap do |cleaner|
|
151
|
+
cleaner.cleanup_workers(options.merge(print_header: true))
|
152
|
+
cleaner.cleanup_jobs(options.merge(print_header: true))
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
c.desc "Cleans up the workers table"
|
157
|
+
c.command(:workers, :w) do |wc|
|
158
|
+
wc.action do |global_options, options|
|
159
|
+
Cleanup.new(global_options).cleanup_workers(options)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
c.desc "Cleans up the jobs table"
|
163
|
+
c.command(:jobs, :j) do |jc|
|
164
|
+
jc.action do |global_options, options|
|
165
|
+
Cleanup.new(global_options).cleanup_jobs(options)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Exekutor
|
2
|
+
module Internal
|
3
|
+
module CLI
|
4
|
+
# Helper methods to load the Rails application
|
5
|
+
module ApplicationLoader
|
6
|
+
# The message to print when loading the Rails application
|
7
|
+
LOADING_MESSAGE = "Loading Rails environment…"
|
8
|
+
|
9
|
+
# Loads the Rails application
|
10
|
+
# @param environment [String] the environment to load (eg. development, production)
|
11
|
+
# @param path [String] the path to the environment file
|
12
|
+
# @param print_message [Boolean] whether to print a loading message to STDOUT
|
13
|
+
def load_application(environment, path = "config/environment.rb", print_message: false)
|
14
|
+
return if @application_loaded
|
15
|
+
if print_message
|
16
|
+
printf LOADING_MESSAGE
|
17
|
+
@loading_message_printed = true
|
18
|
+
end
|
19
|
+
ENV["RAILS_ENV"] = environment unless environment.nil?
|
20
|
+
require File.expand_path(path)
|
21
|
+
@application_loaded = true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Clears the loading message if it was printed
|
25
|
+
def clear_application_loading_message
|
26
|
+
if @loading_message_printed
|
27
|
+
printf "\r#{" " * LOADING_MESSAGE.length}\r"
|
28
|
+
@loading_message_printed = false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative "application_loader"
|
2
|
+
require_relative "default_option_value"
|
3
|
+
require "terminal-table"
|
4
|
+
|
5
|
+
module Exekutor
|
6
|
+
# @private
|
7
|
+
module Internal
|
8
|
+
module CLI
|
9
|
+
# Cleanup for the CLI
|
10
|
+
# @private
|
11
|
+
class Cleanup
|
12
|
+
include ApplicationLoader
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
@global_options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
# Cleans up the workers table
|
19
|
+
# @see Exekutor::Cleanup
|
20
|
+
def cleanup_workers(options)
|
21
|
+
load_application options[:environment], print_message: !quiet?
|
22
|
+
|
23
|
+
ActiveSupport.on_load(:active_record, yield: true) do
|
24
|
+
# Use system time zone
|
25
|
+
Time.zone = Time.new.zone
|
26
|
+
|
27
|
+
clear_application_loading_message unless quiet?
|
28
|
+
timeout = options[:timeout] || options[:worker_timeout] || 4
|
29
|
+
workers = cleaner.cleanup_workers timeout: timeout.hours
|
30
|
+
return if quiet?
|
31
|
+
|
32
|
+
puts Rainbow("Workers").bright.blue if options[:print_header]
|
33
|
+
if workers.present?
|
34
|
+
puts "Purged #{workers.size} worker#{"s" if workers.many?}"
|
35
|
+
if verbose?
|
36
|
+
table = Terminal::Table.new
|
37
|
+
table.headings = ["id", "Last heartbeat"]
|
38
|
+
workers.each { |w| table << [w.id.split("-").first << "…", w.last_heartbeat_at] }
|
39
|
+
puts table
|
40
|
+
end
|
41
|
+
else
|
42
|
+
puts "Nothing purged"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Cleans up the jobs table
|
48
|
+
# @see Exekutor::Cleanup
|
49
|
+
def cleanup_jobs(options)
|
50
|
+
load_application options[:environment], print_message: !quiet?
|
51
|
+
|
52
|
+
ActiveSupport.on_load(:active_record, yield: true) do
|
53
|
+
# Use system time zone
|
54
|
+
Time.zone = Time.new.zone
|
55
|
+
|
56
|
+
clear_application_loading_message unless quiet?
|
57
|
+
timeout = options[:timeout] || options[:job_timeout] || 48
|
58
|
+
status = if options[:job_status].is_a? Array
|
59
|
+
options[:job_status]
|
60
|
+
elsif options[:job_status] && options[:job_status] != DEFAULT_STATUSES
|
61
|
+
options[:job_status]
|
62
|
+
end
|
63
|
+
purged_count = cleaner.cleanup_jobs before: timeout.hours.ago, status: status
|
64
|
+
return if quiet?
|
65
|
+
|
66
|
+
puts Rainbow("Jobs").bright.blue if options[:print_header]
|
67
|
+
if purged_count.zero?
|
68
|
+
puts "Nothing purged"
|
69
|
+
else
|
70
|
+
puts "Purged #{purged_count} job#{"s" if purged_count > 1}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# @return [Boolean] Whether quiet mode is enabled. Overrides verbose mode.
|
78
|
+
def quiet?
|
79
|
+
!!@global_options[:quiet]
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Boolean] Whether verbose mode is enabled. Always returns false if quiet mode is enabled.
|
83
|
+
def verbose?
|
84
|
+
!quiet? && !!@global_options[:verbose]
|
85
|
+
end
|
86
|
+
|
87
|
+
def cleaner
|
88
|
+
@delegate ||= ::Exekutor::Cleanup.new
|
89
|
+
end
|
90
|
+
|
91
|
+
DEFAULT_STATUSES = DefaultOptionValue.new("All except :pending").freeze
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|