resque-approve 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +30 -0
  4. data/lib/resque-approve.rb +10 -0
  5. data/lib/resque/approve_server.rb +232 -0
  6. data/lib/resque/plugins/approve.rb +77 -0
  7. data/lib/resque/plugins/approve/approval_key_list.rb +100 -0
  8. data/lib/resque/plugins/approve/cleaner.rb +44 -0
  9. data/lib/resque/plugins/approve/pending_job.rb +187 -0
  10. data/lib/resque/plugins/approve/pending_job_queue.rb +128 -0
  11. data/lib/resque/plugins/approve/redis_access.rb +16 -0
  12. data/lib/resque/plugins/version.rb +9 -0
  13. data/lib/resque/server/public/approve.css +56 -0
  14. data/lib/resque/server/views/_approval_key_list_pagination.erb +67 -0
  15. data/lib/resque/server/views/_approval_key_rows.erb +18 -0
  16. data/lib/resque/server/views/_job_list_table.erb +30 -0
  17. data/lib/resque/server/views/_job_pagination.erb +67 -0
  18. data/lib/resque/server/views/approval_keys.erb +66 -0
  19. data/lib/resque/server/views/job_details.erb +82 -0
  20. data/lib/resque/server/views/job_list.erb +58 -0
  21. data/lib/tasks/resque-approve_tasks.rake +6 -0
  22. data/spec/approve/approval_key_list_spec.rb +289 -0
  23. data/spec/approve/cleaner_spec.rb +96 -0
  24. data/spec/approve/pending_job_queue_spec.rb +219 -0
  25. data/spec/approve/pending_job_spec.rb +326 -0
  26. data/spec/approve_spec.rb +188 -0
  27. data/spec/examples.txt +135 -0
  28. data/spec/rails_helper.rb +35 -0
  29. data/spec/server/public/approve.css_spec.rb +18 -0
  30. data/spec/server/views/approval_keys.erb_spec.rb +105 -0
  31. data/spec/server/views/job_details.erb_spec.rb +133 -0
  32. data/spec/server/views/job_list.erb_spec.rb +108 -0
  33. data/spec/spec_helper.rb +101 -0
  34. data/spec/support/config/redis-auth.yml +12 -0
  35. data/spec/support/jobs/01_basic_job.rb +13 -0
  36. data/spec/support/jobs/auto_delete_approval_key_job.rb +5 -0
  37. data/spec/support/purge_all.rb +15 -0
  38. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Approve
6
+ VERSION = "0.0.1"
7
+ end
8
+ end
9
+ 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
+ &lt;&lt; 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
+ &lt; 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 &gt;
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 &gt;&gt;
64
+ </a>
65
+ </div>
66
+ <% end %>
67
+ </div>