sidekiq-tasks 1.1.0 → 1.2.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 +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +1 -1
- data/README.md +101 -5
- data/docs/custom_storage.md +44 -11
- data/lib/sidekiq/tasks/config.rb +28 -9
- data/lib/sidekiq/tasks/job.rb +3 -2
- data/lib/sidekiq/tasks/magic_comments/comment.rb +28 -0
- data/lib/sidekiq/tasks/magic_comments/comments.rb +31 -0
- data/lib/sidekiq/tasks/magic_comments/handlers/base.rb +26 -0
- data/lib/sidekiq/tasks/magic_comments/handlers/disable.rb +17 -0
- data/lib/sidekiq/tasks/magic_comments/handlers/dry_run.rb +17 -0
- data/lib/sidekiq/tasks/magic_comments/handlers/enable.rb +17 -0
- data/lib/sidekiq/tasks/magic_comments/handlers/sidekiq_options.rb +32 -0
- data/lib/sidekiq/tasks/magic_comments/registration.rb +11 -0
- data/lib/sidekiq/tasks/magic_comments/registry.rb +19 -0
- data/lib/sidekiq/tasks/magic_comments/scanner.rb +83 -0
- data/lib/sidekiq/tasks/magic_comments.rb +20 -0
- data/lib/sidekiq/tasks/run.rb +161 -0
- data/lib/sidekiq/tasks/sidekiq_options_validator.rb +25 -0
- data/lib/sidekiq/tasks/storage/base.rb +22 -0
- data/lib/sidekiq/tasks/storage/redis.rb +47 -26
- data/lib/sidekiq/tasks/strategies/base.rb +5 -2
- data/lib/sidekiq/tasks/strategies/rake_task.rb +26 -2
- data/lib/sidekiq/tasks/strategies/rules/disable_with_comment.rb +2 -2
- data/lib/sidekiq/tasks/strategies/rules/enable_with_comment.rb +8 -56
- data/lib/sidekiq/tasks/task.rb +24 -7
- data/lib/sidekiq/tasks/task_metadata.rb +18 -2
- data/lib/sidekiq/tasks/version.rb +1 -1
- data/lib/sidekiq/tasks/web/extension.rb +2 -1
- data/lib/sidekiq/tasks/web/helpers/tag_helper.rb +1 -1
- data/lib/sidekiq/tasks/web/helpers/task_helper.rb +19 -0
- data/lib/sidekiq/tasks.rb +18 -1
- data/web/assets/tasks/css/components/alerts.css +25 -0
- data/web/assets/tasks/css/components/report.css +70 -0
- data/web/assets/tasks/css/ext.css +2 -0
- data/web/locales/en.yml +10 -0
- data/web/locales/fr.yml +10 -0
- data/web/views/task.erb +87 -3
- metadata +37 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
module Tasks
|
|
3
|
+
module MagicComments
|
|
4
|
+
autoload :Comment, "sidekiq/tasks/magic_comments/comment"
|
|
5
|
+
autoload :Comments, "sidekiq/tasks/magic_comments/comments"
|
|
6
|
+
autoload :Registry, "sidekiq/tasks/magic_comments/registry"
|
|
7
|
+
autoload :Scanner, "sidekiq/tasks/magic_comments/scanner"
|
|
8
|
+
|
|
9
|
+
module Handlers
|
|
10
|
+
autoload :Base, "sidekiq/tasks/magic_comments/handlers/base"
|
|
11
|
+
autoload :Enable, "sidekiq/tasks/magic_comments/handlers/enable"
|
|
12
|
+
autoload :Disable, "sidekiq/tasks/magic_comments/handlers/disable"
|
|
13
|
+
autoload :DryRun, "sidekiq/tasks/magic_comments/handlers/dry_run"
|
|
14
|
+
autoload :SidekiqOptions, "sidekiq/tasks/magic_comments/handlers/sidekiq_options"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_relative "magic_comments/registration"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
module Tasks
|
|
3
|
+
# The context of a task execution, available from the task code through `Sidekiq::Tasks.current_run`.
|
|
4
|
+
class Run
|
|
5
|
+
THREAD_KEY = :sidekiq_tasks_run
|
|
6
|
+
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
# Outside of a Sidekiq::Tasks job (e.g. `rake` in a terminal), dry run is disabled
|
|
10
|
+
# and the report is printed on exit.
|
|
11
|
+
def self.current
|
|
12
|
+
Thread.current[THREAD_KEY] || standalone
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.with(run)
|
|
16
|
+
previous_run = Thread.current[THREAD_KEY]
|
|
17
|
+
Thread.current[THREAD_KEY] = run
|
|
18
|
+
yield
|
|
19
|
+
ensure
|
|
20
|
+
Thread.current[THREAD_KEY] = previous_run
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.standalone
|
|
24
|
+
@_standalone ||= new.tap do |run|
|
|
25
|
+
at_exit { puts JSON.pretty_generate(run.report.to_h) unless run.report.empty? }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
private_class_method :standalone
|
|
29
|
+
|
|
30
|
+
def initialize(dry_run: false, report_max_bytes: Sidekiq::Tasks.config.report_max_bytes)
|
|
31
|
+
@dry_run = dry_run
|
|
32
|
+
@report = Report.new(max_bytes: report_max_bytes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dry_run?
|
|
36
|
+
@dry_run
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A structured execution report, stored in the task history and displayed in the web interface.
|
|
41
|
+
#
|
|
42
|
+
# The report size is bounded by `max_bytes`, see Sidekiq::Tasks::Storage::Base#report_max_bytes:
|
|
43
|
+
# - the serialized report never exceeds `max_bytes`, the last items of the longest lists are dropped
|
|
44
|
+
# and the counts remain exact,
|
|
45
|
+
# - while the task runs, items stop being kept beyond MEMORY_FACTOR times that size, protecting worker memory,
|
|
46
|
+
# - strings are truncated to MAX_STRING_LENGTH, so that a single long message does not fill the whole report,
|
|
47
|
+
# - at most MAX_KEYS values, lists and item keys, guarding against keys built from the data.
|
|
48
|
+
class Report
|
|
49
|
+
MAX_KEYS = 50
|
|
50
|
+
MAX_STRING_LENGTH = 255
|
|
51
|
+
MEMORY_FACTOR = 4
|
|
52
|
+
|
|
53
|
+
def initialize(max_bytes: Sidekiq::Tasks.config.report_max_bytes)
|
|
54
|
+
@max_bytes = max_bytes
|
|
55
|
+
@kept_bytes = 0
|
|
56
|
+
@values = {}
|
|
57
|
+
@lists = {}
|
|
58
|
+
@truncated = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def []=(key, value)
|
|
62
|
+
key = sanitize_key(key)
|
|
63
|
+
@values[key] = sanitize_scalar(value) if accept_key?(@values, key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def increment(key, by = 1)
|
|
67
|
+
key = sanitize_key(key)
|
|
68
|
+
@values[key] = sanitize_scalar((@values[key] || 0) + by) if accept_key?(@values, key)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Adds an item (a scalar or a flat Hash) to a list.
|
|
72
|
+
def add(key, item)
|
|
73
|
+
key = sanitize_key(key)
|
|
74
|
+
return unless accept_key?(@lists, key)
|
|
75
|
+
|
|
76
|
+
list = @lists[key] ||= {"items" => [], "count" => 0}
|
|
77
|
+
list["count"] += 1
|
|
78
|
+
keep_item(list, sanitize_item(item))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def empty?
|
|
82
|
+
@values.empty? && @lists.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Hash] The report, serializable as JSON and at most `max_bytes` long.
|
|
86
|
+
def to_h
|
|
87
|
+
lists = @lists.transform_values { |list| list.merge("items" => list["items"].dup) }
|
|
88
|
+
report = {"values" => @values, "lists" => lists}
|
|
89
|
+
report["truncated"] = true if @truncated
|
|
90
|
+
return report if Sidekiq.dump_json(report).bytesize <= @max_bytes
|
|
91
|
+
|
|
92
|
+
fit(report.merge("truncated" => true))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Falls back to an empty report when values alone exceed max_bytes.
|
|
98
|
+
def fit(report)
|
|
99
|
+
drop_items(report, Sidekiq.dump_json(report).bytesize)
|
|
100
|
+
return report if Sidekiq.dump_json(report).bytesize <= @max_bytes
|
|
101
|
+
|
|
102
|
+
{"values" => {}, "lists" => {}, "truncated" => true}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Sizes are tracked incrementally so that the report is not serialized again for each dropped item.
|
|
106
|
+
def drop_items(report, size)
|
|
107
|
+
lists = report["lists"].values
|
|
108
|
+
|
|
109
|
+
while size > @max_bytes
|
|
110
|
+
longest_list = lists.max_by { |list| list["items"].size }
|
|
111
|
+
break if longest_list.nil? || longest_list["items"].empty?
|
|
112
|
+
|
|
113
|
+
size -= Sidekiq.dump_json(longest_list["items"].pop).bytesize + 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Items are kept with a margin over max_bytes so that to_h can still balance the lists,
|
|
118
|
+
# but never beyond it, so that the worker memory stays bounded.
|
|
119
|
+
def keep_item(list, item)
|
|
120
|
+
if @kept_bytes > @max_bytes * MEMORY_FACTOR
|
|
121
|
+
@truncated = true
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@kept_bytes += Sidekiq.dump_json(item).bytesize
|
|
126
|
+
list["items"] << item
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def accept_key?(hash, key)
|
|
130
|
+
return true if hash.key?(key) || hash.size < MAX_KEYS
|
|
131
|
+
|
|
132
|
+
@truncated = true
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def sanitize_key(key)
|
|
137
|
+
truncate(key.to_s)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def sanitize_item(item)
|
|
141
|
+
return sanitize_scalar(item) unless item.is_a?(Hash)
|
|
142
|
+
|
|
143
|
+
item.first(MAX_KEYS).to_h { |key, value| [sanitize_key(key), sanitize_scalar(value)] }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sanitize_scalar(value)
|
|
147
|
+
case value
|
|
148
|
+
when Float then value.finite? ? value : value.to_s
|
|
149
|
+
when Numeric, TrueClass, FalseClass, NilClass then value
|
|
150
|
+
else truncate(value.to_s)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def truncate(string)
|
|
155
|
+
return string if string.length <= MAX_STRING_LENGTH
|
|
156
|
+
|
|
157
|
+
"#{string[0...(MAX_STRING_LENGTH - 3)]}..."
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
module Tasks
|
|
3
|
+
module SidekiqOptionsValidator
|
|
4
|
+
KEYS = {
|
|
5
|
+
queue: [String],
|
|
6
|
+
retry: [NilClass, TrueClass, FalseClass, Integer],
|
|
7
|
+
retry_for: [NilClass, Integer, Float],
|
|
8
|
+
dead: [NilClass, TrueClass, FalseClass],
|
|
9
|
+
backtrace: [NilClass, TrueClass, FalseClass, Integer],
|
|
10
|
+
pool: [NilClass, String],
|
|
11
|
+
tags: [NilClass, Array],
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.validate!(options, allow_partial: false)
|
|
15
|
+
Sidekiq::Tasks::Validations.validate_class!(options, [Hash], "sidekiq_options")
|
|
16
|
+
|
|
17
|
+
KEYS.each do |key, classes|
|
|
18
|
+
next if allow_partial && !options.key?(key)
|
|
19
|
+
|
|
20
|
+
Sidekiq::Tasks::Validations.validate_class!(options[key], classes, key)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -60,6 +60,28 @@ module Sidekiq
|
|
|
60
60
|
raise NotImplementedError, "Storage must implement #store_execution_error"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Marks a history entry as a dry run.
|
|
64
|
+
# Optional: storages that don't implement it simply don't display the dry run badge.
|
|
65
|
+
#
|
|
66
|
+
# @param jid [String] The Sidekiq job ID.
|
|
67
|
+
def store_dry_run(_jid)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Stores the execution report for a specific history entry.
|
|
71
|
+
# Optional: storages that don't implement it simply don't display reports.
|
|
72
|
+
#
|
|
73
|
+
# @param jid [String] The Sidekiq job ID.
|
|
74
|
+
# @param report [Hash] The report, see Sidekiq::Tasks::Report#to_h.
|
|
75
|
+
def store_execution_report(_jid, _report)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The maximum size of a stored report, lowered by storages with their own memory constraints.
|
|
79
|
+
#
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def report_max_bytes
|
|
82
|
+
Sidekiq::Tasks.config.report_max_bytes
|
|
83
|
+
end
|
|
84
|
+
|
|
63
85
|
protected
|
|
64
86
|
|
|
65
87
|
def truncate_message(message, max_length)
|
|
@@ -3,6 +3,10 @@ module Sidekiq
|
|
|
3
3
|
module Storage
|
|
4
4
|
class Redis < Base
|
|
5
5
|
JID_PREFIX = "task".freeze
|
|
6
|
+
|
|
7
|
+
# Maximum size of the reports stored in the history of a task, whatever the history_limit.
|
|
8
|
+
TASK_REPORTS_BUDGET = 10 * 1024 * 1024
|
|
9
|
+
|
|
6
10
|
def jid_key
|
|
7
11
|
"#{JID_PREFIX}:#{task_name}"
|
|
8
12
|
end
|
|
@@ -11,22 +15,20 @@ module Sidekiq
|
|
|
11
15
|
"#{jid_key}:history"
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
def execution_key(jid)
|
|
19
|
+
"#{jid_key}:execution:#{jid}"
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def last_enqueue_at
|
|
15
23
|
stored_time("last_enqueue_at")
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
def history
|
|
19
|
-
|
|
27
|
+
entries = Sidekiq.redis { |conn| conn.lrange(history_key, 0, -1) }.map { |raw| Sidekiq.load_json(raw) }
|
|
20
28
|
|
|
21
|
-
return []
|
|
29
|
+
return [] if entries.empty?
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
entry = Sidekiq.load_json(raw)
|
|
25
|
-
["enqueued_at", "executed_at", "finished_at"].each do |key|
|
|
26
|
-
entry[key] = Time.at(entry[key]) if entry[key]
|
|
27
|
-
end
|
|
28
|
-
entry
|
|
29
|
-
end
|
|
31
|
+
entries.zip(executions(entries)).map { |entry, execution| parse_entry(entry.merge(execution)) }
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def store_history(jid, task_args, time, user: nil)
|
|
@@ -34,6 +36,7 @@ module Sidekiq
|
|
|
34
36
|
task_trace = {jid: jid, task_name: task_name, args: task_args, enqueued_at: time.to_f}
|
|
35
37
|
task_trace[:user] = user if user
|
|
36
38
|
conn.lpush(history_key, Sidekiq.dump_json(task_trace))
|
|
39
|
+
delete_executions(conn, conn.lrange(history_key, history_limit, -1))
|
|
37
40
|
conn.ltrim(history_key, 0, history_limit - 1)
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -45,16 +48,24 @@ module Sidekiq
|
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def store_execution(jid, time_key)
|
|
48
|
-
|
|
49
|
-
entry.merge(time_key => Time.now.to_f)
|
|
50
|
-
end
|
|
51
|
+
Sidekiq.redis { |conn| conn.hset(execution_key(jid), time_key, Time.now.to_f) }
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def store_execution_error(jid, error)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
error_message = truncate_message("#{error.class}: #{error.message}", ERROR_MESSAGE_MAX_LENGTH)
|
|
56
|
+
Sidekiq.redis { |conn| conn.hset(execution_key(jid), "error", error_message) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def store_dry_run(jid)
|
|
60
|
+
Sidekiq.redis { |conn| conn.hset(execution_key(jid), "dry_run", Sidekiq.dump_json(true)) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def store_execution_report(jid, report)
|
|
64
|
+
Sidekiq.redis { |conn| conn.hset(execution_key(jid), "report", Sidekiq.dump_json(report)) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def report_max_bytes
|
|
68
|
+
[super, TASK_REPORTS_BUDGET / history_limit].min
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
private
|
|
@@ -69,20 +80,30 @@ module Sidekiq
|
|
|
69
80
|
[nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_f)
|
|
70
81
|
end
|
|
71
82
|
|
|
72
|
-
def
|
|
83
|
+
def executions(entries)
|
|
73
84
|
Sidekiq.redis do |conn|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
entries.each_with_index do |raw, index|
|
|
77
|
-
entry = Sidekiq.load_json(raw)
|
|
78
|
-
next unless entry["jid"] == jid
|
|
79
|
-
|
|
80
|
-
updated_entry = yield(entry)
|
|
81
|
-
conn.lset(history_key, index, Sidekiq.dump_json(updated_entry))
|
|
82
|
-
break
|
|
85
|
+
conn.pipelined do |pipeline|
|
|
86
|
+
entries.each { |entry| pipeline.hgetall(execution_key(entry["jid"])) }
|
|
83
87
|
end
|
|
84
88
|
end
|
|
85
89
|
end
|
|
90
|
+
|
|
91
|
+
def parse_entry(entry)
|
|
92
|
+
["enqueued_at", "executed_at", "finished_at"].each do |key|
|
|
93
|
+
entry[key] = Time.at(entry[key].to_f) if entry[key]
|
|
94
|
+
end
|
|
95
|
+
["dry_run", "report"].each do |key|
|
|
96
|
+
entry[key] = Sidekiq.load_json(entry[key]) if entry[key]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
entry
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def delete_executions(conn, raw_entries)
|
|
103
|
+
return if raw_entries.empty?
|
|
104
|
+
|
|
105
|
+
conn.del(*raw_entries.map { |raw| execution_key(Sidekiq.load_json(raw)["jid"]) })
|
|
106
|
+
end
|
|
86
107
|
end
|
|
87
108
|
end
|
|
88
109
|
end
|
|
@@ -51,9 +51,12 @@ module Sidekiq
|
|
|
51
51
|
#
|
|
52
52
|
# @param name [String] The name of the task to enqueue.
|
|
53
53
|
# @param params [Hash] Parameters to pass to the task.
|
|
54
|
+
# @param sidekiq_options [Hash] Per-task Sidekiq options that override class-level defaults.
|
|
55
|
+
# @param dry_run [Boolean] Whether the task must be executed in dry run mode.
|
|
54
56
|
# @return [String] The JID of the sidekiq job that will execute the task.
|
|
55
|
-
def enqueue_task(name, params = {})
|
|
56
|
-
Sidekiq::Tasks::Job.
|
|
57
|
+
def enqueue_task(name, params = {}, sidekiq_options: {}, dry_run: false)
|
|
58
|
+
job = sidekiq_options.empty? ? Sidekiq::Tasks::Job : Sidekiq::Tasks::Job.set(sidekiq_options)
|
|
59
|
+
job.perform_async(name, params.to_json, dry_run)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
# Returns all the tasks that should be executed.
|
|
@@ -2,6 +2,11 @@ module Sidekiq
|
|
|
2
2
|
module Tasks
|
|
3
3
|
module Strategies
|
|
4
4
|
class RakeTask < Base
|
|
5
|
+
def initialize(rules: [], scanner: Sidekiq::Tasks::MagicComments::Scanner.new)
|
|
6
|
+
super(rules: rules)
|
|
7
|
+
@scanner = scanner
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def load_tasks
|
|
6
11
|
Rake::TaskManager.record_task_metadata = true
|
|
7
12
|
Rake.application.load_rakefile
|
|
@@ -12,14 +17,33 @@ module Sidekiq
|
|
|
12
17
|
Sidekiq::Tasks::TaskMetadata.new(
|
|
13
18
|
name: task.name,
|
|
14
19
|
desc: task.full_comment,
|
|
15
|
-
file: task
|
|
16
|
-
args: task.arg_names
|
|
20
|
+
file: task_file(task),
|
|
21
|
+
args: task.arg_names,
|
|
22
|
+
**magic_comment_options(task)
|
|
17
23
|
)
|
|
24
|
+
rescue Sidekiq::Tasks::ArgumentError => e
|
|
25
|
+
Sidekiq::Tasks::TaskMetadata.new(name: task.name, file: task_file(task), error: e.message)
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
def execute_task(name, args = nil)
|
|
21
29
|
Rake::Task[name].execute(args)
|
|
22
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def task_file(task)
|
|
35
|
+
task.locations.first&.split(":")&.first
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def magic_comment_options(task)
|
|
39
|
+
comments = @scanner.scan(task)
|
|
40
|
+
handlers = Sidekiq::Tasks::MagicComments::Handlers
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
sidekiq_options: comments.fetch(handlers::SidekiqOptions.name_token, default: {}),
|
|
44
|
+
dry_run: comments.any?(handlers::DryRun.name_token),
|
|
45
|
+
}
|
|
46
|
+
end
|
|
23
47
|
end
|
|
24
48
|
end
|
|
25
49
|
end
|
|
@@ -3,67 +3,19 @@ module Sidekiq
|
|
|
3
3
|
module Strategies
|
|
4
4
|
module Rules
|
|
5
5
|
class EnableWithComment < Base
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
lines = read_file_lines(file)
|
|
11
|
-
|
|
12
|
-
return false if lines.nil?
|
|
13
|
-
|
|
14
|
-
return true if task_has_magic_comment?(lines, line_number)
|
|
15
|
-
return true if desc_has_magic_comment?(task.full_comment, lines, line_number)
|
|
16
|
-
|
|
17
|
-
namespace_line_index = find_namespace_line_index(lines, task)
|
|
18
|
-
return false unless namespace_line_index
|
|
19
|
-
|
|
20
|
-
namespace_has_magic_comment?(lines, namespace_line_index)
|
|
6
|
+
def initialize(scanner: Sidekiq::Tasks::MagicComments::Scanner.new)
|
|
7
|
+
super()
|
|
8
|
+
@scanner = scanner
|
|
21
9
|
end
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def magic_comment_regex
|
|
26
|
-
/sidekiq-tasks:enable/
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def read_file_lines(file)
|
|
32
|
-
File.read(file).split("\n")
|
|
33
|
-
rescue Errno::ENOENT
|
|
34
|
-
raise ArgumentError, "File '#{file}' not found"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def desc_has_magic_comment?(desc, lines, task_line)
|
|
38
|
-
return false unless desc
|
|
39
|
-
|
|
40
|
-
desc_line_index = lines[0...task_line].rindex { |line| line.strip.match?(/^desc\b/) }
|
|
41
|
-
|
|
42
|
-
return false unless desc_line_index
|
|
43
|
-
|
|
44
|
-
comment_line = lines[desc_line_index - 1]&.strip
|
|
45
|
-
!!(comment_line =~ magic_comment_regex)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def task_has_magic_comment?(lines, task_line)
|
|
49
|
-
context_range = (task_line - 3..task_line).to_a.select { |i| i >= 0 }
|
|
50
|
-
context_range.reverse.any? do |i|
|
|
51
|
-
lines[i]&.strip&.match?(magic_comment_regex)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def find_namespace_line_index(lines, task)
|
|
56
|
-
namespace = namespace_name(task)
|
|
57
|
-
lines.find_index { |line| line.strip.match?(/^namespace\s+:#{Regexp.escape(namespace)}/) }
|
|
11
|
+
def respected?(task)
|
|
12
|
+
@scanner.scan(task).any?(magic_comment_token)
|
|
58
13
|
end
|
|
59
14
|
|
|
60
|
-
|
|
61
|
-
comment_line = lines[namespace_line_index - 1]&.strip
|
|
62
|
-
comment_line&.match?(magic_comment_regex)
|
|
63
|
-
end
|
|
15
|
+
protected
|
|
64
16
|
|
|
65
|
-
def
|
|
66
|
-
|
|
17
|
+
def magic_comment_token
|
|
18
|
+
Sidekiq::Tasks::MagicComments::Handlers::Enable.name_token
|
|
67
19
|
end
|
|
68
20
|
end
|
|
69
21
|
end
|
data/lib/sidekiq/tasks/task.rb
CHANGED
|
@@ -6,8 +6,9 @@ module Sidekiq
|
|
|
6
6
|
extend Forwardable
|
|
7
7
|
include Sidekiq::Tasks::Validations
|
|
8
8
|
|
|
9
|
-
def_delegators :metadata, :name, :desc, :file, :args
|
|
9
|
+
def_delegators :metadata, :name, :desc, :file, :args, :sidekiq_options, :error, :error?
|
|
10
10
|
def_delegators :storage, :last_enqueue_at, :history
|
|
11
|
+
def_delegator :metadata, :dry_run?, :dry_run_supported?
|
|
11
12
|
|
|
12
13
|
attr_reader :metadata, :strategy
|
|
13
14
|
|
|
@@ -22,29 +23,45 @@ module Sidekiq
|
|
|
22
23
|
validate_class!(strategy, [Sidekiq::Tasks::Strategies::Base], "strategy")
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
def enqueue(params = {}, user: nil)
|
|
26
|
-
|
|
26
|
+
def enqueue(params = {}, user: nil, dry_run: false)
|
|
27
|
+
raise Sidekiq::Tasks::ArgumentError, "cannot enqueue broken task '#{name}': #{error}" if error?
|
|
28
|
+
|
|
29
|
+
enqueue_options = {}
|
|
30
|
+
enqueue_options[:sidekiq_options] = sidekiq_options if sidekiq_options.any?
|
|
31
|
+
enqueue_options[:dry_run] = true if dry_run
|
|
32
|
+
|
|
33
|
+
jid = strategy.enqueue_task(name, params, **enqueue_options)
|
|
27
34
|
|
|
28
35
|
storage.store_enqueue(jid, params, user: user)
|
|
36
|
+
storage.store_dry_run(jid) if dry_run
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
def execute(params = {}, jid: nil)
|
|
39
|
+
def execute(params = {}, jid: nil, dry_run: false)
|
|
40
|
+
run = Sidekiq::Tasks::Run.new(dry_run: dry_run, report_max_bytes: storage.report_max_bytes)
|
|
32
41
|
storage.store_execution(jid, "executed_at")
|
|
33
42
|
|
|
34
43
|
begin
|
|
35
|
-
strategy.execute_task(name, params)
|
|
44
|
+
Sidekiq::Tasks::Run.with(run) { strategy.execute_task(name, params) }
|
|
36
45
|
rescue StandardError, SystemExit => e
|
|
37
|
-
|
|
46
|
+
finish_execution(jid, run)
|
|
38
47
|
storage.store_execution_error(jid, e)
|
|
39
48
|
raise
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
finish_execution(jid, run)
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
def storage
|
|
46
55
|
@_storage ||= Sidekiq::Tasks.config.storage.new(name, history_limit: Sidekiq::Tasks.config.history_limit)
|
|
47
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# The report is stored before finished_at so that it is displayed as soon as the execution appears finished.
|
|
61
|
+
def finish_execution(jid, run)
|
|
62
|
+
storage.store_execution_report(jid, run.report.to_h) unless run.report.empty?
|
|
63
|
+
storage.store_execution(jid, "finished_at")
|
|
64
|
+
end
|
|
48
65
|
end
|
|
49
66
|
end
|
|
50
67
|
end
|
|
@@ -3,24 +3,40 @@ module Sidekiq
|
|
|
3
3
|
class TaskMetadata
|
|
4
4
|
include Sidekiq::Tasks::Validations
|
|
5
5
|
|
|
6
|
-
attr_reader :name, :desc, :file, :args
|
|
6
|
+
attr_reader :name, :desc, :file, :args, :sidekiq_options, :error, :dry_run
|
|
7
7
|
|
|
8
|
-
def initialize(name:, file:, desc: "", args: [])
|
|
8
|
+
def initialize(name:, file:, desc: "", args: [], sidekiq_options: {}, error: nil, dry_run: false)
|
|
9
9
|
@name = name
|
|
10
10
|
@file = file
|
|
11
11
|
@desc = desc
|
|
12
12
|
@args = args
|
|
13
|
+
@sidekiq_options = sidekiq_options
|
|
14
|
+
@error = error
|
|
15
|
+
@dry_run = dry_run
|
|
13
16
|
|
|
14
17
|
validate_params!
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
def error?
|
|
21
|
+
!@error.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dry_run?
|
|
25
|
+
@dry_run
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
private
|
|
18
29
|
|
|
19
30
|
def validate_params!
|
|
20
31
|
validate_class!(name, [String, Symbol], "name")
|
|
21
32
|
validate_class!(file, [String, NilClass], "file")
|
|
33
|
+
validate_class!(error, [String, NilClass], "error")
|
|
34
|
+
return if error?
|
|
35
|
+
|
|
22
36
|
validate_class!(desc, [String, NilClass], "desc")
|
|
23
37
|
validate_array_classes!(args, [String, Symbol], "args")
|
|
38
|
+
validate_class!(sidekiq_options, [Hash], "sidekiq_options")
|
|
39
|
+
validate_class!(dry_run, [TrueClass, FalseClass], "dry_run")
|
|
24
40
|
end
|
|
25
41
|
end
|
|
26
42
|
end
|
|
@@ -62,7 +62,8 @@ module Sidekiq
|
|
|
62
62
|
args = Sidekiq::Tasks::Web::Params.new(task, fetch_param("args")).permit!
|
|
63
63
|
|
|
64
64
|
current_user = Sidekiq::Tasks.config.current_user&.call(env)
|
|
65
|
-
task.
|
|
65
|
+
dry_run = task.dry_run_supported? && fetch_param("dry_run") == "1"
|
|
66
|
+
task.enqueue(args, user: current_user, dry_run: dry_run)
|
|
66
67
|
|
|
67
68
|
redirect(task_url(root_path, task))
|
|
68
69
|
rescue Sidekiq::Tasks::ArgumentError => e
|
|
@@ -6,7 +6,7 @@ module Sidekiq
|
|
|
6
6
|
extend self
|
|
7
7
|
|
|
8
8
|
def build_tag(tag, content = nil, **attributes, &block)
|
|
9
|
-
attr_string = attributes.map { |key, value| "#{key}=\"#{value}\"" }.join(" ")
|
|
9
|
+
attr_string = attributes.map { |key, value| "#{key}=\"#{ERB::Util.html_escape(value)}\"" }.join(" ")
|
|
10
10
|
attr_string = " #{attr_string}" unless attr_string.empty?
|
|
11
11
|
|
|
12
12
|
"<#{tag}#{attr_string}>#{block&.call || content}</#{tag}>"
|
|
@@ -36,6 +36,13 @@ module Sidekiq
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Returns the table columns for a report list: the keys of Hash items, or nil for scalar items.
|
|
40
|
+
def report_list_columns(items)
|
|
41
|
+
return nil unless items.any? && items.all?(Hash)
|
|
42
|
+
|
|
43
|
+
items.flat_map(&:keys).uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
def format_task_duration(start_time, end_time)
|
|
40
47
|
return "-" unless start_time && end_time
|
|
41
48
|
|
|
@@ -48,6 +55,18 @@ module Sidekiq
|
|
|
48
55
|
"#{[duration_in_milliseconds, 1].max}ms"
|
|
49
56
|
end
|
|
50
57
|
end
|
|
58
|
+
|
|
59
|
+
def format_task_sidekiq_options(task)
|
|
60
|
+
task.sidekiq_options.map { |key, value| "#{key}: #{format_sidekiq_option_value(value)}" }.join(", ")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def format_sidekiq_option_value(value)
|
|
64
|
+
case value
|
|
65
|
+
when Array then "[#{value.join(", ")}]"
|
|
66
|
+
when nil then "-"
|
|
67
|
+
else value.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
51
70
|
end
|
|
52
71
|
end
|
|
53
72
|
end
|