resque-job_history 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +28 -0
- data/lib/resque-job_history.rb +7 -0
- data/lib/resque/job_history_server.rb +189 -0
- data/lib/resque/plugins/job_history.rb +63 -0
- data/lib/resque/plugins/job_history/cleaner.rb +92 -0
- data/lib/resque/plugins/job_history/history_base.rb +61 -0
- data/lib/resque/plugins/job_history/history_list.rb +88 -0
- data/lib/resque/plugins/job_history/job.rb +144 -0
- data/lib/resque/plugins/job_history/job_list.rb +90 -0
- data/lib/resque/plugins/version.rb +7 -0
- data/lib/resque/server/public/job_history.css +29 -0
- data/lib/resque/server/views/_job_class_pagination.erb +67 -0
- data/lib/resque/server/views/_job_pagination.erb +71 -0
- data/lib/resque/server/views/_jobs_list.erb +55 -0
- data/lib/resque/server/views/job_class_details.erb +98 -0
- data/lib/resque/server/views/job_details.erb +74 -0
- data/lib/resque/server/views/job_history.erb +114 -0
- data/lib/tasks/resque-job_history_tasks.rake +4 -0
- metadata +148 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module JobHistory
|
4
|
+
# JobHistory cleanup functions to allow the user to cleanup Redis for histories.
|
5
|
+
class HistoryList < HistoryBase
|
6
|
+
attr_accessor :list_name
|
7
|
+
|
8
|
+
def initialize(class_name, list_name)
|
9
|
+
super(class_name)
|
10
|
+
|
11
|
+
@list_name = list_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_job(job_id)
|
15
|
+
add_to_history
|
16
|
+
|
17
|
+
job_count = redis.lpush(job_list_key, job_id)
|
18
|
+
redis.incr(total_jobs_key)
|
19
|
+
|
20
|
+
delete_old_jobs(job_count)
|
21
|
+
|
22
|
+
job_count
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_job(job_id)
|
26
|
+
redis.lrem(job_list_key, 0, job_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def paged_jobs(page_num = 1, page_size = nil)
|
30
|
+
page_size ||= class_page_size
|
31
|
+
page_size = Resque::Plugins::JobHistory::HistoryBase::PAGE_SIZE if page_size.to_i < 1
|
32
|
+
start = (page_num - 1) * page_size
|
33
|
+
start = 0 if start >= num_jobs
|
34
|
+
|
35
|
+
jobs(start, start + page_size - 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def jobs(start = 0, stop = -1)
|
39
|
+
job_ids(start, stop).map do |job_id|
|
40
|
+
Resque::Plugins::JobHistory::Job.new(class_name, job_id)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def job_ids(start = 0, stop = -1)
|
45
|
+
redis.lrange(job_list_key, start, stop)
|
46
|
+
end
|
47
|
+
|
48
|
+
def num_jobs
|
49
|
+
redis.llen(job_list_key)
|
50
|
+
end
|
51
|
+
|
52
|
+
def total
|
53
|
+
redis.get(total_jobs_key).to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def latest_job
|
57
|
+
job_id = redis.lrange(job_list_key, 0, 0).first
|
58
|
+
|
59
|
+
Resque::Plugins::JobHistory::Job.new(class_name, job_id) if job_id
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def add_to_history
|
65
|
+
redis.sadd(job_history_key, class_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete_old_jobs(job_count)
|
69
|
+
max_jobs = class_history_len
|
70
|
+
|
71
|
+
while job_count > max_jobs
|
72
|
+
Resque::Plugins::JobHistory::Job.new(class_name, redis.rpop(job_list_key)).purge
|
73
|
+
|
74
|
+
job_count -= 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def total_jobs_key
|
79
|
+
"#{job_history_base_key}.total_#{list_name}_jobs"
|
80
|
+
end
|
81
|
+
|
82
|
+
def job_list_key
|
83
|
+
"#{job_history_base_key}.#{list_name}_jobs"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module JobHistory
|
4
|
+
# a class encompassing a single job.
|
5
|
+
class Job < HistoryBase
|
6
|
+
attr_accessor :job_id
|
7
|
+
|
8
|
+
def initialize(class_name, job_id)
|
9
|
+
super(class_name)
|
10
|
+
|
11
|
+
@job_id = job_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def job_key
|
15
|
+
"#{job_history_base_key}.#{job_id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_time
|
19
|
+
stored_values[:start_time].try(:to_time)
|
20
|
+
end
|
21
|
+
|
22
|
+
def finished?
|
23
|
+
stored_values[:end_time].present?
|
24
|
+
end
|
25
|
+
|
26
|
+
def succeeded?
|
27
|
+
error.blank?
|
28
|
+
end
|
29
|
+
|
30
|
+
def duration
|
31
|
+
(end_time || Time.now) - start_time
|
32
|
+
end
|
33
|
+
|
34
|
+
def end_time
|
35
|
+
stored_values[:end_time].try(:to_time)
|
36
|
+
end
|
37
|
+
|
38
|
+
def args
|
39
|
+
decode_args(stored_values[:args])
|
40
|
+
end
|
41
|
+
|
42
|
+
def error
|
43
|
+
stored_values[:error]
|
44
|
+
end
|
45
|
+
|
46
|
+
def start(*args)
|
47
|
+
num_jobs = running_list.add_job(job_id)
|
48
|
+
|
49
|
+
record_num_jobs(num_jobs)
|
50
|
+
record_job_start(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def finish
|
54
|
+
redis.hset(job_key, "end_time", Time.now.utc.to_s)
|
55
|
+
|
56
|
+
finished_list.add_job(job_id)
|
57
|
+
running_list.remove_job(job_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def failed(exception)
|
61
|
+
redis.hset(job_key, "error", exception.message)
|
62
|
+
|
63
|
+
finish
|
64
|
+
end
|
65
|
+
|
66
|
+
def cancel
|
67
|
+
redis.hset(job_key,
|
68
|
+
"error",
|
69
|
+
"Unknown - Job failed to signal ending after the configured purge time or "\
|
70
|
+
"was canceled manually.")
|
71
|
+
|
72
|
+
finish
|
73
|
+
end
|
74
|
+
|
75
|
+
def retry
|
76
|
+
return unless described_class
|
77
|
+
|
78
|
+
Resque.enqueue described_class, *args
|
79
|
+
end
|
80
|
+
|
81
|
+
def purge
|
82
|
+
running_list.remove_job(job_id)
|
83
|
+
finished_list.remove_job(job_id)
|
84
|
+
|
85
|
+
redis.del(job_key)
|
86
|
+
end
|
87
|
+
|
88
|
+
def max_jobs
|
89
|
+
redis.get(max_running_key).to_i
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def record_job_start(*args)
|
95
|
+
redis.hset(job_key, "start_time", Time.now.utc.to_s)
|
96
|
+
redis.hset(job_key, "args", encode_args(*args))
|
97
|
+
end
|
98
|
+
|
99
|
+
def stored_values
|
100
|
+
unless @stored_values
|
101
|
+
@stored_values = redis.hgetall(job_key).with_indifferent_access
|
102
|
+
end
|
103
|
+
|
104
|
+
@stored_values
|
105
|
+
end
|
106
|
+
|
107
|
+
def encode_args(*args)
|
108
|
+
Resque.encode(args)
|
109
|
+
end
|
110
|
+
|
111
|
+
def decode_args(args_string)
|
112
|
+
Resque.decode(args_string)
|
113
|
+
end
|
114
|
+
|
115
|
+
def record_num_jobs(num_jobs)
|
116
|
+
if redis.get(max_running_key).to_i < num_jobs
|
117
|
+
redis.set(max_running_key, num_jobs)
|
118
|
+
end
|
119
|
+
|
120
|
+
return unless num_jobs > class_history_len
|
121
|
+
|
122
|
+
clean_old_running_jobs
|
123
|
+
end
|
124
|
+
|
125
|
+
def max_running_key
|
126
|
+
"#{job_history_base_key}.max_jobs"
|
127
|
+
end
|
128
|
+
|
129
|
+
def clean_old_running_jobs
|
130
|
+
too_old_time = class_purge_age.ago
|
131
|
+
|
132
|
+
running_list.jobs.each do |job|
|
133
|
+
job_start = job.start_time
|
134
|
+
|
135
|
+
if job_start.blank? || job_start.to_time < too_old_time
|
136
|
+
job.start(*job.args) if job_start.blank?
|
137
|
+
job.cancel
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module JobHistory
|
4
|
+
# A class encompassing tasks about the jobs as a whole.
|
5
|
+
#
|
6
|
+
# This class gets a list of the classes and can provide a summary for each.
|
7
|
+
class JobList < HistoryBase
|
8
|
+
def initialize
|
9
|
+
super("")
|
10
|
+
end
|
11
|
+
|
12
|
+
def order_param(sort_option, current_sort, current_order)
|
13
|
+
current_order ||= "asc"
|
14
|
+
|
15
|
+
if sort_option == current_sort
|
16
|
+
current_order == "asc" ? "desc" : "asc"
|
17
|
+
else
|
18
|
+
"asc"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def job_summaries(sort_key = :class_name,
|
23
|
+
sort_order = "asc",
|
24
|
+
page_num = 1,
|
25
|
+
page_size = Resque::Plugins::JobHistory::HistoryBase::PAGE_SIZE)
|
26
|
+
jobs = sorted_job_summaries(sort_key)
|
27
|
+
|
28
|
+
page_start = (page_num - 1) * page_size
|
29
|
+
page_start = 0 if page_start > jobs.length
|
30
|
+
|
31
|
+
(sort_order == "desc" ? jobs.reverse : jobs)[page_start..page_start + page_size - 1]
|
32
|
+
end
|
33
|
+
|
34
|
+
def job_classes
|
35
|
+
redis.smembers(job_history_key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def job_class_summary(class_name)
|
39
|
+
history = Resque::Plugins::JobHistory::HistoryBase.new(class_name)
|
40
|
+
|
41
|
+
running_list = history.running_list
|
42
|
+
finished_list = history.finished_list
|
43
|
+
|
44
|
+
class_summary_hash(class_name, finished_list, running_list)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def latest_job(running_list, finished_list)
|
50
|
+
running_list.latest_job || finished_list.latest_job
|
51
|
+
end
|
52
|
+
|
53
|
+
def sorted_job_summaries(sort_key)
|
54
|
+
job_classes.map { |class_name| job_class_summary(class_name) }.sort_by do |job_summary|
|
55
|
+
summary_sort_value(job_summary, sort_key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def summary_sort_value(job_summary, sort_key)
|
60
|
+
case sort_key.to_sym
|
61
|
+
when :class_name,
|
62
|
+
:running_jobs,
|
63
|
+
:finished_jobs,
|
64
|
+
:total_finished_jobs,
|
65
|
+
:total_run_jobs,
|
66
|
+
:max_running_jobs
|
67
|
+
job_summary[sort_key.to_sym]
|
68
|
+
when :start_time
|
69
|
+
job_summary[:last_run].start_time
|
70
|
+
when :durration
|
71
|
+
job_summary[:last_run].duration
|
72
|
+
when :success
|
73
|
+
job_summary[:last_run].succeeded?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def class_summary_hash(class_name, finished_list, running_list)
|
78
|
+
{ class_name: class_name,
|
79
|
+
class_name_valid: running_list.class_name_valid?,
|
80
|
+
running_jobs: running_list.num_jobs,
|
81
|
+
finished_jobs: finished_list.num_jobs,
|
82
|
+
total_run_jobs: running_list.total,
|
83
|
+
total_finished_jobs: finished_list.total,
|
84
|
+
max_running_jobs: Resque::Plugins::JobHistory::Job.new(class_name, "").max_jobs,
|
85
|
+
last_run: latest_job(running_list, finished_list) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
.job_history_pagination_block {
|
2
|
+
width: 100%;
|
3
|
+
display: flex;
|
4
|
+
justify-content: space-between;
|
5
|
+
margin-bottom: 5px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.job_history_first_page {
|
9
|
+
margin-right: 5px;
|
10
|
+
}
|
11
|
+
|
12
|
+
.job_history_prev_page {
|
13
|
+
margin-right: 5px;
|
14
|
+
margin-left: 5px;
|
15
|
+
}
|
16
|
+
|
17
|
+
.job_history_page {
|
18
|
+
margin-right: 5px;
|
19
|
+
margin-left: 5px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.job_history_next_page {
|
23
|
+
margin-right: 5px;
|
24
|
+
margin-left: 5px;
|
25
|
+
}
|
26
|
+
|
27
|
+
.job_history_last_page {
|
28
|
+
margin-left: 5px;
|
29
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
<div class="job_history_pagination_block">
|
2
|
+
<% total_pages = job_list.job_classes.length / page_size %>
|
3
|
+
<% total_pages += 1 if job_list.job_classes.length % page_size > 0 %>
|
4
|
+
<% page_num = 1 if page_num > total_pages %>
|
5
|
+
<% first_page = [1, page_num - 3].max %>
|
6
|
+
<% last_page = [total_pages, page_num + 3].min %>
|
7
|
+
<% last_page = page_num < 4 ? [total_pages, last_page + (4 - page_num)].min : last_page %>
|
8
|
+
<% first_page = page_num > total_pages - 3 ? [1, first_page + ((total_pages - page_num) - 3)].max : first_page %>
|
9
|
+
|
10
|
+
<% if total_pages > 1 %>
|
11
|
+
<div class="job_history_prev_links">
|
12
|
+
<a href="job%20history?<%= { sort: @sort_by,
|
13
|
+
page_size: page_size,
|
14
|
+
page_num: 1,
|
15
|
+
order: @sort_order }.to_param %>"
|
16
|
+
class="job_history_first_page"
|
17
|
+
disabled="<%= first_page > 1 %>">
|
18
|
+
<< First
|
19
|
+
</a>
|
20
|
+
|
21
|
+
<a href="job%20history?<%= { sort: @sort_by,
|
22
|
+
page_size: page_size,
|
23
|
+
page_num: page_num - 1,
|
24
|
+
order: @sort_order }.to_param %>"
|
25
|
+
class="job_history_prev_page"
|
26
|
+
disabled="<%= page_num > 1 %>">
|
27
|
+
< Prev
|
28
|
+
</a>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div class="job_history_pages">
|
32
|
+
<% (first_page..last_page).each do |page_number| %>
|
33
|
+
<% if page_number != page_num %>
|
34
|
+
<a href="job%20history?<%= { sort: @sort_by,
|
35
|
+
page_size: page_size,
|
36
|
+
page_num: page_number,
|
37
|
+
order: @sort_order }.to_param %>"
|
38
|
+
class="job_history_page">
|
39
|
+
<%= page_number %>
|
40
|
+
</a>
|
41
|
+
<% else %>
|
42
|
+
<%= page_number %>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<div class="job_history_next_links">
|
48
|
+
<a href="job%20history?<%= { sort: @sort_by,
|
49
|
+
page_size: page_size,
|
50
|
+
page_num: page_num + 1,
|
51
|
+
order: @sort_order }.to_param %>"
|
52
|
+
class="job_history_next_page"
|
53
|
+
disabled="<%= page_num < last_page %>">
|
54
|
+
Next >
|
55
|
+
</a>
|
56
|
+
|
57
|
+
<a href="job%20history?<%= { sort: @sort_by,
|
58
|
+
page_size: page_size,
|
59
|
+
page_num: total_pages,
|
60
|
+
order: @sort_order }.to_param %>"
|
61
|
+
class="job_history_last_page"
|
62
|
+
disabled="<%= last_page < total_pages %>">
|
63
|
+
Last >>
|
64
|
+
</a>
|
65
|
+
</div>
|
66
|
+
<% end %>
|
67
|
+
</div>
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<div class="job_history_pagination_block">
|
2
|
+
<% total_pages = history_list.num_jobs / page_size %>
|
3
|
+
<% total_pages += 1 if history_list.num_jobs % page_size > 0 %>
|
4
|
+
<% first_page = [1, page_num - 3].max %>
|
5
|
+
<% last_page = [total_pages, page_num + 3].min %>
|
6
|
+
<% last_page = page_num < 4 ? [total_pages, last_page + (4 - page_num)].min : last_page %>
|
7
|
+
<% first_page = page_num > total_pages - 3 ? [1, first_page + ((total_pages - page_num) - 3)].max : first_page %>
|
8
|
+
|
9
|
+
<% if total_pages > 1 %>
|
10
|
+
<div class="job_history_prev_links">
|
11
|
+
<a href="job_class_details?<%= { class_name: class_name,
|
12
|
+
"#{primary_type}_page_size" => page_size,
|
13
|
+
"#{primary_type}_page_num" => 1,
|
14
|
+
"#{secondary_type}_page_size" => secondary_page_size,
|
15
|
+
"#{secondary_type}_page_num" => secondary_page_num }.to_param %>"
|
16
|
+
class="job_history_first_page"
|
17
|
+
disabled="<%= first_page > 1 %>">
|
18
|
+
<< First
|
19
|
+
</a>
|
20
|
+
|
21
|
+
<a href="job_class_details?<%= { class_name: class_name,
|
22
|
+
"#{primary_type}_page_size" => page_size,
|
23
|
+
"#{primary_type}_page_num" => [1, page_num - 1].max,
|
24
|
+
"#{secondary_type}_page_size" => secondary_page_size,
|
25
|
+
"#{secondary_type}_page_num" => secondary_page_num }.to_param %>"
|
26
|
+
class="job_history_prev_page"
|
27
|
+
disabled="<%= page_num > 1 %>">
|
28
|
+
< Prev
|
29
|
+
</a>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="job_history_pages">
|
33
|
+
<% (first_page..last_page).each do |page_number| %>
|
34
|
+
<% if page_number != page_num %>
|
35
|
+
<a href="job_class_details?<%= { class_name: class_name,
|
36
|
+
"#{primary_type}_page_size" => page_size,
|
37
|
+
"#{primary_type}_page_num" => page_number,
|
38
|
+
"#{secondary_type}_page_size" => secondary_page_size,
|
39
|
+
"#{secondary_type}_page_num" => secondary_page_num }.to_param %>"
|
40
|
+
class="job_history_page">
|
41
|
+
<%= page_number %>
|
42
|
+
</a>
|
43
|
+
<% else %>
|
44
|
+
<%= page_number %>
|
45
|
+
<% end %>
|
46
|
+
<% end %>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class="job_history_next_links">
|
50
|
+
<a href="job_class_details?<%= { class_name: class_name,
|
51
|
+
"#{primary_type}_page_size" => page_size,
|
52
|
+
"#{primary_type}_page_num" => [total_pages, page_num + 1].min,
|
53
|
+
"#{secondary_type}_page_size" => secondary_page_size,
|
54
|
+
"#{secondary_type}_page_num" => secondary_page_num }.to_param %>"
|
55
|
+
class="job_history_next_page"
|
56
|
+
disabled="<%= page_num < last_page %>">
|
57
|
+
Next >
|
58
|
+
</a>
|
59
|
+
|
60
|
+
<a href="job_class_details?<%= { class_name: class_name,
|
61
|
+
"#{primary_type}_page_size" => page_size,
|
62
|
+
"#{primary_type}_page_num" => total_pages,
|
63
|
+
"#{secondary_type}_page_size" => secondary_page_size,
|
64
|
+
"#{secondary_type}_page_num" => secondary_page_num }.to_param %>"
|
65
|
+
class="job_history_last_page"
|
66
|
+
disabled="<%= last_page < total_pages %>">
|
67
|
+
Last >>
|
68
|
+
</a>
|
69
|
+
</div>
|
70
|
+
<% end %>
|
71
|
+
</div>
|