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