exekutor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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