delayed 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
- 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
|