exekutor 0.1.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.
- 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
|