resque-approve 0.0.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/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>
|