resque 3.0.2 → 3.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 357503e977394f513419b3146484eab796354a978d6d96e74e16fae0dc2fd3de
4
- data.tar.gz: 4e3da6258793f0b094cba61c35436d30f1193d8d43553e312b8e5c14cbb7ee5b
3
+ metadata.gz: 47f174d6353bf3747da1ec6189f5b405717399418b421c8b071b3fa806869355
4
+ data.tar.gz: 573d98cd9d90a504bf1eb79e8d93b0fcd59d67bc36adc5126cbb144304e03bc3
5
5
  SHA512:
6
- metadata.gz: 00e3f45822df5638bb129c32c10878e2651cb74081711d4d4ef30da47403195c07276a4dbcb60e650c35dbc40ba0bcfb48d4a2c625eb442dec476e8a18929a7a
7
- data.tar.gz: 942ff614b651c87ab52726aa88564243fd60b3c87edff7670fcb61491f2a45516a8c91eed44119d5945a322f0598f9be28d3ab5d610146e9d488e261514bf0af
6
+ metadata.gz: 265e97433c7e8eb1d5fcaafad19f4505fabca6580e9e727a07f1e281a7af310ddae15b6721452cd7064249f55dcdf1bfa3967ea3823b8f1c436eee91c15bb917
7
+ data.tar.gz: 9bce6a28a4ee107fc1f347ac92b7b885823d67a2b725fd0152ea2ad7e9b34a6003884e35330f4876a9bddf854aaa46bbec096db1766e5d653b6572b4b0f99ec9
data/HISTORY.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## 3.1.0
2
+
3
+ ### Breaking Changes
4
+
5
+ * **Minimum Ruby version is now 3.2.0** - Ruby 3.0 and 3.1 are no longer supported (#1958)
6
+ * **Minimum Redis server version is now 4.0** - key removal now uses `UNLINK` instead of `DEL` (#1962)
7
+
8
+ ### Added
9
+
10
+ * Added `always` hooks - run after a job finishes, whether it succeeded or failed (#1966)
11
+ * Each failure now has a unique `failure_id` (#1943)
12
+ * resque-web: Dark mode (#1963)
13
+ * resque-web: Delete button for queued jobs (#1969)
14
+ * resque-web: `link` and `script` tag helpers for plugins (#1914)
15
+
16
+ ### Fixed
17
+
18
+ * Keep the heartbeat thread alive when a heartbeat fails (#1967)
19
+ * `Resque::Failure::Multiple` saves to every backend even when one of them raises (#1964)
20
+ * Let Rails eager load the application rather than doing it ourselves (#1867)
21
+ * Test the Rails and Redis versions the CI matrix claims to cover (#1978)
22
+ * Require `minitest/mock` from its extracted gem (#1976)
23
+ * Get on modern RuboCop (#1977)
24
+
25
+ ## 3.0.3
26
+
27
+ ### Fixed
28
+
29
+ * resque-web: Fix an off-by-one error in the failed jobs pagination end index (#1931)
30
+ * resque-web: Pass the browser url as an argv element instead of a shell string, and launch it on Windows without going through `cmd.exe` (#1956)
31
+ * resque-web: Probe ports with `URI(...).open` instead of `Kernel.open` (#1955)
32
+ * resque-web: Build the relatized time markup with DOM APIs instead of HTML strings (#1957)
33
+
1
34
  ## 3.0.2
2
35
 
3
36
  ### Added
data/README.markdown CHANGED
@@ -39,13 +39,14 @@ The Resque frontend tells you what workers are doing, what workers are
39
39
  not doing, what queues you're using, what's in those queues, provides
40
40
  general usage stats, and helps you track failures.
41
41
 
42
- Resque 3.0 requires Ruby 3.0.0 or newer.
42
+ Resque requires Ruby 3.2.0 or newer.
43
43
 
44
44
  **Version Support:**
45
- - Ruby: 3.0, 3.1, 3.2, 3.3, 3.4+
45
+ - Ruby: 3.2, 3.3, 3.4, 4.0
46
+ - Redis server: 4.0+
46
47
  - Redis gem: 4.0+
47
48
  - Rack: 2.x or 3.x
48
- - Rails (for ActiveJob): 7.2+ (requires Ruby 3.1+ for Rails 8.0+)
49
+ - Rails (for ActiveJob): 7.2+
49
50
 
50
51
  ### Resque 3.0
51
52
 
@@ -431,6 +432,15 @@ Resque::Failure::Multiple.classes = [Resque::Failure::Redis, Resque::Failure::Ai
431
432
  Resque::Failure.backend = Resque::Failure::Multiple
432
433
  ```
433
434
 
435
+ Each failure is recorded with a unique `failure_id` shared by every backend that
436
+ accepts one, so the same failure can be correlated across them. Failures are
437
+ still addressed by index; the id is additive metadata. See
438
+ [docs/FAILURE_IDS.md](https://github.com/resque/resque/blob/master/docs/FAILURE_IDS.md).
439
+
440
+ A backend that raises while saving no longer stops the ones after it. Once every
441
+ backend has run, `Resque::Failure::Multiple::BackendError` is raised; its
442
+ `original_errors` maps each failed backend class to the exception it raised.
443
+
434
444
  Keep this in mind when writing your jobs: you may want to throw
435
445
  exceptions you would not normally throw in order to assist debugging.
436
446
 
data/Rakefile CHANGED
@@ -44,11 +44,6 @@ task "env:aj_integration" do
44
44
  end
45
45
 
46
46
  Rake::TestTask.new('test:activejob:integration' => 'env:aj_integration') do |t|
47
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1")
48
- puts "Integration tests require Ruby 3.1 or later"
49
- exit 0
50
- end
51
-
52
47
  t.description = "Run integration tests for Resque::ActiveJob::Adapter"
53
48
  t.libs << "test"
54
49
  t.libs << "test/active_job"
@@ -133,7 +133,7 @@ module Resque
133
133
  def remove_queue(queue)
134
134
  @redis.pipelined do |piped|
135
135
  piped.srem(:queues, [queue.to_s])
136
- piped.del(redis_key_for_queue(queue))
136
+ piped.unlink(redis_key_for_queue(queue))
137
137
  end
138
138
  end
139
139
 
@@ -178,7 +178,7 @@ module Resque
178
178
  end
179
179
 
180
180
  def remove_failed_queue(failed_queue_name=:failed)
181
- @redis.del(failed_queue_name)
181
+ @redis.unlink(failed_queue_name)
182
182
  end
183
183
 
184
184
  def num_failed(failed_queue_name=:failed)
@@ -198,7 +198,7 @@ module Resque
198
198
  end
199
199
 
200
200
  def clear_failed_queue(failed_queue_name=:failed)
201
- @redis.del(failed_queue_name)
201
+ @redis.unlink(failed_queue_name)
202
202
  end
203
203
 
204
204
  def update_item_in_failed_queue(index_in_failed_queue,new_item_data,failed_queue_name=:failed)
@@ -252,8 +252,8 @@ module Resque
252
252
  def unregister_worker(worker, &block)
253
253
  @redis.pipelined do |piped|
254
254
  piped.srem(:workers, [worker.id])
255
- piped.del(redis_key_for_worker(worker))
256
- piped.del(redis_key_for_worker_start_time(worker))
255
+ piped.unlink(redis_key_for_worker(worker))
256
+ piped.unlink(redis_key_for_worker_start_time(worker))
257
257
  piped.hdel(HEARTBEAT_KEY, worker.to_s)
258
258
 
259
259
  block.call redis: piped
@@ -291,7 +291,7 @@ module Resque
291
291
 
292
292
  def worker_done_working(worker, &block)
293
293
  @redis.pipelined do |piped|
294
- piped.del(redis_key_for_worker(worker))
294
+ piped.unlink(redis_key_for_worker(worker))
295
295
  block.call redis: piped
296
296
  end
297
297
  end
@@ -328,7 +328,7 @@ module Resque
328
328
  end
329
329
 
330
330
  def clear_stat(stat, redis: @redis)
331
- redis.del("stat:#{stat}")
331
+ redis.unlink("stat:#{stat}")
332
332
  end
333
333
  end
334
334
  end
@@ -17,11 +17,16 @@ module Resque
17
17
  # The payload object associated with the failed job
18
18
  attr_accessor :payload
19
19
 
20
+ # A unique identifier for this failure, shared by every backend that
21
+ # accepts one. Nil when generation is turned off.
22
+ attr_accessor :failure_id
23
+
20
24
  def initialize(exception, worker, queue, payload)
21
25
  @exception = exception
22
26
  @worker = worker
23
27
  @queue = queue
24
28
  @payload = payload
29
+ @failure_id = SecureRandom.uuid if Resque::Failure.generate_failure_ids?
25
30
  end
26
31
 
27
32
  # When a job fails, a new instance of your Failure backend is created
@@ -3,6 +3,17 @@ module Resque
3
3
  # A Failure backend that uses multiple backends
4
4
  # delegates all queries to the first backend
5
5
  class Multiple < Base
6
+ class BackendError < StandardError
7
+ attr_reader :original_errors
8
+
9
+ def initialize(original_errors = {})
10
+ @original_errors = original_errors
11
+ message = "Some backends raise error: " + @original_errors.map{|backend, e|
12
+ "#{backend}:#{e.class}:#{e.message.inspect}"
13
+ }.join("; ")
14
+ super(message)
15
+ end
16
+ end
6
17
 
7
18
  class << self
8
19
  attr_accessor :classes
@@ -19,7 +30,18 @@ module Resque
19
30
  end
20
31
 
21
32
  def save
22
- @backends.each(&:save)
33
+ errors = {}
34
+
35
+ @backends.each do |backend|
36
+ backend.failure_id = failure_id if backend.respond_to?(:failure_id=)
37
+ backend.save
38
+ rescue
39
+ errors[backend.class] = $!
40
+ end
41
+
42
+ unless errors.empty?
43
+ raise BackendError.new(errors)
44
+ end
23
45
  end
24
46
 
25
47
  # The number of failures.
@@ -22,6 +22,7 @@ module Resque
22
22
  :worker => worker.to_s,
23
23
  :queue => queue
24
24
  }
25
+ data[:failure_id] = failure_id if failure_id
25
26
  data = Resque.encode(data)
26
27
  data_store.push_to_failed_queue(data)
27
28
  end
@@ -22,6 +22,7 @@ module Resque
22
22
  :worker => worker.to_s,
23
23
  :queue => queue
24
24
  }
25
+ data[:failure_id] = failure_id if failure_id
25
26
  data = Resque.encode(data)
26
27
  data_store.push_to_failed_queue(data,Resque::Failure.failure_queue_name(queue))
27
28
  end
@@ -9,12 +9,33 @@ module Resque
9
9
  # Creates a new failure, which is delegated to the appropriate backend.
10
10
  #
11
11
  # Expects a hash with the following keys:
12
- # :exception - The Exception object
13
- # :worker - The Worker object who is reporting the failure
14
- # :queue - The string name of the queue from which the job was pulled
15
- # :payload - The job's payload
12
+ # :exception - The Exception object
13
+ # :worker - The Worker object who is reporting the failure
14
+ # :queue - The string name of the queue from which the job was pulled
15
+ # :payload - The job's payload
16
+ # :failure_id - Optional id to record this failure under, in place of the
17
+ # one the backend generates for itself.
16
18
  def self.create(options = {})
17
- backend.new(*options.values_at(:exception, :worker, :queue, :payload)).save
19
+ failure = backend.new(*options.values_at(:exception, :worker, :queue, :payload))
20
+
21
+ # Backends are not required to accept an id.
22
+ if options[:failure_id] && failure.respond_to?(:failure_id=)
23
+ failure.failure_id = options[:failure_id]
24
+ end
25
+
26
+ failure.save
27
+ end
28
+
29
+ class << self
30
+ attr_writer :generate_failure_ids
31
+
32
+ # Whether failures are given a `failure_id` when they are built.
33
+ # Defaults to true. An explicit `:failure_id` passed to `create` sets the
34
+ # `failure_id` even if automatic generation is disabled.
35
+ def generate_failure_ids?
36
+ return true if @generate_failure_ids.nil?
37
+ !!@generate_failure_ids
38
+ end
18
39
  end
19
40
 
20
41
  #
data/lib/resque/job.rb CHANGED
@@ -201,6 +201,8 @@ module Resque
201
201
  rescue Object => e
202
202
  run_failure_hooks(e)
203
203
  raise e
204
+ ensure
205
+ run_always_hooks($!)
204
206
  end
205
207
  end
206
208
 
@@ -278,6 +280,10 @@ module Resque
278
280
  @failure_hooks ||= Plugin.failure_hooks(payload_class)
279
281
  end
280
282
 
283
+ def always_hooks
284
+ @always_hooks ||= Plugin.always_hooks(payload_class)
285
+ end
286
+
281
287
  def run_failure_hooks(exception)
282
288
  begin
283
289
  job_args = args || []
@@ -292,5 +298,17 @@ module Resque
292
298
  @failure_hooks_ran = true
293
299
  end
294
300
  end
301
+
302
+ def run_always_hooks(job_failure = nil)
303
+ job_args = args || []
304
+ always_hooks.each { |hook| payload_class.send(hook, *job_args) } if has_payload_class?
305
+ rescue Exception => e
306
+ if job_failure
307
+ error_message = "Additional error (#{e.class}: #{e}) occurred in running always hooks for job #{inspect}\n" \
308
+ "Original error that caused job failure was #{e.class}: #{job_failure.class}: #{job_failure.message}"
309
+ raise RuntimeError.new(error_message)
310
+ end
311
+ raise e
312
+ end
295
313
  end
296
314
  end
data/lib/resque/plugin.rb CHANGED
@@ -55,6 +55,11 @@ module Resque
55
55
  get_hook_names(job, 'on_failure')
56
56
  end
57
57
 
58
+ # Given an object, returns a list of `always` hook names
59
+ def always_hooks(job)
60
+ get_hook_names(job, 'always')
61
+ end
62
+
58
63
  # Given an object, returns a list `after_enqueue` hook names.
59
64
  def after_enqueue_hooks(job)
60
65
  get_hook_names(job, 'after_enqueue')
@@ -1,6 +1,44 @@
1
+ // Theme toggle functionality
2
+ (function() {
3
+ // Reading localStorage throws outright when the browser blocks site data,
4
+ // and this runs before the rest of the file.
5
+ var storedTheme;
6
+ try {
7
+ storedTheme = localStorage.getItem('resque-theme');
8
+ } catch (e) {
9
+ return;
10
+ }
11
+ if (storedTheme) {
12
+ document.documentElement.setAttribute('data-theme', storedTheme);
13
+ }
14
+ })();
15
+
1
16
  $(function() {
2
17
  var poll_interval = 2
3
18
 
19
+ // Theme toggle handler
20
+ $('.theme-toggle').click(function() {
21
+ var html = document.documentElement;
22
+ var currentTheme = html.getAttribute('data-theme');
23
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
24
+ var newTheme;
25
+
26
+ if (currentTheme === 'dark') {
27
+ newTheme = 'light';
28
+ } else if (currentTheme === 'light') {
29
+ newTheme = 'dark';
30
+ } else {
31
+ // No explicit theme set, toggle from system preference
32
+ newTheme = prefersDark ? 'light' : 'dark';
33
+ }
34
+
35
+ html.setAttribute('data-theme', newTheme);
36
+ try {
37
+ localStorage.setItem('resque-theme', newTheme);
38
+ } catch (e) {}
39
+ return false;
40
+ });
41
+
4
42
  var relatizer = function(){
5
43
  var dt = $(this).text(), relatized = $.relatizeDate(this)
6
44
  if ($(this).parents("a").length > 0 || $(this).is("a")) {
@@ -11,10 +49,9 @@ $(function() {
11
49
  } else {
12
50
  $(this)
13
51
  .text('')
14
- .append( $('<a href="#" class="toggle_format" title="' + dt + '" />')
15
- .append('<span class="date_time">' + dt +
16
- '</span><span class="relatized_time">' +
17
- relatized + '</span>') )
52
+ .append( $('<a href="#" class="toggle_format" />').attr('title', dt)
53
+ .append( $('<span class="date_time" />').text(dt) )
54
+ .append( $('<span class="relatized_time" />').text(relatized) ) )
18
55
  }
19
56
  };
20
57
 
@@ -1,91 +1,201 @@
1
- html { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13px; }
1
+ :root {
2
+ --bg-primary: #efefef;
3
+ --bg-secondary: #fff;
4
+ --bg-header: #000;
5
+ --bg-header-hover: #333;
6
+ --bg-table-header: #efefef;
7
+ --bg-highlight: #efefef;
8
+ --bg-working: #f4ffe4;
9
+ --bg-failure: #ffecec;
10
+ --bg-queue-tag: #b1d2e9;
11
+ --bg-pagination: #efefef;
12
+ --text-pagination: #555;
13
+ --bg-failed-item: linear-gradient(to bottom, #efefef, #fff);
14
+ --bg-subnav-link: #dd5b5b;
15
+ --accent: #ce1212;
16
+ --accent-dark: #b00909;
17
+ --text-primary: #111;
18
+ --text-secondary: #888;
19
+ --text-muted: #999;
20
+ --text-header: #333;
21
+ --text-white: #fff;
22
+ --text-working: #7ac312;
23
+ --text-failure: #d37474;
24
+ --text-queue-tag: #3274a2;
25
+ --border-color: #ccc;
26
+ --border-footer: #ccc;
27
+ }
28
+
29
+ @media (prefers-color-scheme: dark) {
30
+ :root:not([data-theme="light"]) {
31
+ --bg-primary: #1a1a1a;
32
+ --bg-secondary: #2d2d2d;
33
+ --bg-header: #0d0d0d;
34
+ --bg-header-hover: #444;
35
+ --bg-table-header: #333;
36
+ --bg-highlight: #3a3a3a;
37
+ --bg-working: #1a2e0a;
38
+ --bg-failure: #3d1a1a;
39
+ --bg-queue-tag: #1a3a4d;
40
+ --bg-pagination: #333;
41
+ --text-pagination: #bbb;
42
+ --bg-failed-item: linear-gradient(to bottom, #333, #2d2d2d);
43
+ --bg-subnav-link: #8b3a3a;
44
+ --text-primary: #e0e0e0;
45
+ --text-secondary: #aaa;
46
+ --text-muted: #888;
47
+ --text-header: #ccc;
48
+ --text-working: #8fd14f;
49
+ --text-failure: #e88;
50
+ --text-queue-tag: #6ab0e0;
51
+ --border-color: #555;
52
+ --border-footer: #444;
53
+ }
54
+ }
55
+
56
+ [data-theme="dark"] {
57
+ --bg-primary: #1a1a1a;
58
+ --bg-secondary: #2d2d2d;
59
+ --bg-header: #0d0d0d;
60
+ --bg-header-hover: #444;
61
+ --bg-table-header: #333;
62
+ --bg-highlight: #3a3a3a;
63
+ --bg-working: #1a2e0a;
64
+ --bg-failure: #3d1a1a;
65
+ --bg-queue-tag: #1a3a4d;
66
+ --bg-pagination: #333;
67
+ --text-pagination: #bbb;
68
+ --bg-failed-item: linear-gradient(to bottom, #333, #2d2d2d);
69
+ --bg-subnav-link: #8b3a3a;
70
+ --text-primary: #e0e0e0;
71
+ --text-secondary: #aaa;
72
+ --text-muted: #888;
73
+ --text-header: #ccc;
74
+ --text-working: #8fd14f;
75
+ --text-failure: #e88;
76
+ --text-queue-tag: #6ab0e0;
77
+ --border-color: #555;
78
+ --border-footer: #444;
79
+ }
80
+
81
+ html { background: var(--bg-primary); font-family:Arial, Verdana, sans-serif; font-size:13px; }
2
82
  body { padding:0; margin:0; }
3
83
 
4
- .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #ce1212;}
5
- .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;}
84
+ .header { background: var(--bg-header); padding:8px 5% 0 5%; border-bottom:5px solid var(--accent); position: relative;}
85
+ .header h1 { color: var(--text-header); font-size:90%; font-weight:bold; margin-bottom:6px;}
6
86
  .header ul li { display:inline;}
7
- .header ul li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; }
8
- .header ul li a:hover { background:#333;}
9
- .header ul li.current a { background:#ce1212; font-weight:bold; color:#fff;}
87
+ .header ul li a { color: var(--text-white); text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; }
88
+ .header ul li a:hover { background: var(--bg-header-hover);}
89
+ .header ul li.current a { background: var(--accent); font-weight:bold; color: var(--text-white);}
10
90
 
11
- .header .namespace { position: absolute; right: 75px; top: 12px; color: #FFF; font-weight: bold; }
91
+ .header .namespace { position: absolute; right: 50px; top: 12px; color: var(--text-white); font-weight: bold; }
12
92
 
13
- .subnav { padding:2px 5% 7px 5%; background:#ce1212; font-size:90%;}
93
+ .subnav { padding:2px 5% 7px 5%; background: var(--accent); font-size:90%;}
14
94
  .subnav li { display:inline;}
15
- .subnav li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; background:#dd5b5b; padding:5px; -webkit-border-radius:3px; -moz-border-radius:3px;}
16
- .subnav li.current a { background:#fff; font-weight:bold; color:#ce1212;}
17
- .subnav li a:active { background:#b00909;}
95
+ .subnav li a { color: var(--text-white); text-decoration:none; margin-right:10px; display:inline-block; background: var(--bg-subnav-link); padding:5px; -webkit-border-radius:3px; -moz-border-radius:3px;}
96
+ .subnav li.current a { background: var(--bg-secondary); font-weight:bold; color: var(--accent);}
97
+ .subnav li a:active { background: var(--accent-dark);}
18
98
 
19
- #main { padding:10px 5%; background:#fff; overflow:hidden; }
99
+ #main { padding:10px 5%; background: var(--bg-secondary); overflow:hidden; }
20
100
  #main .logo { float:right; margin:10px;}
21
- #main span.hl { background:#efefef; padding:2px;}
22
- #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color:#ce1212;}
23
- #main h2 { margin:10px 0; font-size:130%;}
101
+ #main span.hl { background: var(--bg-highlight); padding:2px;}
102
+ #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color: var(--accent);}
103
+ #main h2 { margin:10px 0; font-size:130%; color: var(--text-primary);}
24
104
  #main table { width:100%; margin:10px 0;}
25
- #main table tr td, #main table tr th { border:1px solid #ccc; padding:6px;}
26
- #main table tr th { background:#efefef; color:#888; font-size:15px; font-weight:bold;}
27
- #main table tr td.no-data { text-align:center; padding:40px 0; color:#999; font-style:italic; font-size:130%;}
28
- #main a { color:#111;}
29
- #main p { margin:5px 0;}
30
- #main p.intro { margin-bottom:15px; font-size:85%; color:#999; margin-top:0; line-height:1.3;}
105
+ #main table tr td, #main table tr th { border:1px solid var(--border-color); padding:6px;}
106
+ #main table tr th { background: var(--bg-table-header); color: var(--text-secondary); font-size:15px; font-weight:bold;}
107
+ #main table tr td { background: var(--bg-secondary); color: var(--text-primary);}
108
+ #main table tr td.no-data { text-align:center; padding:40px 0; color: var(--text-muted); font-style:italic; font-size:130%;}
109
+ #main a { color: var(--text-primary);}
110
+ #main p { margin:5px 0; color: var(--text-primary);}
111
+ #main p.intro { margin-bottom:15px; font-size:85%; color: var(--text-muted); margin-top:0; line-height:1.3;}
31
112
  #main h1.wi { margin-bottom:5px;}
32
- #main p.sub { font-size:95%; color:#999;}
113
+ #main p.sub { font-size:95%; color: var(--text-muted);}
33
114
 
34
115
  #main table.queues { width:40%;}
35
116
  #main table.queues td.queue { font-weight:bold; width:50%;}
36
- #main table.queues tr.failure td { background:#ffecec; font-size:90%; color:#d37474;}
37
- #main table.queues tr.failure td a{ color:#d37474;}
38
- #main table.queues tr.first_failure td { border-top:2px solid #d37474; }
117
+ #main table.queues tr.failure td { background: var(--bg-failure); font-size:90%; color: var(--text-failure);}
118
+ #main table.queues tr.failure td a{ color: var(--text-failure);}
119
+ #main table.queues tr.first_failure td { border-top:2px solid var(--text-failure); }
39
120
 
40
121
  #main table.jobs td.class { font-family:Monaco, "Courier New", monospace; font-size:90%; width:50%;}
41
122
  #main table.jobs td.args{ width:50%;}
123
+ #main table.jobs td.remove-job { width:1%; text-align:right;}
124
+ #main table.jobs td.remove-job form { float:none; margin:0;}
42
125
 
43
- #main table.workers td.icon {width:1%; background:#efefef;text-align:center;}
126
+ #main table.workers td.icon {width:1%; background: var(--bg-highlight);text-align:center;}
44
127
  #main table.workers td.icon img { height: 16px; width: 16px; }
45
128
  #main table.workers td.where { width:25%;}
46
129
  #main table.workers td.queues { width:35%;}
47
- #main .queue-tag { background:#b1d2e9; padding:2px; margin:0 3px; font-size:80%; text-decoration:none; text-transform:uppercase; font-weight:bold; color:#3274a2; -webkit-border-radius:4px; -moz-border-radius:4px;}
48
- #main .queue-tag a { color: #3274A2; text-decoration: none; }
130
+ #main .queue-tag { background: var(--bg-queue-tag); padding:2px; margin:0 3px; font-size:80%; text-decoration:none; text-transform:uppercase; font-weight:bold; color: var(--text-queue-tag); -webkit-border-radius:4px; -moz-border-radius:4px;}
131
+ #main .queue-tag a { color: var(--text-queue-tag); text-decoration: none; }
49
132
  #main table.workers td.queues.queue { width:10%;}
50
133
  #main table.workers td.process { width:35%;}
51
- #main table.workers td.process span.waiting { color:#999; font-size:90%;}
134
+ #main table.workers td.process span.waiting { color: var(--text-muted); font-size:90%;}
52
135
  #main table.workers td.process small { font-size:80%; margin-left:5px;}
53
136
  #main table.workers td.process code { font-family:Monaco, "Courier New", monospace; font-size:90%;}
54
- #main table.workers td.process small a { color:#999;}
55
- #main.polling table.workers tr.working td { background:#f4ffe4; color:#7ac312;}
56
- #main.polling table.workers tr.working td.where a { color:#7ac312;}
137
+ #main table.workers td.process small a { color: var(--text-muted);}
138
+ #main.polling table.workers tr.working td { background: var(--bg-working); color: var(--text-working);}
139
+ #main.polling table.workers tr.working td.where a { color: var(--text-working);}
57
140
  #main.polling table.workers tr.working td.process code { font-weight:bold;}
58
141
 
59
142
 
60
- #main table.stats th { font-size:100%; width:40%; color:#000;}
61
- #main hr { border:0; border-top:5px solid #efefef; margin:15px 0;}
143
+ #main table.stats th { font-size:100%; width:40%; color: var(--text-primary);}
144
+ #main hr { border:0; border-top:5px solid var(--bg-highlight); margin:15px 0;}
62
145
 
63
- #footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;}
64
- #footer p a { color:#999;}
146
+ #footer { padding:10px 5%; background: var(--bg-primary); color: var(--text-muted); font-size:85%; line-height:1.5; border-top:5px solid var(--border-footer); padding-top:10px;}
147
+ #footer p a { color: var(--text-muted);}
65
148
 
66
149
  #main p.poll { background:url(poll.png) no-repeat 0 2px; padding:3px 0; padding-left:23px; float:right; font-size:85%; }
67
150
 
68
151
  #main ul.failed {}
69
- #main ul.failed li {background:-webkit-gradient(linear, left top, left bottom, from(#efefef), to(#fff)) #efefef; margin-top:10px; padding:10px; overflow:hidden; -webkit-border-radius:5px; border:1px solid #ccc; }
70
- #main ul.failed li dl dt {font-size:80%; color:#999; width:60px; float:left; padding-top:1px; text-align:right;}
71
- #main ul.failed li dl dd {margin-bottom:10px; margin-left:70px;}
152
+ #main ul.failed li {background: var(--bg-failed-item); background-color: var(--bg-highlight); margin-top:10px; padding:10px; overflow:hidden; -webkit-border-radius:5px; border:1px solid var(--border-color); }
153
+ #main ul.failed li dl dt {font-size:80%; color: var(--text-muted); width:60px; float:left; padding-top:1px; text-align:right;}
154
+ #main ul.failed li dl dd {margin-bottom:10px; margin-left:70px; color: var(--text-primary);}
72
155
  #main ul.failed li dl dd .retried { float:right; text-align: right; }
73
156
  #main ul.failed li dl dd .retried .remove { display:none; margin-top: 8px; }
74
157
  #main ul.failed li.hover dl dd .retried .remove { display:block; }
75
158
  #main ul.failed li dl dd .controls { display:none; float:right; }
76
159
  #main ul.failed li.hover dl dd .controls { display:block; }
77
- #main ul.failed li dl dd code, #main ul.failed li dl dd pre { font-family:Monaco, "Courier New", monospace; font-size:90%; white-space: pre-wrap;}
160
+ #main ul.failed li dl dd code, #main ul.failed li dl dd pre { font-family:Monaco, "Courier New", monospace; font-size:90%; white-space: pre-wrap; color: var(--text-primary);}
78
161
  #main ul.failed li dl dd.error a {font-family:Monaco, "Courier New", monospace; font-size:90%; }
79
162
  #main ul.failed li dl dd.error pre { margin-top:3px; line-height:1.3;}
80
163
 
81
- #main div.pagination { background:#efefef; padding:10px; text-align:center}
82
- #main div.pagination a, #main div.pagination span { display: inline-block; padding: 4px 8px; color: #555; border: solid 1px #555; margin-top: 6px; }
83
- #main div.pagination span { background: #555; color: white; }
164
+ #main div.pagination { background: var(--bg-pagination); padding:10px; text-align:center}
165
+ #main div.pagination a, #main div.pagination span { display: inline-block; padding: 4px 8px; color: var(--text-pagination); border: solid 1px var(--text-pagination); margin-top: 6px; }
166
+ #main div.pagination span { background: var(--text-pagination); color: var(--bg-secondary); }
84
167
 
85
168
  #main form {float:right; margin-top:-10px;margin-left:10px;}
86
169
 
87
170
  #main .time a.toggle_format {text-decoration:none;}
88
171
 
89
- #failed tr.total td {background-color: #FFECEC; color: #D37474; font-size: 15px; font-weight: bold;}
172
+ #failed tr.total td {background-color: var(--bg-failure); color: var(--text-failure); font-size: 15px; font-weight: bold;}
90
173
  #failed .center {text-align: center;}
91
174
  #failed .failed_class { padding-left: 20px; font-size:12px; }
175
+
176
+ /* Theme toggle button */
177
+ .theme-toggle {
178
+ position: absolute;
179
+ right: 15px;
180
+ top: 10px;
181
+ background: transparent;
182
+ border: none;
183
+ cursor: pointer;
184
+ padding: 5px;
185
+ font-size: 18px;
186
+ line-height: 1;
187
+ color: var(--text-white);
188
+ opacity: 0.8;
189
+ transition: opacity 0.2s;
190
+ }
191
+ .theme-toggle:hover {
192
+ opacity: 1;
193
+ }
194
+ .theme-toggle .sun { display: none; }
195
+ .theme-toggle .moon { display: inline; }
196
+ [data-theme="dark"] .theme-toggle .sun { display: inline; }
197
+ [data-theme="dark"] .theme-toggle .moon { display: none; }
198
+ @media (prefers-color-scheme: dark) {
199
+ :root:not([data-theme="light"]) .theme-toggle .sun { display: inline; }
200
+ :root:not([data-theme="light"]) .theme-toggle .moon { display: none; }
201
+ }
@@ -3,12 +3,12 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <title>Resque</title>
6
- <link href="<%=u 'reset.css' %>" media="screen" rel="stylesheet" type="text/css">
7
- <link href="<%=u 'style.css' %>" media="screen" rel="stylesheet" type="text/css">
8
- <script src="<%=u 'jquery-3.6.0.min.js' %>" type="text/javascript"></script>
9
- <script src="<%=u 'jquery.relatize_date.js' %>" type="text/javascript"></script>
10
- <script src="<%=u 'ranger.js' %>" type="text/javascript"></script>
11
- <script src="<%=u 'main.js' %>" type="text/javascript"></script>
6
+ <%= link_tag 'reset.css' %>
7
+ <%= link_tag 'style.css' %>
8
+ <%= script_tag 'jquery-3.6.0.min.js' %>
9
+ <%= script_tag 'jquery.relatize_date.js' %>
10
+ <%= script_tag 'ranger.js' %>
11
+ <%= script_tag 'main.js' %>
12
12
  </head>
13
13
  <body>
14
14
  <div class="header">
@@ -17,6 +17,10 @@
17
17
  <%= tab tab_name %>
18
18
  <% end %>
19
19
  </ul>
20
+ <button class="theme-toggle" title="Toggle dark/light mode" aria-label="Toggle dark/light mode">
21
+ <span class="sun">&#9728;</span>
22
+ <span class="moon">&#9790;</span>
23
+ </button>
20
24
  <% if Resque.redis.namespace != :resque %>
21
25
  <abbr class="namespace" title="Resque's Redis Namespace">
22
26
  <%= h Resque.redis.namespace %>
@@ -11,16 +11,23 @@
11
11
  <tr>
12
12
  <th>Class</th>
13
13
  <th>Args</th>
14
+ <th>&nbsp;</th>
14
15
  </tr>
15
16
  <% for job in (jobs = resque.peek(current_queue, start, 20)) %>
16
17
  <tr>
17
18
  <td class='class'><%= partial :job_class, :job => job %></td>
18
19
  <td class='args'><%=h job['args'].inspect %></td>
20
+ <td class='remove-job'>
21
+ <form method="POST" action="<%= h u("/queues/#{current_queue}/remove-job") %>">
22
+ <input type="hidden" name="payload" value="<%= h Resque.encode(job) %>" />
23
+ <input type="submit" name="" value="Delete" class="confirmSubmission" />
24
+ </form>
25
+ </td>
19
26
  </tr>
20
27
  <% end %>
21
28
  <% if jobs.empty? %>
22
29
  <tr>
23
- <td class='no-data' colspan='2'>There are no pending jobs in this queue</td>
30
+ <td class='no-data' colspan='3'>There are no pending jobs in this queue</td>
24
31
  </tr>
25
32
  <% end %>
26
33
  </table>
data/lib/resque/server.rb CHANGED
@@ -73,6 +73,21 @@ module Resque
73
73
  redirect u('queues')
74
74
  end
75
75
 
76
+ post "/queues/:id/remove-job" do
77
+ halt 400, 'Missing job payload' if params[:payload].to_s.empty?
78
+
79
+ begin
80
+ # Round-tripped so the value removed is encoded exactly the way the
81
+ # queue holds it, whatever the form sent.
82
+ payload = Resque.encode(Resque.decode(params[:payload]))
83
+ rescue Resque::Helpers::DecodeException
84
+ halt 400, 'Invalid job payload'
85
+ end
86
+
87
+ Resque.data_store.remove_from_queue(params[:id], payload)
88
+ redirect u("queues/#{params[:id]}")
89
+ end
90
+
76
91
  get "/failed/?" do
77
92
  if Resque::Failure.url
78
93
  redirect Resque::Failure.url
@@ -18,6 +18,14 @@ module Resque
18
18
  end
19
19
  alias_method :u, :url_path
20
20
 
21
+ def script_tag(src)
22
+ %Q(<script src="#{url_path(src)}" type="text/javascript"></script>)
23
+ end
24
+
25
+ def link_tag(src)
26
+ %Q(<link href="#{url_path(src)}" media="screen" rel="stylesheet" type="text/css">)
27
+ end
28
+
21
29
  def path_prefix
22
30
  request.env['SCRIPT_NAME']
23
31
  end
@@ -150,7 +158,7 @@ module Resque
150
158
 
151
159
  def failed_end_at
152
160
  if failed_start_at + failed_per_page > failed_size
153
- failed_size
161
+ failed_size - 1
154
162
  else
155
163
  failed_start_at + failed_per_page - 1
156
164
  end
data/lib/resque/tasks.rb CHANGED
@@ -41,15 +41,8 @@ namespace :resque do
41
41
  threads.each { |thread| thread.join }
42
42
  end
43
43
 
44
- # Preload app files if this is Rails
45
- task :preload => :setup do
46
- if defined?(Rails) && Rails.respond_to?(:application)
47
- if Rails.application.config.eager_load
48
- ActiveSupport.run_load_hooks(:before_eager_load, Rails.application)
49
- Rails.application.config.eager_load_namespaces.each(&:eager_load!)
50
- end
51
- end
52
- end
44
+ # Defined for backwards compatibility.
45
+ task :preload => :setup
53
46
 
54
47
  namespace :failures do
55
48
  desc "Sort the 'failed' queue for the redis_multi_queue failure backend"
@@ -1,3 +1,3 @@
1
1
  module Resque
2
- VERSION = '3.0.2'
2
+ VERSION = '3.1.0'
3
3
  end
@@ -156,8 +156,8 @@ module Resque
156
156
  end
157
157
  end
158
158
 
159
- def uri_open(*args)
160
- (RbConfig::CONFIG['ruby_version'] < '2.7') ? open(*args) : URI.open(*args)
159
+ def uri_open(url, *args)
160
+ URI.parse(url).open(*args)
161
161
  end
162
162
 
163
163
  def write_url
@@ -226,8 +226,8 @@ module Resque
226
226
 
227
227
  def launch!(specific_url = nil, path = nil)
228
228
  return if options[:skip_launch]
229
- cmd = WINDOWS ? "start" : "open"
230
- system "#{cmd} #{specific_url || url}#{path}"
229
+ target = "#{specific_url || url}#{path}"
230
+ WINDOWS ? system("rundll32", "url.dll,FileProtocolHandler", target) : system("open", target)
231
231
  end
232
232
 
233
233
  def kill!
data/lib/resque/worker.rb CHANGED
@@ -553,7 +553,15 @@ module Resque
553
553
 
554
554
  @heartbeat_thread = Thread.new do
555
555
  loop do
556
- heartbeat!
556
+ begin
557
+ heartbeat!
558
+ rescue => e
559
+ # A raise here would kill the thread and stop the worker
560
+ # heartbeating, but the worker itself keeps running and is then
561
+ # pruned out from under itself.
562
+ log_with_severity :error, "Failed to heartbeat: #{e.class}: #{e.message}"
563
+ end
564
+
557
565
  signaled = @heartbeat_thread_signal.wait_for_signal(Resque.heartbeat_interval)
558
566
  break if signaled
559
567
  end
data/lib/resque.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'mono_logger'
2
2
  require 'redis/namespace'
3
3
  require 'forwardable'
4
+ require 'securerandom'
4
5
 
5
6
  require 'resque/version'
6
7
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Wanstrath
8
8
  - Steve Klabnik
9
9
  - Terence Lee
10
10
  - Michael Bianco
11
+ - Patrick Tulskie
11
12
  bindir: bin
12
13
  cert_chain: []
13
14
  date: 1980-01-02 00:00:00.000000000 Z
@@ -153,7 +154,9 @@ description: |2
153
154
  * A Ruby library for creating, querying, and processing jobs
154
155
  * A Rake task for starting a worker which processes jobs
155
156
  * A Sinatra app for monitoring queues, jobs, and workers.
156
- email: steve@steveklabnik.com
157
+ email:
158
+ - steve@steveklabnik.com
159
+ - PatrickTulskie@gmail.com
157
160
  executables:
158
161
  - resque
159
162
  - resque-web
@@ -236,14 +239,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
236
239
  requirements:
237
240
  - - ">="
238
241
  - !ruby/object:Gem::Version
239
- version: 3.0.0
242
+ version: 3.2.0
240
243
  required_rubygems_version: !ruby/object:Gem::Requirement
241
244
  requirements:
242
245
  - - ">="
243
246
  - !ruby/object:Gem::Version
244
247
  version: '0'
245
248
  requirements: []
246
- rubygems_version: 4.0.6
249
+ rubygems_version: 4.0.16
247
250
  specification_version: 4
248
251
  summary: Resque is a Redis-backed queueing system.
249
252
  test_files: []