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.
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>