delayed 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +560 -0
- data/Rakefile +35 -0
- data/lib/delayed.rb +72 -0
- data/lib/delayed/active_job_adapter.rb +65 -0
- data/lib/delayed/backend/base.rb +166 -0
- data/lib/delayed/backend/job_preparer.rb +43 -0
- data/lib/delayed/exceptions.rb +14 -0
- data/lib/delayed/job.rb +250 -0
- data/lib/delayed/lifecycle.rb +85 -0
- data/lib/delayed/message_sending.rb +65 -0
- data/lib/delayed/monitor.rb +134 -0
- data/lib/delayed/performable_mailer.rb +22 -0
- data/lib/delayed/performable_method.rb +47 -0
- data/lib/delayed/plugin.rb +15 -0
- data/lib/delayed/plugins/connection.rb +13 -0
- data/lib/delayed/plugins/instrumentation.rb +39 -0
- data/lib/delayed/priority.rb +164 -0
- data/lib/delayed/psych_ext.rb +135 -0
- data/lib/delayed/railtie.rb +7 -0
- data/lib/delayed/runnable.rb +46 -0
- data/lib/delayed/serialization/active_record.rb +18 -0
- data/lib/delayed/syck_ext.rb +42 -0
- data/lib/delayed/tasks.rb +40 -0
- data/lib/delayed/worker.rb +233 -0
- data/lib/delayed/yaml_ext.rb +10 -0
- data/lib/delayed_job.rb +1 -0
- data/lib/delayed_job_active_record.rb +1 -0
- data/lib/generators/delayed/generator.rb +7 -0
- data/lib/generators/delayed/migration_generator.rb +28 -0
- data/lib/generators/delayed/next_migration_version.rb +14 -0
- data/lib/generators/delayed/templates/migration.rb +22 -0
- data/spec/autoloaded/clazz.rb +6 -0
- data/spec/autoloaded/instance_clazz.rb +5 -0
- data/spec/autoloaded/instance_struct.rb +6 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/database.yml +25 -0
- data/spec/delayed/active_job_adapter_spec.rb +267 -0
- data/spec/delayed/job_spec.rb +953 -0
- data/spec/delayed/monitor_spec.rb +276 -0
- data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
- data/spec/delayed/priority_spec.rb +154 -0
- data/spec/delayed/serialization/active_record_spec.rb +15 -0
- data/spec/delayed/tasks_spec.rb +116 -0
- data/spec/helper.rb +196 -0
- data/spec/lifecycle_spec.rb +77 -0
- data/spec/message_sending_spec.rb +149 -0
- data/spec/performable_mailer_spec.rb +68 -0
- data/spec/performable_method_spec.rb +123 -0
- data/spec/psych_ext_spec.rb +94 -0
- data/spec/sample_jobs.rb +117 -0
- data/spec/worker_spec.rb +235 -0
- data/spec/yaml_ext_spec.rb +48 -0
- metadata +326 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
module Delayed
|
2
|
+
class InvalidCallback < RuntimeError; end
|
3
|
+
|
4
|
+
class Lifecycle
|
5
|
+
EVENTS = {
|
6
|
+
execute: [nil],
|
7
|
+
enqueue: [:job],
|
8
|
+
perform: %i(worker job),
|
9
|
+
error: %i(worker job),
|
10
|
+
failure: %i(worker job),
|
11
|
+
thread: %i(worker job),
|
12
|
+
invoke_job: [:job],
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@callbacks = EVENTS.keys.each_with_object({}) do |e, hash|
|
17
|
+
hash[e] = Callback.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def before(event, &block)
|
22
|
+
add(:before, event, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def after(event, &block)
|
26
|
+
add(:after, event, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def around(event, &block)
|
30
|
+
add(:around, event, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_callbacks(event, *args, &block)
|
34
|
+
missing_callback(event) unless @callbacks.key?(event)
|
35
|
+
|
36
|
+
unless EVENTS[event].size == args.size
|
37
|
+
raise ArgumentError, "Callback #{event} expects #{EVENTS[event].size} parameter(s): #{EVENTS[event].join(', ')}"
|
38
|
+
end
|
39
|
+
|
40
|
+
@callbacks[event].execute(*args, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def add(type, event, &block)
|
46
|
+
missing_callback(event) unless @callbacks.key?(event)
|
47
|
+
@callbacks[event].add(type, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def missing_callback(event)
|
51
|
+
raise InvalidCallback, "Unknown callback event: #{event}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Callback
|
56
|
+
def initialize
|
57
|
+
@before = []
|
58
|
+
@after = []
|
59
|
+
|
60
|
+
# Identity proc. Avoids special cases when there is no existing around chain.
|
61
|
+
@around = lambda { |*args, &block| block.call(*args) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def execute(*args, &block)
|
65
|
+
@before.each { |c| c.call(*args) }
|
66
|
+
result = @around.call(*args, &block)
|
67
|
+
@after.each { |c| c.call(*args) }
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def add(type, &callback)
|
72
|
+
case type
|
73
|
+
when :before
|
74
|
+
@before << callback
|
75
|
+
when :after
|
76
|
+
@after << callback
|
77
|
+
when :around
|
78
|
+
chain = @around # use a local variable so that the current chain is closed over in the following lambda
|
79
|
+
@around = lambda { |*a, &block| chain.call(*a) { |*b| callback.call(*b, &block) } }
|
80
|
+
else
|
81
|
+
raise InvalidCallback, "Invalid callback type: #{type}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'active_support/proxy_object'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
class DelayProxy < ActiveSupport::ProxyObject
|
5
|
+
def initialize(payload_class, target, options)
|
6
|
+
@payload_class = payload_class
|
7
|
+
@target = target
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(method, *args)
|
12
|
+
Job.enqueue({ payload_object: @payload_class.new(@target, method.to_sym, args) }.merge(@options))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module MessageSending
|
17
|
+
def delay(options = {})
|
18
|
+
DelayProxy.new(PerformableMethod, self, options)
|
19
|
+
end
|
20
|
+
alias __delay__ delay
|
21
|
+
|
22
|
+
def send_later(method, *args)
|
23
|
+
warn '[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method'
|
24
|
+
__delay__.__send__(method, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_at(time, method, *args)
|
28
|
+
warn '[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method'
|
29
|
+
__delay__(run_at: time).__send__(method, *args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module MessageSendingClassMethods
|
34
|
+
def handle_asynchronously(method, opts = {}) # rubocop:disable Metrics/PerceivedComplexity
|
35
|
+
aliased_method = method.to_s.sub(/([?!=])$/, '')
|
36
|
+
punctuation = $1 # rubocop:disable Style/PerlBackrefs
|
37
|
+
with_method = "#{aliased_method}_with_delay#{punctuation}"
|
38
|
+
without_method = "#{aliased_method}_without_delay#{punctuation}"
|
39
|
+
define_method(with_method) do |*args|
|
40
|
+
curr_opts = opts.clone
|
41
|
+
curr_opts.each_key do |key|
|
42
|
+
next unless (val = curr_opts[key]).is_a?(Proc)
|
43
|
+
|
44
|
+
curr_opts[key] = if val.arity == 1
|
45
|
+
val.call(self)
|
46
|
+
else
|
47
|
+
val.call
|
48
|
+
end
|
49
|
+
end
|
50
|
+
delay(curr_opts).__send__(without_method, *args)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method without_method, method
|
54
|
+
alias_method method, with_method
|
55
|
+
|
56
|
+
if public_method_defined?(without_method)
|
57
|
+
public method
|
58
|
+
elsif protected_method_defined?(without_method)
|
59
|
+
protected method
|
60
|
+
elsif private_method_defined?(without_method)
|
61
|
+
private method
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Monitor
|
3
|
+
include Runnable
|
4
|
+
|
5
|
+
METRICS = %w(
|
6
|
+
count
|
7
|
+
future_count
|
8
|
+
locked_count
|
9
|
+
erroring_count
|
10
|
+
failed_count
|
11
|
+
max_lock_age
|
12
|
+
max_age
|
13
|
+
working_count
|
14
|
+
workable_count
|
15
|
+
alert_age_percent
|
16
|
+
).freeze
|
17
|
+
|
18
|
+
cattr_accessor :sleep_delay, instance_writer: false, default: 60
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@jobs = Job.group(priority_case_statement).group(:queue)
|
22
|
+
@jobs = @jobs.where(queue: Worker.queues) if Worker.queues.any?
|
23
|
+
end
|
24
|
+
|
25
|
+
def run!
|
26
|
+
ActiveSupport::Notifications.instrument('delayed.monitor.run', default_tags) do
|
27
|
+
METRICS.each { |metric| emit_metric!(metric) }
|
28
|
+
end
|
29
|
+
interruptable_sleep(sleep_delay)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :jobs
|
35
|
+
|
36
|
+
def emit_metric!(metric)
|
37
|
+
send("#{metric}_grouped").reverse_merge(default_results).each do |(priority, queue), value|
|
38
|
+
ActiveSupport::Notifications.instrument(
|
39
|
+
"delayed.job.#{metric}",
|
40
|
+
default_tags.merge(priority: Priority.new(priority).to_s, queue: queue, value: value),
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_results
|
46
|
+
@default_results ||= Priority.names.values.flat_map { |priority|
|
47
|
+
(Worker.queues.presence || [Worker.default_queue_name]).map do |queue|
|
48
|
+
[[priority.to_i, queue], 0]
|
49
|
+
end
|
50
|
+
}.to_h
|
51
|
+
end
|
52
|
+
|
53
|
+
def say(message)
|
54
|
+
Delayed.say(message)
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_tags
|
58
|
+
@default_tags ||= {
|
59
|
+
table: Job.table_name,
|
60
|
+
database: Job.database_name,
|
61
|
+
database_adapter: Job.database_adapter_name,
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def count_grouped
|
66
|
+
jobs.count
|
67
|
+
end
|
68
|
+
|
69
|
+
def future_count_grouped
|
70
|
+
jobs.where("run_at > ?", Job.db_time_now).count
|
71
|
+
end
|
72
|
+
|
73
|
+
def locked_count_grouped
|
74
|
+
jobs.locked.count
|
75
|
+
end
|
76
|
+
|
77
|
+
def erroring_count_grouped
|
78
|
+
jobs.erroring.count
|
79
|
+
end
|
80
|
+
|
81
|
+
def failed_count_grouped
|
82
|
+
jobs.failed.count
|
83
|
+
end
|
84
|
+
|
85
|
+
def max_lock_age_grouped
|
86
|
+
oldest_locked_job_grouped.each_with_object({}) do |job, metrics|
|
87
|
+
metrics[[job.priority.to_i, job.queue]] = Job.db_time_now - job.locked_at
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def max_age_grouped
|
92
|
+
oldest_workable_job_grouped.each_with_object({}) do |job, metrics|
|
93
|
+
metrics[[job.priority.to_i, job.queue]] = Job.db_time_now - job.run_at
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def alert_age_percent_grouped
|
98
|
+
oldest_workable_job_grouped.each_with_object({}) do |job, metrics|
|
99
|
+
max_age = Job.db_time_now - job.run_at
|
100
|
+
metrics[[job.priority.to_i, job.queue]] = [max_age / job.priority.alert_age * 100, 100].min if job.priority.alert_age
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def workable_count_grouped
|
105
|
+
jobs.workable(Job.db_time_now).count
|
106
|
+
end
|
107
|
+
|
108
|
+
def working_count_grouped
|
109
|
+
jobs.working.count
|
110
|
+
end
|
111
|
+
|
112
|
+
def oldest_locked_job_grouped
|
113
|
+
jobs.working.select("#{priority_case_statement} AS priority, queue, MIN(locked_at) AS locked_at")
|
114
|
+
end
|
115
|
+
|
116
|
+
def oldest_workable_job_grouped
|
117
|
+
jobs.workable(Job.db_time_now).select("(#{priority_case_statement}) AS priority, queue, MIN(run_at) AS run_at")
|
118
|
+
end
|
119
|
+
|
120
|
+
def priority_case_statement
|
121
|
+
[
|
122
|
+
'CASE',
|
123
|
+
Priority.ranges.values.map do |range|
|
124
|
+
[
|
125
|
+
"WHEN priority >= #{range.first.to_i}",
|
126
|
+
("AND priority < #{range.last.to_i}" unless range.last.infinite?),
|
127
|
+
"THEN #{range.first.to_i}",
|
128
|
+
].compact
|
129
|
+
end,
|
130
|
+
'END',
|
131
|
+
].flatten.join(' ')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
class PerformableMailer < PerformableMethod
|
5
|
+
def perform
|
6
|
+
mailer = object.send(method_name, *args)
|
7
|
+
mailer.respond_to?(:deliver_now) ? mailer.deliver_now : mailer.deliver
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module DelayMail
|
12
|
+
def delay(options = {})
|
13
|
+
DelayProxy.new(PerformableMailer, self, options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Mail::Message.class_eval do
|
19
|
+
def delay(*_args)
|
20
|
+
raise 'Use MyMailer.delay.mailer_action(args) to delay sending of emails.'
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Delayed
|
2
|
+
class PerformableMethod
|
3
|
+
attr_accessor :object, :method_name, :args
|
4
|
+
|
5
|
+
def initialize(object, method_name, args)
|
6
|
+
raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
|
7
|
+
|
8
|
+
if !her_model?(object) && object.respond_to?(:persisted?) && !object.persisted?
|
9
|
+
raise(ArgumentError, "job cannot be created for non-persisted record: #{object.inspect}")
|
10
|
+
end
|
11
|
+
|
12
|
+
self.object = object
|
13
|
+
self.args = args
|
14
|
+
self.method_name = method_name.to_sym
|
15
|
+
end
|
16
|
+
|
17
|
+
def display_name
|
18
|
+
if object.is_a?(Class)
|
19
|
+
"#{object}.#{method_name}"
|
20
|
+
else
|
21
|
+
"#{object.class}##{method_name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform
|
26
|
+
object.send(method_name, *args) if object
|
27
|
+
end
|
28
|
+
|
29
|
+
def method(sym)
|
30
|
+
object.method(sym)
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(symbol, *args)
|
34
|
+
object.send(symbol, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to?(symbol, include_private = false)
|
38
|
+
super || object.respond_to?(symbol, include_private)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def her_model?(object)
|
44
|
+
object.class.respond_to?(:save_existing)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
class Plugin
|
5
|
+
class_attribute :callback_block
|
6
|
+
|
7
|
+
def self.callbacks(&block)
|
8
|
+
self.callback_block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.class.callback_block.call(Delayed.lifecycle) if self.class.callback_block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Plugins
|
3
|
+
class Instrumentation < Plugin
|
4
|
+
callbacks do |lifecycle|
|
5
|
+
lifecycle.around(:enqueue) do |job, *args, &block|
|
6
|
+
ActiveSupport::Notifications.instrument('delayed.job.enqueue', active_support_notifications_tags(job)) do
|
7
|
+
block.call(job, *args)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
lifecycle.around(:invoke_job) do |job, *args, &block|
|
12
|
+
ActiveSupport::Notifications.instrument('delayed.job.run', active_support_notifications_tags(job)) do
|
13
|
+
block.call(job, *args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
lifecycle.after(:error) do |_worker, job, *_args|
|
18
|
+
ActiveSupport::Notifications.instrument('delayed.job.error', active_support_notifications_tags(job))
|
19
|
+
end
|
20
|
+
|
21
|
+
lifecycle.after(:failure) do |_worker, job, *_args|
|
22
|
+
ActiveSupport::Notifications.instrument('delayed.job.failure', active_support_notifications_tags(job))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.active_support_notifications_tags(job)
|
27
|
+
{
|
28
|
+
job_name: job.name,
|
29
|
+
priority: job.priority,
|
30
|
+
queue: job.queue,
|
31
|
+
table: job.class.table_name,
|
32
|
+
database: job.class.database_name,
|
33
|
+
database_adapter: job.class.database_adapter_name,
|
34
|
+
job: job,
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Priority < Numeric
|
3
|
+
# A Delayed::Priority represents a value that exists within a named range.
|
4
|
+
# Here are the default ranges and their names:
|
5
|
+
#
|
6
|
+
# 0-9: interactive
|
7
|
+
# 10-19: user_visible
|
8
|
+
# 20-29: eventual
|
9
|
+
# 30+: reporting
|
10
|
+
#
|
11
|
+
# Ranges can be customized. They must be positive and must include a name for priority >= 0.
|
12
|
+
# The following config will produce ranges 0-99 (high), 100-499 (medium) and 500+ (low):
|
13
|
+
#
|
14
|
+
# > Delayed::Priority.names = { high: 0, medium: 100, low: 500 }
|
15
|
+
|
16
|
+
DEFAULT_NAMES = {
|
17
|
+
interactive: 0, # These jobs will actively hinder end-user interactions until they are complete, e.g. work behind a loading spinner
|
18
|
+
user_visible: 10, # These jobs have end-user-visible side effects that will not obviously impact customers, e.g. welcome emails
|
19
|
+
eventual: 20, # These jobs affect business process that are tolerant to some degree of queue backlog, e.g. syncing with other services
|
20
|
+
reporting: 30, # These jobs are for processes that can complete on a slower timeline, e.g. daily report generation
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
# Priorities can be mapped to alerting thresholds for job age (time since run_at), runtime, and attempts.
|
24
|
+
# These thresholds can be used to emit events or metrics. Here are the default values (for the default priorities):
|
25
|
+
#
|
26
|
+
# === Age Alerts ==========
|
27
|
+
# interactive: 1 minute
|
28
|
+
# user_visible: 3 minutes
|
29
|
+
# eventual: 1.5 hours
|
30
|
+
# reporting: 4 hours
|
31
|
+
#
|
32
|
+
# === Run Time Alerts ======
|
33
|
+
# interactive: 30 seconds
|
34
|
+
# user_visible: 90 seconds
|
35
|
+
# eventual: 5 minutes
|
36
|
+
# reporting: 10 minutes
|
37
|
+
#
|
38
|
+
# === Attempts Alerts =====
|
39
|
+
# interactive: 3 attempts
|
40
|
+
# user_visible: 5 attempts
|
41
|
+
# eventual: 8 attempts
|
42
|
+
# reporting: 8 attempts
|
43
|
+
#
|
44
|
+
# Alerting thresholds can be customized. The keys must match `Delayed::Priority.names`.
|
45
|
+
#
|
46
|
+
# Delayed::Priority.alerts = {
|
47
|
+
# high: { age: 30.seconds, run_time: 15.seconds, attempts: 3 },
|
48
|
+
# medium: { age: 2.minutes, run_time: 1.minute, attempts: 6 },
|
49
|
+
# low: { age: 10.minutes, run_time: 2.minutes, attempts: 9 },
|
50
|
+
# }
|
51
|
+
|
52
|
+
DEFAULT_ALERTS = {
|
53
|
+
interactive: { age: 1.minute, run_time: 30.seconds, attempts: 3 },
|
54
|
+
user_visible: { age: 3.minutes, run_time: 90.seconds, attempts: 5 },
|
55
|
+
eventual: { age: 1.5.hours, run_time: 5.minutes, attempts: 8 },
|
56
|
+
reporting: { age: 4.hours, run_time: 10.minutes, attempts: 8 },
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def names
|
61
|
+
@names || default_names
|
62
|
+
end
|
63
|
+
|
64
|
+
def alerts
|
65
|
+
@alerts || default_alerts
|
66
|
+
end
|
67
|
+
|
68
|
+
def names=(names)
|
69
|
+
raise "must include a name for priority >= 0" if names && !names.value?(0)
|
70
|
+
|
71
|
+
@ranges = nil
|
72
|
+
@alerts = nil
|
73
|
+
@names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def alerts=(alerts)
|
77
|
+
if alerts
|
78
|
+
unknown_names = alerts.keys - names.keys
|
79
|
+
raise "unknown priority name(s): #{unknown_names}" if unknown_names.any?
|
80
|
+
end
|
81
|
+
|
82
|
+
@alerts = alerts&.sort_by { |k, _| names.keys.index(k) }&.to_h
|
83
|
+
end
|
84
|
+
|
85
|
+
def ranges
|
86
|
+
@ranges ||= names.zip(names.except(names.keys.first)).each_with_object({}) do |((name, lower), (_, upper)), obj|
|
87
|
+
obj[name] = (lower...(upper || Float::INFINITY))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def default_names
|
94
|
+
@default_names ||= DEFAULT_NAMES.transform_values { |v| new(v) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def default_alerts
|
98
|
+
@names ? {} : DEFAULT_ALERTS
|
99
|
+
end
|
100
|
+
|
101
|
+
def respond_to_missing?(method_name, include_private = false)
|
102
|
+
names.key?(method_name) || super
|
103
|
+
end
|
104
|
+
|
105
|
+
def method_missing(method_name, *args)
|
106
|
+
if names.key?(method_name) && args.none?
|
107
|
+
names[method_name]
|
108
|
+
else
|
109
|
+
super
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
attr_reader :value
|
115
|
+
|
116
|
+
delegate :to_i, to: :value
|
117
|
+
delegate :to_s, to: :name
|
118
|
+
|
119
|
+
def initialize(value)
|
120
|
+
super()
|
121
|
+
value = self.class.names[value] if value.is_a?(Symbol)
|
122
|
+
@value = value.to_i
|
123
|
+
end
|
124
|
+
|
125
|
+
def name
|
126
|
+
@name ||= self.class.ranges.find { |(_, r)| r.include?(to_i) }&.first
|
127
|
+
end
|
128
|
+
|
129
|
+
def alert_age
|
130
|
+
self.class.alerts.dig(name, :age)
|
131
|
+
end
|
132
|
+
|
133
|
+
def alert_run_time
|
134
|
+
self.class.alerts.dig(name, :run_time)
|
135
|
+
end
|
136
|
+
|
137
|
+
def alert_attempts
|
138
|
+
self.class.alerts.dig(name, :attempts)
|
139
|
+
end
|
140
|
+
|
141
|
+
def coerce(other)
|
142
|
+
[self.class.new(other), self]
|
143
|
+
end
|
144
|
+
|
145
|
+
def <=>(other)
|
146
|
+
other = other.to_i if other.is_a?(self.class)
|
147
|
+
to_i <=> other
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def respond_to_missing?(method_name, include_private = false)
|
153
|
+
method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym) || super
|
154
|
+
end
|
155
|
+
|
156
|
+
def method_missing(method_name, *args)
|
157
|
+
if method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)
|
158
|
+
method_name.to_s[0..-2] == to_s
|
159
|
+
else
|
160
|
+
super
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|