resque-approve 0.0.1
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/Rakefile +30 -0
- data/lib/resque-approve.rb +10 -0
- data/lib/resque/approve_server.rb +232 -0
- data/lib/resque/plugins/approve.rb +77 -0
- data/lib/resque/plugins/approve/approval_key_list.rb +100 -0
- data/lib/resque/plugins/approve/cleaner.rb +44 -0
- data/lib/resque/plugins/approve/pending_job.rb +187 -0
- data/lib/resque/plugins/approve/pending_job_queue.rb +128 -0
- data/lib/resque/plugins/approve/redis_access.rb +16 -0
- data/lib/resque/plugins/version.rb +9 -0
- data/lib/resque/server/public/approve.css +56 -0
- data/lib/resque/server/views/_approval_key_list_pagination.erb +67 -0
- data/lib/resque/server/views/_approval_key_rows.erb +18 -0
- data/lib/resque/server/views/_job_list_table.erb +30 -0
- data/lib/resque/server/views/_job_pagination.erb +67 -0
- data/lib/resque/server/views/approval_keys.erb +66 -0
- data/lib/resque/server/views/job_details.erb +82 -0
- data/lib/resque/server/views/job_list.erb +58 -0
- data/lib/tasks/resque-approve_tasks.rake +6 -0
- data/spec/approve/approval_key_list_spec.rb +289 -0
- data/spec/approve/cleaner_spec.rb +96 -0
- data/spec/approve/pending_job_queue_spec.rb +219 -0
- data/spec/approve/pending_job_spec.rb +326 -0
- data/spec/approve_spec.rb +188 -0
- data/spec/examples.txt +135 -0
- data/spec/rails_helper.rb +35 -0
- data/spec/server/public/approve.css_spec.rb +18 -0
- data/spec/server/views/approval_keys.erb_spec.rb +105 -0
- data/spec/server/views/job_details.erb_spec.rb +133 -0
- data/spec/server/views/job_list.erb_spec.rb +108 -0
- data/spec/spec_helper.rb +101 -0
- data/spec/support/config/redis-auth.yml +12 -0
- data/spec/support/jobs/01_basic_job.rb +13 -0
- data/spec/support/jobs/auto_delete_approval_key_job.rb +5 -0
- data/spec/support/purge_all.rb +15 -0
- metadata +292 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Approve
|
6
|
+
# A class for cleaning out all redis values associated with
|
7
|
+
class Cleaner
|
8
|
+
include RedisAccess
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def redis
|
12
|
+
@redis ||= Resque::Plugins::Approve::Cleaner.new.redis
|
13
|
+
end
|
14
|
+
|
15
|
+
def purge_all
|
16
|
+
keys = redis.keys("*")
|
17
|
+
|
18
|
+
return if keys.blank?
|
19
|
+
|
20
|
+
redis.del(*keys)
|
21
|
+
end
|
22
|
+
|
23
|
+
def cleanup_jobs
|
24
|
+
jobs = redis.keys("approve.pending_job.*")
|
25
|
+
|
26
|
+
jobs.each do |job_key|
|
27
|
+
job = Resque::Plugins::Approve::PendingJob.new(job_key[20..-1])
|
28
|
+
|
29
|
+
job.queue.verify_job(job)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cleanup_queues
|
34
|
+
key_list = Resque::Plugins::Approve::ApprovalKeyList.new
|
35
|
+
|
36
|
+
key_list.queues.each do |pending_job_queue|
|
37
|
+
key_list.remove_key(pending_job_queue.approval_key) if pending_job_queue.num_jobs.zero?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Approve
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
|
8
|
+
# A class representing a Pending job that is awaiting approval.
|
9
|
+
#
|
10
|
+
# Each job has the following values:
|
11
|
+
# id - The approve gem ID used to store and restore the job in Redis
|
12
|
+
# class_name - The name of the job class to be enqueued
|
13
|
+
# args - The arguments for the job to be enqueued
|
14
|
+
#
|
15
|
+
# approval_key - The approval key for the pending job that will be used to release/approve the job.
|
16
|
+
# This will default to nil.
|
17
|
+
# approval_queue - The queue that the pending job will be enqueued into.
|
18
|
+
# This will default to the queue for the job.
|
19
|
+
# approval_at - The time when the job is to be enqueued.
|
20
|
+
# This will call `enqueue_at` to enqueue the job, so this option will only work
|
21
|
+
# properly if you use the `resque-scheduler` gem.
|
22
|
+
#
|
23
|
+
# When using resque-scheduler, there are two ways to delay enqueue a job
|
24
|
+
# and how you do this depends on your use case.
|
25
|
+
#
|
26
|
+
# Resque.enqueue YourJob, params, approval_key: "your key", approval_at: time
|
27
|
+
# This will enqueue the job for approval, which will pause the job until it is approved.
|
28
|
+
# Once approved, the job will delay enqueue for time, and will execute immediately or at
|
29
|
+
# that time depending on if the time has passed.
|
30
|
+
#
|
31
|
+
# This is the recommended method to use as it will not run the job early, and it will allow
|
32
|
+
# you to release it without knowing if it is still delayed or not.
|
33
|
+
#
|
34
|
+
# You can also do:
|
35
|
+
# Resque.enqueue_at time, YourJob, params, approval_key: "your key"
|
36
|
+
# This will delay enqueue the job - because it has not been enqueued yet, the job
|
37
|
+
# cannot be releaed until the time has passed and the job is actually enqueued.
|
38
|
+
# Any time after that point, it can be released. Releasing the key before this
|
39
|
+
# time has no effect on this job.
|
40
|
+
class PendingJob
|
41
|
+
include Resque::Plugins::Approve::RedisAccess
|
42
|
+
include Comparable
|
43
|
+
|
44
|
+
attr_reader :id
|
45
|
+
|
46
|
+
def initialize(id = SecureRandom.uuid, class_name: nil, args: [])
|
47
|
+
@approve_options = {}
|
48
|
+
@id = id
|
49
|
+
@class_name = class_name.is_a?(String) ? class_name : class_name&.name
|
50
|
+
self.args = args
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(other)
|
54
|
+
return nil unless other.is_a?(Resque::Plugins::Approve::PendingJob)
|
55
|
+
|
56
|
+
id <=> other.id
|
57
|
+
end
|
58
|
+
|
59
|
+
def class_name
|
60
|
+
@class_name ||= stored_values[:class_name]
|
61
|
+
end
|
62
|
+
|
63
|
+
def args
|
64
|
+
@args = if @args.present?
|
65
|
+
@args
|
66
|
+
else
|
67
|
+
Array.wrap(decode_args(stored_values[:args]))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def args=(value)
|
72
|
+
if value.nil?
|
73
|
+
@args = []
|
74
|
+
else
|
75
|
+
@args = Array.wrap(value).dup
|
76
|
+
|
77
|
+
extract_approve_options
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def approve_options
|
82
|
+
@approve_options = if @approve_options.present?
|
83
|
+
@approve_options
|
84
|
+
else
|
85
|
+
(decode_args(stored_values[:approve_options])&.first || {}).with_indifferent_access
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def requires_approval?
|
90
|
+
@requires_approval ||= approve_options.key?(:approval_key) || approve_options[:requires_approval]
|
91
|
+
end
|
92
|
+
|
93
|
+
def approval_key
|
94
|
+
@approval_key ||= approve_options[:approval_key]
|
95
|
+
end
|
96
|
+
|
97
|
+
def approval_queue
|
98
|
+
@approval_queue ||= approve_options[:approval_queue] || Resque.queue_from_class(klass)
|
99
|
+
end
|
100
|
+
|
101
|
+
def approval_at
|
102
|
+
@approval_at ||= approve_options[:approval_at]&.to_time
|
103
|
+
end
|
104
|
+
|
105
|
+
def queue_time
|
106
|
+
@queue_time ||= stored_values[:queue_time]&.to_time
|
107
|
+
end
|
108
|
+
|
109
|
+
def enqueue_job
|
110
|
+
return_value = if approval_at.present?
|
111
|
+
Resque.enqueue_at_with_queue approval_queue, approval_at, klass, *args
|
112
|
+
else
|
113
|
+
Resque.enqueue_to approval_queue, klass, *args
|
114
|
+
end
|
115
|
+
|
116
|
+
delete
|
117
|
+
|
118
|
+
return_value
|
119
|
+
end
|
120
|
+
|
121
|
+
# rubocop:disable Metrics/AbcSize
|
122
|
+
def save!
|
123
|
+
redis.hset(job_key, "class_name", class_name)
|
124
|
+
redis.hset(job_key, "args", encode_args(*args))
|
125
|
+
redis.hset(job_key, "approve_options", encode_args(approve_options))
|
126
|
+
redis.hset(job_key, "queue_time", Time.now)
|
127
|
+
end
|
128
|
+
|
129
|
+
# rubocop:enable Metrics/AbcSize
|
130
|
+
|
131
|
+
def delete
|
132
|
+
# Make sure the job is loaded into memory so we can use it even though we are going to delete it.
|
133
|
+
stored_values
|
134
|
+
|
135
|
+
return if class_name.blank?
|
136
|
+
|
137
|
+
redis.del(job_key)
|
138
|
+
|
139
|
+
queue.remove_job(self)
|
140
|
+
end
|
141
|
+
|
142
|
+
def queue
|
143
|
+
@queue ||= Resque::Plugins::Approve::PendingJobQueue.new(approval_key)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def klass
|
149
|
+
@klass ||= class_name.constantize
|
150
|
+
end
|
151
|
+
|
152
|
+
def job_key
|
153
|
+
@job_key ||= "approve.pending_job.#{id}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def stored_values
|
157
|
+
@stored_values ||= (redis.hgetall(job_key) || {}).with_indifferent_access
|
158
|
+
end
|
159
|
+
|
160
|
+
def extract_approve_options
|
161
|
+
return if args.blank? || !args[-1].is_a?(Hash)
|
162
|
+
|
163
|
+
self.approve_options = args.pop
|
164
|
+
|
165
|
+
options = approve_options.slice!(:approval_key, :approval_queue, :approval_at)
|
166
|
+
|
167
|
+
args << options.to_hash if options.present?
|
168
|
+
end
|
169
|
+
|
170
|
+
def encode_args(*args)
|
171
|
+
Resque.encode(args)
|
172
|
+
end
|
173
|
+
|
174
|
+
def decode_args(args_string)
|
175
|
+
return if args_string.blank?
|
176
|
+
|
177
|
+
Resque.decode(args_string)
|
178
|
+
end
|
179
|
+
|
180
|
+
def approve_options=(value)
|
181
|
+
@approve_options = (value&.dup || {}).with_indifferent_access
|
182
|
+
end
|
183
|
+
end
|
184
|
+
# rubocop:enable Metrics/ClassLength
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Approve
|
6
|
+
# A class representing a queue of Pending jobs.
|
7
|
+
#
|
8
|
+
# The queue is named with the approval_key for all of the jobs in the queue and contains a list of the jobs.
|
9
|
+
class PendingJobQueue
|
10
|
+
include Resque::Plugins::Approve::RedisAccess
|
11
|
+
|
12
|
+
attr_reader :approval_key
|
13
|
+
|
14
|
+
def initialize(approval_key)
|
15
|
+
@approval_key = approval_key
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete
|
19
|
+
jobs.each(&:delete)
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_job(job)
|
23
|
+
redis.lrem(queue_key, 0, job.id)
|
24
|
+
|
25
|
+
remove_approval_key(job)
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify_job(job)
|
29
|
+
ApprovalKeyList.new.add_key(job.approval_key)
|
30
|
+
|
31
|
+
ids = redis.lrange(queue_key, 0, -1)
|
32
|
+
|
33
|
+
return if ids.include?(job.id)
|
34
|
+
|
35
|
+
redis.lpush(queue_key, job.id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_job(job)
|
39
|
+
redis.rpush(queue_key, job.id)
|
40
|
+
|
41
|
+
job.save!
|
42
|
+
end
|
43
|
+
|
44
|
+
def approve_one
|
45
|
+
id = redis.lpop(queue_key)
|
46
|
+
|
47
|
+
enqueue_job(id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def approve_all
|
51
|
+
true while approve_one
|
52
|
+
end
|
53
|
+
|
54
|
+
def pop_job
|
55
|
+
id = redis.rpop(queue_key)
|
56
|
+
|
57
|
+
enqueue_job(id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_one
|
61
|
+
id = redis.lpop(queue_key)
|
62
|
+
|
63
|
+
delete_job(id)
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove_all
|
67
|
+
true while remove_one
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_job_pop
|
71
|
+
id = redis.rpop(queue_key)
|
72
|
+
|
73
|
+
delete_job(id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def paged_jobs(page_num = 1, job_page_size = nil)
|
77
|
+
job_page_size ||= 20
|
78
|
+
job_page_size = job_page_size.to_i
|
79
|
+
job_page_size = 20 if job_page_size < 1
|
80
|
+
start = (page_num - 1) * job_page_size
|
81
|
+
start = 0 if start >= num_jobs || start.negative?
|
82
|
+
|
83
|
+
jobs(start, start + job_page_size - 1)
|
84
|
+
end
|
85
|
+
|
86
|
+
def jobs(start = 0, stop = -1)
|
87
|
+
redis.lrange(queue_key, start, stop).map { |id| Resque::Plugins::Approve::PendingJob.new(id) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def num_jobs
|
91
|
+
redis.llen(queue_key)
|
92
|
+
end
|
93
|
+
|
94
|
+
def first_enqueued
|
95
|
+
jobs(0, 0).first&.queue_time
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def remove_approval_key(job)
|
101
|
+
return unless job.class_name.constantize.auto_delete_approval_key
|
102
|
+
|
103
|
+
ApprovalKeyList.new.remove_key(approval_key) if num_jobs.zero?
|
104
|
+
end
|
105
|
+
|
106
|
+
def enqueue_job(id)
|
107
|
+
return false unless id.present?
|
108
|
+
|
109
|
+
Resque::Plugins::Approve::PendingJob.new(id).enqueue_job
|
110
|
+
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def delete_job(id)
|
115
|
+
return false unless id.present?
|
116
|
+
|
117
|
+
Resque::Plugins::Approve::PendingJob.new(id).delete
|
118
|
+
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
def queue_key
|
123
|
+
@queue_key ||= "approve.job_queue.#{approval_key}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Approve
|
6
|
+
# A module to add a `redis` method for a class in this gem that needs redis to get a reids object that is namespaced.
|
7
|
+
module RedisAccess
|
8
|
+
NAME_SPACE = "Resque::Plugins::Approve::"
|
9
|
+
|
10
|
+
def redis
|
11
|
+
@redis ||= Redis::Namespace.new(NAME_SPACE, redis: Resque.redis)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
.approve_pagination_block {
|
2
|
+
width: 100%;
|
3
|
+
display: flex;
|
4
|
+
justify-content: space-between;
|
5
|
+
margin-bottom: 5px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.approve_first_page {
|
9
|
+
margin-right: 5px;
|
10
|
+
}
|
11
|
+
|
12
|
+
.approve_prev_page {
|
13
|
+
margin-right: 5px;
|
14
|
+
margin-left: 5px;
|
15
|
+
}
|
16
|
+
|
17
|
+
.approve_page {
|
18
|
+
margin-right: 5px;
|
19
|
+
margin-left: 5px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.approve_next_page {
|
23
|
+
margin-right: 5px;
|
24
|
+
margin-left: 5px;
|
25
|
+
}
|
26
|
+
|
27
|
+
.approve_last_page {
|
28
|
+
margin-left: 5px;
|
29
|
+
}
|
30
|
+
|
31
|
+
.table_container {
|
32
|
+
width: 100%;
|
33
|
+
overflow-x: scroll;
|
34
|
+
}
|
35
|
+
|
36
|
+
.approve_error {
|
37
|
+
background-color: lightcoral;
|
38
|
+
}
|
39
|
+
|
40
|
+
.approve_linear_history_div {
|
41
|
+
margin-top: 5px;
|
42
|
+
float: left;
|
43
|
+
}
|
44
|
+
|
45
|
+
#approve_search_div {
|
46
|
+
float: right;
|
47
|
+
margin-bottom: 10px;
|
48
|
+
}
|
49
|
+
|
50
|
+
#approve_search_div form {
|
51
|
+
margin-top: 0px;
|
52
|
+
}
|
53
|
+
|
54
|
+
.approve_reset {
|
55
|
+
clear: both;
|
56
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
<div class="approve_pagination_block">
|
2
|
+
<% total_pages = queue_list.job_queues.length / page_size %>
|
3
|
+
<% total_pages += 1 if queue_list.job_queues.length % page_size > 0 %>
|
4
|
+
<% page_num = 1 if page_num > total_pages || page_num < 1 %>
|
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="approve_prev_links">
|
12
|
+
<a href="<%= u("approve") %>?<%= { sort: @sort_by,
|
13
|
+
page_size: page_size,
|
14
|
+
page_num: 1,
|
15
|
+
order: @sort_order }.to_param %>"
|
16
|
+
class="approve_first_page"
|
17
|
+
disabled="<%= first_page > 1 %>">
|
18
|
+
<< First
|
19
|
+
</a>
|
20
|
+
|
21
|
+
<a href="<%= u("approve") %>?<%= { sort: @sort_by,
|
22
|
+
page_size: page_size,
|
23
|
+
page_num: [1, page_num - 1].max,
|
24
|
+
order: @sort_order }.to_param %>"
|
25
|
+
class="approve_prev_page"
|
26
|
+
disabled="<%= page_num > 1 %>">
|
27
|
+
< Prev
|
28
|
+
</a>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div class="approve_pages">
|
32
|
+
<% (first_page..last_page).each do |page_number| %>
|
33
|
+
<% if page_number != page_num %>
|
34
|
+
<a href="<%= u("approve") %>?<%= { sort: @sort_by,
|
35
|
+
page_size: page_size,
|
36
|
+
page_num: page_number,
|
37
|
+
order: @sort_order }.to_param %>"
|
38
|
+
class="approve_page">
|
39
|
+
<%= page_number %>
|
40
|
+
</a>
|
41
|
+
<% else %>
|
42
|
+
<%= page_number %>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<div class="approve_next_links">
|
48
|
+
<a href="<%= u("approve") %>?<%= { sort: @sort_by,
|
49
|
+
page_size: page_size,
|
50
|
+
page_num: [total_pages, page_num + 1].min,
|
51
|
+
order: @sort_order }.to_param %>"
|
52
|
+
class="approve_next_page"
|
53
|
+
disabled="<%= page_num < last_page %>">
|
54
|
+
Next >
|
55
|
+
</a>
|
56
|
+
|
57
|
+
<a href="<%= u("approve") %>?<%= { sort: @sort_by,
|
58
|
+
page_size: page_size,
|
59
|
+
page_num: total_pages,
|
60
|
+
order: @sort_order }.to_param %>"
|
61
|
+
class="approve_last_page"
|
62
|
+
disabled="<%= last_page < total_pages %>">
|
63
|
+
Last >>
|
64
|
+
</a>
|
65
|
+
</div>
|
66
|
+
<% end %>
|
67
|
+
</div>
|