sidekiq-hierarchy 0.1.1
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/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +57 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +396 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/img/dashboard.png +0 -0
- data/img/failed_workflow.png +0 -0
- data/img/in_progress_workflow.png +0 -0
- data/img/job.png +0 -0
- data/img/workflow_set.png +0 -0
- data/lib/sidekiq-hierarchy.rb +1 -0
- data/lib/sidekiq/hierarchy.rb +105 -0
- data/lib/sidekiq/hierarchy/callback_registry.rb +33 -0
- data/lib/sidekiq/hierarchy/client/middleware.rb +23 -0
- data/lib/sidekiq/hierarchy/faraday/middleware.rb +16 -0
- data/lib/sidekiq/hierarchy/http.rb +8 -0
- data/lib/sidekiq/hierarchy/job.rb +290 -0
- data/lib/sidekiq/hierarchy/notifications.rb +8 -0
- data/lib/sidekiq/hierarchy/observers.rb +9 -0
- data/lib/sidekiq/hierarchy/observers/job_update.rb +15 -0
- data/lib/sidekiq/hierarchy/observers/workflow_update.rb +18 -0
- data/lib/sidekiq/hierarchy/rack/middleware.rb +27 -0
- data/lib/sidekiq/hierarchy/server/middleware.rb +62 -0
- data/lib/sidekiq/hierarchy/version.rb +5 -0
- data/lib/sidekiq/hierarchy/web.rb +149 -0
- data/lib/sidekiq/hierarchy/workflow.rb +130 -0
- data/lib/sidekiq/hierarchy/workflow_set.rb +134 -0
- data/sidekiq-hierarchy.gemspec +33 -0
- data/web/views/_job_progress_bar.erb +28 -0
- data/web/views/_job_table.erb +37 -0
- data/web/views/_job_timings.erb +10 -0
- data/web/views/_progress_bar.erb +8 -0
- data/web/views/_search_bar.erb +17 -0
- data/web/views/_summary_bar.erb +30 -0
- data/web/views/_workflow_progress_bar.erb +24 -0
- data/web/views/_workflow_set_clear.erb +7 -0
- data/web/views/_workflow_table.erb +33 -0
- data/web/views/_workflow_timings.erb +14 -0
- data/web/views/_workflow_tree.erb +82 -0
- data/web/views/_workflow_tree_node.erb +18 -0
- data/web/views/job.erb +12 -0
- data/web/views/not_found.erb +1 -0
- data/web/views/status.erb +120 -0
- data/web/views/workflow.erb +45 -0
- data/web/views/workflow_set.erb +3 -0
- metadata +225 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Hierarchy
|
3
|
+
module Observers
|
4
|
+
class JobUpdate
|
5
|
+
def register(callback_registry)
|
6
|
+
callback_registry.subscribe(Notifications::JOB_UPDATE, self)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(job, status, old_status)
|
10
|
+
job.workflow.update_status(status)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Hierarchy
|
3
|
+
module Observers
|
4
|
+
class WorkflowUpdate
|
5
|
+
def register(callback_registry)
|
6
|
+
callback_registry.subscribe(Notifications::WORKFLOW_UPDATE, self)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(workflow, status, old_status)
|
10
|
+
from_set = WorkflowSet.for_status(old_status)
|
11
|
+
to_set = WorkflowSet.for_status(status)
|
12
|
+
|
13
|
+
to_set.move(workflow, from_set) # Move/add to the new status set
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'sidekiq/hierarchy/http'
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Hierarchy
|
6
|
+
module Rack
|
7
|
+
class Middleware
|
8
|
+
# transform from http header to rack names
|
9
|
+
JID_HEADER_KEY = "HTTP_#{Sidekiq::Hierarchy::Http::JID_HEADER.upcase.gsub('-','_')}".freeze
|
10
|
+
WORKFLOW_HEADER_KEY = "HTTP_#{Sidekiq::Hierarchy::Http::WORKFLOW_HEADER.upcase.gsub('-','_')}".freeze
|
11
|
+
|
12
|
+
def initialize(app)
|
13
|
+
@app = app
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
Sidekiq::Hierarchy.current_jid = env[JID_HEADER_KEY]
|
18
|
+
Sidekiq::Hierarchy.current_workflow = Workflow.find_by_jid(env[WORKFLOW_HEADER_KEY]) if env[WORKFLOW_HEADER_KEY]
|
19
|
+
@app.call(env)
|
20
|
+
ensure
|
21
|
+
Sidekiq::Hierarchy.current_workflow = nil
|
22
|
+
Sidekiq::Hierarchy.current_jid = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Hierarchy
|
3
|
+
module Server
|
4
|
+
class Middleware
|
5
|
+
def initialize(options={})
|
6
|
+
end
|
7
|
+
|
8
|
+
# Wraps around the actual execution of a job. Takes params:
|
9
|
+
# worker - the instance of the worker to be used for execution
|
10
|
+
# msg - the hash of job info, something like {'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true}
|
11
|
+
# queue - the named queue to use
|
12
|
+
# Must propagate return value upwards.
|
13
|
+
# Since jobs raise errors for signalling, those must be propagated as well.
|
14
|
+
def call(worker, msg, queue)
|
15
|
+
if msg['workflow'] == true # root job -- start of a new workflow
|
16
|
+
Sidekiq::Hierarchy.current_workflow = Workflow.find_by_jid(worker.jid)
|
17
|
+
elsif msg['workflow'].is_a?(String) # child job -- inherit parent's workflow
|
18
|
+
Sidekiq::Hierarchy.current_workflow = Workflow.find_by_jid(msg['workflow'])
|
19
|
+
end
|
20
|
+
Sidekiq::Hierarchy.current_jid = worker.jid
|
21
|
+
|
22
|
+
Sidekiq::Hierarchy.record_job_running
|
23
|
+
ret = yield
|
24
|
+
Sidekiq::Hierarchy.record_job_complete
|
25
|
+
|
26
|
+
ret
|
27
|
+
rescue Exception => e
|
28
|
+
if exception_caused_by_shutdown?(e) || retries_remaining?(msg)
|
29
|
+
# job will be pushed back onto queue during hard_shutdown or if retries are permitted
|
30
|
+
Sidekiq::Hierarchy.record_job_requeued
|
31
|
+
else
|
32
|
+
Sidekiq::Hierarchy.record_job_failed
|
33
|
+
end
|
34
|
+
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
|
38
|
+
def retries_remaining?(msg)
|
39
|
+
return false unless msg['retry']
|
40
|
+
|
41
|
+
retry_count = msg['retry_count'] || 0
|
42
|
+
max_retries = if msg['retry'].is_a?(Fixnum)
|
43
|
+
msg['retry']
|
44
|
+
else
|
45
|
+
Sidekiq::Middleware::Server::RetryJobs::DEFAULT_MAX_RETRY_ATTEMPTS
|
46
|
+
end
|
47
|
+
|
48
|
+
# this check requires prepending the middleware before sidekiq's builtin retry
|
49
|
+
retry_count < max_retries
|
50
|
+
end
|
51
|
+
private :retries_remaining?
|
52
|
+
|
53
|
+
def exception_caused_by_shutdown?(e)
|
54
|
+
e.instance_of?(Sidekiq::Shutdown) ||
|
55
|
+
# In Ruby 2.1+, check if original exception was Shutdown
|
56
|
+
(defined?(e.cause) && exception_caused_by_shutdown?(e.cause))
|
57
|
+
end
|
58
|
+
private :exception_caused_by_shutdown?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# Web interface to Sidekiq-hierarchy
|
2
|
+
# Optimised for ease-of-use, not efficiency; it's probably best
|
3
|
+
# not to leave this open in a tab forever.
|
4
|
+
# Sidekiq seems to use Bootstrap 3.0.0 currently; find docs at
|
5
|
+
# http://bootstrapdocs.com/v3.0.0/docs/
|
6
|
+
module Sidekiq
|
7
|
+
module Hierarchy
|
8
|
+
module Web
|
9
|
+
module Helpers
|
10
|
+
def sidekiq_hierarchy_template(template_name)
|
11
|
+
File.read(File.join(VIEW_PATH, "#{template_name}.erb"))
|
12
|
+
end
|
13
|
+
|
14
|
+
def job_url(job=nil)
|
15
|
+
"#{root_path}hierarchy/jobs/#{job.jid if job}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def workflow_url(workflow=nil)
|
19
|
+
"#{root_path}hierarchy/workflows/#{workflow.jid if workflow}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def workflow_set_url(status)
|
23
|
+
"#{root_path}hierarchy/workflow_sets/#{status}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def status_updated_at(job)
|
27
|
+
case job.status
|
28
|
+
when :enqueued
|
29
|
+
job.enqueued_at
|
30
|
+
when :running, :requeued
|
31
|
+
job.run_at
|
32
|
+
when :complete
|
33
|
+
job.complete_at
|
34
|
+
when :failed
|
35
|
+
job.failed_at
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def bootstrap_status(status)
|
40
|
+
case status
|
41
|
+
when :enqueued, :requeued
|
42
|
+
'warning'
|
43
|
+
when :running
|
44
|
+
'info'
|
45
|
+
when :complete
|
46
|
+
'success'
|
47
|
+
when :failed
|
48
|
+
'danger'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
VIEW_PATH = File.expand_path('../../../../web/views', __FILE__)
|
54
|
+
PER_PAGE = 20
|
55
|
+
|
56
|
+
def self.registered(app)
|
57
|
+
app.helpers Helpers
|
58
|
+
|
59
|
+
app.not_found do
|
60
|
+
erb sidekiq_hierarchy_template(:not_found)
|
61
|
+
end
|
62
|
+
|
63
|
+
app.get '/hierarchy/?' do
|
64
|
+
running_set = RunningSet.new
|
65
|
+
complete_set = CompleteSet.new
|
66
|
+
failed_set = FailedSet.new
|
67
|
+
|
68
|
+
@running = running_set.each.take(PER_PAGE)
|
69
|
+
@complete = complete_set.each.take(PER_PAGE)
|
70
|
+
@failed = failed_set.each.take(PER_PAGE)
|
71
|
+
|
72
|
+
erb sidekiq_hierarchy_template(:status)
|
73
|
+
end
|
74
|
+
|
75
|
+
app.delete '/hierarchy/?' do
|
76
|
+
[RunningSet.new, CompleteSet.new, FailedSet.new].each(&:remove_all)
|
77
|
+
redirect back
|
78
|
+
end
|
79
|
+
|
80
|
+
app.get '/hierarchy/workflow_sets/:status' do |status|
|
81
|
+
@status = status.to_sym
|
82
|
+
if workflow_set = WorkflowSet.for_status(@status)
|
83
|
+
@workflows = workflow_set.each.take(PER_PAGE)
|
84
|
+
erb sidekiq_hierarchy_template(:workflow_set)
|
85
|
+
else
|
86
|
+
halt 404
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
app.delete '/hierarchy/workflow_sets/:status' do |status|
|
91
|
+
@status = status.to_sym
|
92
|
+
if workflow_set = WorkflowSet.for_status(@status)
|
93
|
+
workflow_set.each(&:delete)
|
94
|
+
redirect back
|
95
|
+
else
|
96
|
+
halt 404
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
app.get '/hierarchy/workflows/?' do
|
101
|
+
if params['workflow_jid'] =~ /\A\h{24}\z/
|
102
|
+
redirect to("/hierarchy/workflows/#{params['workflow_jid']}")
|
103
|
+
else
|
104
|
+
redirect to(:hierarchy)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
app.get %r{\A/hierarchy/workflows/(\h{24})\z} do |workflow_jid|
|
109
|
+
@workflow = Workflow.find_by_jid(workflow_jid)
|
110
|
+
if @workflow.exists?
|
111
|
+
erb sidekiq_hierarchy_template(:workflow)
|
112
|
+
else
|
113
|
+
halt 404
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
app.delete %r{\A/hierarchy/workflows/(\h{24})\z} do |workflow_jid|
|
118
|
+
workflow = Workflow.find_by_jid(workflow_jid)
|
119
|
+
redirect_url = "/hierarchy/workflow_sets/#{workflow.status}"
|
120
|
+
workflow.delete
|
121
|
+
|
122
|
+
redirect to(redirect_url)
|
123
|
+
end
|
124
|
+
|
125
|
+
app.get '/hierarchy/jobs/?' do
|
126
|
+
if params['jid'] =~ /\A\h{24}\z/
|
127
|
+
redirect to("/hierarchy/jobs/#{params['jid']}")
|
128
|
+
else
|
129
|
+
redirect back
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
app.get %r{\A/hierarchy/jobs/(\h{24})\z} do |jid|
|
134
|
+
@job = Job.find(jid)
|
135
|
+
@workflow = @job.workflow
|
136
|
+
if @job.exists? && @workflow.exists?
|
137
|
+
erb sidekiq_hierarchy_template(:job)
|
138
|
+
else
|
139
|
+
halt 404
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
require 'sidekiq/web' unless defined?(Sidekiq::Web)
|
148
|
+
Sidekiq::Web.register(Sidekiq::Hierarchy::Web)
|
149
|
+
Sidekiq::Web.tabs['Hierarchy'] = 'hierarchy'
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Hierarchy
|
3
|
+
class Workflow
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :root
|
7
|
+
|
8
|
+
def initialize(root)
|
9
|
+
@root = root
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
alias_method :find, :new
|
14
|
+
|
15
|
+
def find_by_jid(root_jid)
|
16
|
+
find(Job.find(root_jid))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
delegate [:jid, :[], :[]=, :exists?] => :@root
|
21
|
+
|
22
|
+
def ==(other_workflow)
|
23
|
+
other_workflow.instance_of?(self.class) &&
|
24
|
+
self.jid == other_workflow.jid
|
25
|
+
end
|
26
|
+
|
27
|
+
def workflow_set
|
28
|
+
WorkflowSet.for_status(status)
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete
|
32
|
+
wset = workflow_set # save it for later
|
33
|
+
root.delete # deleting nodes is more important than a dangling reference
|
34
|
+
wset.remove(self) if wset # now we can clear out from the set
|
35
|
+
end
|
36
|
+
|
37
|
+
# Walks the tree in DFS order (for optimal completion checking)
|
38
|
+
# Returns an Enumerator; use #to_a to get an array instead
|
39
|
+
def jobs
|
40
|
+
to_visit = [root]
|
41
|
+
Enumerator.new do |y|
|
42
|
+
while node = to_visit.pop
|
43
|
+
y << node # sugar for yielding a value
|
44
|
+
to_visit += node.children
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
### Status
|
51
|
+
|
52
|
+
def status
|
53
|
+
case self[Job::WORKFLOW_STATUS_FIELD]
|
54
|
+
when Job::STATUS_RUNNING
|
55
|
+
:running
|
56
|
+
when Job::STATUS_COMPLETE
|
57
|
+
:complete
|
58
|
+
when Job::STATUS_FAILED
|
59
|
+
:failed
|
60
|
+
else
|
61
|
+
:unknown
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_status(from_job_status)
|
66
|
+
old_status = status
|
67
|
+
return if [:failed, :complete].include?(old_status) # these states are final
|
68
|
+
|
69
|
+
if [:enqueued, :running, :requeued].include?(from_job_status)
|
70
|
+
new_status, s_val = :running, Job::STATUS_RUNNING
|
71
|
+
elsif from_job_status == :failed
|
72
|
+
new_status, s_val = :failed, Job::STATUS_FAILED
|
73
|
+
elsif from_job_status == :complete && jobs.all?(&:complete?)
|
74
|
+
new_status, s_val = :complete, Job::STATUS_COMPLETE
|
75
|
+
end
|
76
|
+
|
77
|
+
return if !new_status || new_status == old_status # don't publish null updates
|
78
|
+
self[Job::WORKFLOW_STATUS_FIELD] = s_val
|
79
|
+
|
80
|
+
Sidekiq::Hierarchy.publish(Notifications::WORKFLOW_UPDATE, self, new_status, old_status)
|
81
|
+
end
|
82
|
+
|
83
|
+
def running?
|
84
|
+
status == :running
|
85
|
+
end
|
86
|
+
|
87
|
+
def complete?
|
88
|
+
status == :complete
|
89
|
+
end
|
90
|
+
|
91
|
+
def failed?
|
92
|
+
status == :failed
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
### Calculated metrics
|
97
|
+
|
98
|
+
def enqueued_at
|
99
|
+
root.enqueued_at
|
100
|
+
end
|
101
|
+
|
102
|
+
def run_at
|
103
|
+
root.run_at
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the time at which all jobs were complete;
|
107
|
+
# nil if any jobs are still incomplete
|
108
|
+
def complete_at
|
109
|
+
jobs.map(&:complete_at).max if complete?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the earliest time at which a job failed;
|
113
|
+
# nil if none did
|
114
|
+
def failed_at
|
115
|
+
jobs.map(&:failed_at).compact.min if failed?
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
### Serialisation
|
120
|
+
|
121
|
+
def as_json(options={})
|
122
|
+
root.as_json(options)
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_s
|
126
|
+
Sidekiq.dump_json(self.as_json)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Hierarchy
|
3
|
+
|
4
|
+
### Implementations
|
5
|
+
|
6
|
+
# A sorted set of Workflows that permits enumeration
|
7
|
+
class WorkflowSet
|
8
|
+
PAGE_SIZE = 100
|
9
|
+
|
10
|
+
def self.for_status(status)
|
11
|
+
case status
|
12
|
+
when :running
|
13
|
+
RunningSet.new
|
14
|
+
when :complete
|
15
|
+
CompleteSet.new
|
16
|
+
when :failed
|
17
|
+
FailedSet.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(status)
|
22
|
+
raise ArgumentError, 'status cannot be nil' if status.nil?
|
23
|
+
@status = status
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other_workflow_set)
|
27
|
+
other_workflow_set.instance_of?(self.class)
|
28
|
+
end
|
29
|
+
|
30
|
+
def size
|
31
|
+
Sidekiq.redis { |conn| conn.zcard(redis_zkey) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def add(workflow)
|
35
|
+
Sidekiq.redis { |conn| conn.zadd(redis_zkey, Time.now.to_f, workflow.jid) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def contains?(workflow)
|
39
|
+
!!Sidekiq.redis { |conn| conn.zscore(redis_zkey, workflow.jid) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Remove a workflow from the set if it is present. This operation can
|
43
|
+
# only be executed as cleanup (i.e., on a workflow that has been
|
44
|
+
# unpersisted/deleted); otherwise it will fail in order to avoid
|
45
|
+
# memory leaks.
|
46
|
+
def remove(workflow)
|
47
|
+
raise 'Workflow still exists' if workflow.exists?
|
48
|
+
Sidekiq.redis { |conn| conn.zrem(redis_zkey, workflow.jid) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Move a workflow to this set from its current one
|
52
|
+
# This should really be done in Lua, but unit testing support is just not there,
|
53
|
+
# so there is a potential race condition in which a workflow could end up in
|
54
|
+
# multiple sets. the effect of this is minimal, so we'll fix it later.
|
55
|
+
def move(workflow, from_set=nil)
|
56
|
+
Sidekiq.redis do |conn|
|
57
|
+
conn.multi do
|
58
|
+
conn.zrem(from_set.redis_zkey, workflow.jid) if from_set
|
59
|
+
conn.zadd(redis_zkey, Time.now.to_f, workflow.jid)
|
60
|
+
end.last
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def each
|
65
|
+
return enum_for(:each) unless block_given?
|
66
|
+
|
67
|
+
elements = []
|
68
|
+
last_max_score = Time.now.to_f
|
69
|
+
loop do
|
70
|
+
elements = Sidekiq.redis do |conn|
|
71
|
+
conn.zrevrangebyscore(redis_zkey, last_max_score, '-inf', limit: [0, PAGE_SIZE], with_scores: true)
|
72
|
+
.drop_while { |elt| elements.include?(elt) }
|
73
|
+
end
|
74
|
+
break if elements.empty?
|
75
|
+
elements.each { |jid, _| yield Workflow.find_by_jid(jid) }
|
76
|
+
_, last_max_score = elements.last # timestamp of last element
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def redis_zkey
|
81
|
+
"hierarchy:set:#{@status}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# An implementation of WorkflowSet that auto-prunes by time & size
|
86
|
+
# to stay within space constraints. Do _not_ use for workflows that
|
87
|
+
# cannot be lost (i.e., are in any state of progress, or require followup)
|
88
|
+
class PruningSet < WorkflowSet
|
89
|
+
def self.timeout
|
90
|
+
Sidekiq.options[:dead_timeout_in_seconds]
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.max_workflows
|
94
|
+
Sidekiq.options[:dead_max_workflows] || Sidekiq.options[:dead_max_jobs]
|
95
|
+
end
|
96
|
+
|
97
|
+
def add(workflow)
|
98
|
+
prune
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def prune
|
103
|
+
Sidekiq.redis do |conn|
|
104
|
+
conn.multi do
|
105
|
+
conn.zrangebyscore(redis_zkey, '-inf', Time.now.to_f - self.class.timeout) # old workflows
|
106
|
+
conn.zrevrange(redis_zkey, self.class.max_workflows, -1) # excess workflows
|
107
|
+
end.flatten.uniq # take the union of both pruning strategies
|
108
|
+
.tap { |to_remove| conn.zrem(redis_zkey, to_remove) if to_remove.any? }
|
109
|
+
end.each { |jid| Workflow.find_by_jid(jid).delete }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
### Instances
|
115
|
+
|
116
|
+
class RunningSet < WorkflowSet
|
117
|
+
def initialize
|
118
|
+
super 'running'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class CompleteSet < PruningSet
|
123
|
+
def initialize
|
124
|
+
super 'complete'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class FailedSet < PruningSet
|
129
|
+
def initialize
|
130
|
+
super 'failed'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|