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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/exe/exekutor +7 -0
  5. data/lib/active_job/queue_adapters/exekutor_adapter.rb +14 -0
  6. data/lib/exekutor/asynchronous.rb +188 -0
  7. data/lib/exekutor/cleanup.rb +56 -0
  8. data/lib/exekutor/configuration.rb +373 -0
  9. data/lib/exekutor/hook.rb +172 -0
  10. data/lib/exekutor/info/worker.rb +20 -0
  11. data/lib/exekutor/internal/base_record.rb +11 -0
  12. data/lib/exekutor/internal/callbacks.rb +138 -0
  13. data/lib/exekutor/internal/cli/app.rb +173 -0
  14. data/lib/exekutor/internal/cli/application_loader.rb +36 -0
  15. data/lib/exekutor/internal/cli/cleanup.rb +96 -0
  16. data/lib/exekutor/internal/cli/daemon.rb +108 -0
  17. data/lib/exekutor/internal/cli/default_option_value.rb +29 -0
  18. data/lib/exekutor/internal/cli/info.rb +126 -0
  19. data/lib/exekutor/internal/cli/manager.rb +260 -0
  20. data/lib/exekutor/internal/configuration_builder.rb +113 -0
  21. data/lib/exekutor/internal/database_connection.rb +21 -0
  22. data/lib/exekutor/internal/executable.rb +75 -0
  23. data/lib/exekutor/internal/executor.rb +242 -0
  24. data/lib/exekutor/internal/hooks.rb +87 -0
  25. data/lib/exekutor/internal/listener.rb +176 -0
  26. data/lib/exekutor/internal/logger.rb +74 -0
  27. data/lib/exekutor/internal/provider.rb +308 -0
  28. data/lib/exekutor/internal/reserver.rb +95 -0
  29. data/lib/exekutor/internal/status_server.rb +132 -0
  30. data/lib/exekutor/job.rb +31 -0
  31. data/lib/exekutor/job_error.rb +11 -0
  32. data/lib/exekutor/job_options.rb +95 -0
  33. data/lib/exekutor/plugins/appsignal.rb +46 -0
  34. data/lib/exekutor/plugins.rb +13 -0
  35. data/lib/exekutor/queue.rb +141 -0
  36. data/lib/exekutor/version.rb +6 -0
  37. data/lib/exekutor/worker.rb +219 -0
  38. data/lib/exekutor.rb +49 -0
  39. data/lib/generators/exekutor/configuration_generator.rb +18 -0
  40. data/lib/generators/exekutor/install_generator.rb +43 -0
  41. data/lib/generators/exekutor/templates/install/functions/job_notifier.sql +7 -0
  42. data/lib/generators/exekutor/templates/install/functions/requeue_orphaned_jobs.sql +7 -0
  43. data/lib/generators/exekutor/templates/install/initializers/exekutor.rb.erb +14 -0
  44. data/lib/generators/exekutor/templates/install/migrations/create_exekutor_schema.rb.erb +83 -0
  45. data/lib/generators/exekutor/templates/install/triggers/notify_workers.sql +6 -0
  46. data/lib/generators/exekutor/templates/install/triggers/requeue_orphaned_jobs.sql +5 -0
  47. data.tar.gz.sig +0 -0
  48. metadata +403 -0
  49. 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