sidekiq-failures 0.4.5 → 1.0.2
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 +5 -5
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +37 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +6 -0
- data/README.md +4 -2
- data/lib/sidekiq/failures/locales/ja.yml +6 -0
- data/lib/sidekiq/failures/locales/pt-BR.yml +5 -0
- data/lib/sidekiq/failures/locales/zh-cn.yml +5 -0
- data/lib/sidekiq/failures/middleware.rb +3 -2
- data/lib/sidekiq/failures/version.rb +1 -1
- data/lib/sidekiq/failures/views/failures.erb +14 -14
- data/lib/sidekiq/failures/web_extension.rb +12 -1
- data/lib/sidekiq/failures.rb +18 -4
- data/sidekiq-failures.gemspec +10 -3
- data/test/failures_test.rb +16 -0
- data/test/middleware_test.rb +58 -64
- data/test/test_helper.rb +3 -5
- data/test/web_extension_test.rb +84 -63
- metadata +31 -15
- data/.travis.yml +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 19188851daa2c09c4d9fd539cdc5ebf1dba9ef99d8c9ed0e2c9ef3840ed0141e
|
4
|
+
data.tar.gz: 16b650f8e29fce5be5b384133914c10bbe95181c963a55cfb0f75f8ddb177091
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ed7905bc899029c186d54ea9161b167d6e2477ba4c17da2b19b8f243903f5ae4c1ee2004f17d63b0c57adee458c963c33408724753e37d1442f191b84fe0b3a
|
7
|
+
data.tar.gz: 85b87d7dfc6f41e4d4cb1f3b3c0cfcc1f02ed97358c7b0479facc625003ef401bf20669868914715185e242c3bbded6c0262d125e1915ca723304c1629e4adba
|
@@ -0,0 +1,37 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
fail-fast: false
|
14
|
+
matrix:
|
15
|
+
ruby: [2.6, 2.7, '3.0', 3.1]
|
16
|
+
sidekiq: [4.2, 5.2, 6.2]
|
17
|
+
services:
|
18
|
+
redis:
|
19
|
+
image: redis
|
20
|
+
options: >-
|
21
|
+
--health-cmd "redis-cli ping"
|
22
|
+
--health-interval 10s
|
23
|
+
--health-timeout 5s
|
24
|
+
--health-retries 5
|
25
|
+
ports:
|
26
|
+
- 6379:6379
|
27
|
+
env:
|
28
|
+
SIDEKIQ_VERSION: ~> ${{ matrix.sidekiq }}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v3
|
31
|
+
- name: Set up Ruby
|
32
|
+
uses: ruby/setup-ruby@v1
|
33
|
+
with:
|
34
|
+
bundler-cache: true # 'bundle install' and cache gems
|
35
|
+
ruby-version: ${{ matrix.ruby }}
|
36
|
+
- name: Run tests
|
37
|
+
run: bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
|
+
## 1.0.2
|
4
|
+
|
5
|
+
* Pass now required argument to Sidekiq's JobRetry.new (#140 @mcasper)
|
6
|
+
|
7
|
+
## 1.0.1
|
8
|
+
|
9
|
+
* Add license config to the gemspec (#115 @reiz)
|
10
|
+
* Guard against failure error_message being `nil` (#122 @mcasper)
|
11
|
+
* Fix filtering failures with 0 results on Sidekiq Pro (#125 @substars)
|
12
|
+
|
13
|
+
## 1.0.0
|
14
|
+
|
15
|
+
* WebUI improvements (@bbtfr)
|
16
|
+
* Use ActiveJob to display info when available (@dreyks)
|
17
|
+
* New locales (@ryohashimoto @gurgelrenan)
|
18
|
+
* Sidekiq 4 and higher compatibility (@davekrupinski )
|
19
|
+
* Sidekiq 5 fixes (@fidelisrafael)
|
20
|
+
|
3
21
|
## 0.4.5
|
4
22
|
|
5
23
|
* Limit error message to 500 symbols (@antonzaytsev)
|
data/Gemfile
CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
gem 'sidekiq', ENV['SIDEKIQ_VERSION'] if ENV['SIDEKIQ_VERSION']
|
7
|
+
|
8
|
+
# to test Pro-specific functionality, set SIDEKIQ_PRO_CREDS on `bundle install`
|
9
|
+
# and SIDEKIQ_PRO_VERSION on `bundle install` and `rake test`
|
10
|
+
source "https://#{ENV['SIDEKIQ_PRO_CREDS']}@enterprise.contribsys.com/" do
|
11
|
+
gem 'sidekiq-pro', ENV['SIDEKIQ_PRO_VERSION'] if ENV['SIDEKIQ_PRO_VERSION']
|
12
|
+
end if ENV['SIDEKIQ_PRO_VERSION']
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Sidekiq::Failures
|
1
|
+
# Sidekiq::Failures 
|
2
2
|
|
3
3
|
Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you browse
|
4
4
|
them. Makes use of Sidekiq's custom tabs and middleware chain.
|
@@ -20,6 +20,8 @@ gem 'sidekiq-failures'
|
|
20
20
|
Simply having the gem in your Gemfile is enough to get you started. Your failed
|
21
21
|
jobs will be visible via a Failures tab in the Web UI.
|
22
22
|
|
23
|
+
If you have not previously used the Web UI, it is a part of the Sidekiq gem. [See Sidekiq's docs about Web UI here.](https://github.com/mperham/sidekiq/wiki/Monitoring#web-ui)
|
24
|
+
|
23
25
|
## Configuring
|
24
26
|
|
25
27
|
### Maximum Tracked Failures
|
@@ -136,7 +138,7 @@ Sidekiq::Failures.reset_failures
|
|
136
138
|
|
137
139
|
## Dependencies
|
138
140
|
|
139
|
-
Depends on Sidekiq >=
|
141
|
+
Depends on Sidekiq >= 4.0.0
|
140
142
|
|
141
143
|
## Contributing
|
142
144
|
|
@@ -80,11 +80,12 @@ module Sidekiq
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def retry_middleware
|
83
|
-
@retry_middleware ||=
|
83
|
+
@retry_middleware ||=
|
84
|
+
Sidekiq::Failures.retry_middleware_class.new(@config || {})
|
84
85
|
end
|
85
86
|
|
86
87
|
def default_max_retries
|
87
|
-
Sidekiq::
|
88
|
+
Sidekiq::Failures.retry_middleware_class::DEFAULT_MAX_RETRY_ATTEMPTS
|
88
89
|
end
|
89
90
|
|
90
91
|
def hostname
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<div class="col-sm-5">
|
3
3
|
<h3><%= t('FailedJobs') %></h3>
|
4
4
|
</div>
|
5
|
-
<% if @failures.
|
5
|
+
<% if @failures.count > 0 && @total_size > @count %>
|
6
6
|
<div class="col-sm-4">
|
7
7
|
<%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
|
8
8
|
</div>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
<%= filtering('failures') if respond_to?(:filtering) %>
|
11
11
|
</header>
|
12
12
|
|
13
|
-
<% if @failures.
|
13
|
+
<% if @failures.count > 0 %>
|
14
14
|
<form action="<%= root_path %>failures" method="post">
|
15
15
|
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
16
16
|
<table class="table table-striped table-bordered table-white">
|
@@ -21,11 +21,11 @@
|
|
21
21
|
<input type="checkbox" class="check_all" />
|
22
22
|
</label>
|
23
23
|
</th>
|
24
|
-
<th
|
25
|
-
<th
|
26
|
-
<th
|
27
|
-
<th
|
28
|
-
<th
|
24
|
+
<th><%= t('FailedAt') %></th>
|
25
|
+
<th><%= t('Queue') %></th>
|
26
|
+
<th><%= t('Worker') %></th>
|
27
|
+
<th><%= t('Arguments') %></th>
|
28
|
+
<th><%= t('Error') %></th>
|
29
29
|
</tr>
|
30
30
|
</thead>
|
31
31
|
<% @failures.each do |entry| %>
|
@@ -35,17 +35,17 @@
|
|
35
35
|
<input type='checkbox' name='key[]' value='<%= job_params(entry.item, entry.score) %>' />
|
36
36
|
</label>
|
37
37
|
</td>
|
38
|
-
<td><a href="<%= root_path %>failures/<%= job_params(entry.item, entry.score) %>"><%= entry
|
38
|
+
<td><a href="<%= root_path %>failures/<%= job_params(entry.item, entry.score) %>"><%= safe_relative_time(entry['failed_at']) %></a></td>
|
39
39
|
<td>
|
40
|
-
<
|
40
|
+
<a href="<%= root_path %>queues/<%= entry.queue %>"><%= entry.queue %></a>
|
41
41
|
</td>
|
42
|
+
<td><%= entry.respond_to?(:display_class) ? entry.display_class : entry.klass %></td>
|
42
43
|
<td>
|
43
|
-
<
|
44
|
+
<div class="args"><%= display_args(entry.respond_to?(:display_args) ? entry.display_args : entry.args) %></div>
|
44
45
|
</td>
|
45
|
-
<td><%= safe_relative_time(entry['failed_at']) %></td>
|
46
46
|
<td style="overflow: auto; padding: 10px;">
|
47
47
|
<a class="backtrace" href="#" onclick="$(this).next().toggle(); return false">
|
48
|
-
<%= h entry['error_class'] %>: <%= h entry['error_message'].size > 500 ? entry['error_message'][0..500] + '...' : entry['error_message'] %>
|
48
|
+
<%= h entry['error_class'] %>: <%= h entry['error_message'].to_s.size > 500 ? entry['error_message'][0..500] + '...' : entry['error_message'] %>
|
49
49
|
</a>
|
50
50
|
<pre style="display: none; background: none; border: 0; width: 100%; max-height: 30em; font-size: 0.8em; white-space: nowrap; overflow: auto;">
|
51
51
|
<%= entry['error_backtrace'].join("<br />") if entry['error_backtrace'] %>
|
@@ -70,7 +70,7 @@
|
|
70
70
|
<input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('RetryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
71
71
|
</form>
|
72
72
|
|
73
|
-
<% if @failures.
|
73
|
+
<% if @failures.count > 0 && @total_size > @count %>
|
74
74
|
<div class="col-sm-4">
|
75
75
|
<%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
|
76
76
|
</div>
|
@@ -81,5 +81,5 @@
|
|
81
81
|
<% end %>
|
82
82
|
<form action="<%= root_path %>failures/all/reset" method="post">
|
83
83
|
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
84
|
-
<input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('
|
84
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('ResetCounter') %>" data-confirm="<%= t('AreYouSure') %>" />
|
85
85
|
</form>
|
@@ -19,7 +19,7 @@ module Sidekiq
|
|
19
19
|
|
20
20
|
app.get "/failures" do
|
21
21
|
@count = (params[:count] || 25).to_i
|
22
|
-
(@current_page, @total_size, @failures) = page(LIST_KEY, params[:page], @count)
|
22
|
+
(@current_page, @total_size, @failures) = page(LIST_KEY, params[:page], @count, :reverse => true)
|
23
23
|
@failures = @failures.map {|msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
24
24
|
|
25
25
|
render(:erb, File.read(File.join(view_path, "failures.erb")))
|
@@ -78,6 +78,17 @@ module Sidekiq
|
|
78
78
|
FailureSet.new.retry_all_failures
|
79
79
|
redirect "#{root_path}failures"
|
80
80
|
end
|
81
|
+
|
82
|
+
app.get '/filter/failures' do
|
83
|
+
redirect "#{root_path}failures"
|
84
|
+
end
|
85
|
+
|
86
|
+
app.post '/filter/failures' do
|
87
|
+
@failures = Sidekiq::Failures::FailureSet.new.scan("*#{params[:substr]}*")
|
88
|
+
@current_page = 1
|
89
|
+
@count = @total_size = @failures.count
|
90
|
+
render(:erb, File.read(File.join(view_path, "failures.erb")))
|
91
|
+
end
|
81
92
|
end
|
82
93
|
end
|
83
94
|
end
|
data/lib/sidekiq/failures.rb
CHANGED
@@ -5,6 +5,7 @@ rescue LoadError
|
|
5
5
|
end
|
6
6
|
|
7
7
|
require "sidekiq/api"
|
8
|
+
require "sidekiq/version"
|
8
9
|
require "sidekiq/failures/version"
|
9
10
|
require "sidekiq/failures/sorted_entry"
|
10
11
|
require "sidekiq/failures/failure_set"
|
@@ -47,9 +48,11 @@ module Sidekiq
|
|
47
48
|
|
48
49
|
# Fetches the failures max count value
|
49
50
|
def self.failures_max_count
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
if !instance_variable_defined?(:@failures_max_count) || @failures_max_count.nil?
|
52
|
+
1000
|
53
|
+
else
|
54
|
+
@failures_max_count
|
55
|
+
end
|
53
56
|
end
|
54
57
|
|
55
58
|
module Failures
|
@@ -62,12 +65,23 @@ module Sidekiq
|
|
62
65
|
def self.count
|
63
66
|
Sidekiq.redis {|r| r.zcard(LIST_KEY) }
|
64
67
|
end
|
68
|
+
|
69
|
+
def self.retry_middleware_class
|
70
|
+
if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('5.0.0')
|
71
|
+
require 'sidekiq/job_retry'
|
72
|
+
Sidekiq::JobRetry
|
73
|
+
else
|
74
|
+
require 'sidekiq/middleware/server/retry_jobs'
|
75
|
+
Sidekiq::Middleware::Server::RetryJobs
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
65
79
|
end
|
66
80
|
end
|
67
81
|
|
68
82
|
Sidekiq.configure_server do |config|
|
69
83
|
config.server_middleware do |chain|
|
70
|
-
chain.insert_before Sidekiq::
|
84
|
+
chain.insert_before Sidekiq::Failures.retry_middleware_class,
|
71
85
|
Sidekiq::Failures::Middleware
|
72
86
|
end
|
73
87
|
end
|
data/sidekiq-failures.gemspec
CHANGED
@@ -7,16 +7,23 @@ Gem::Specification.new do |gem|
|
|
7
7
|
gem.description = %q{Keep track of Sidekiq failed jobs}
|
8
8
|
gem.summary = %q{Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you browse them. Makes use of Sidekiq's custom tabs and middleware chain.}
|
9
9
|
gem.homepage = "https://github.com/mhfs/sidekiq-failures/"
|
10
|
+
gem.license = "MIT"
|
10
11
|
|
11
12
|
gem.files = `git ls-files`.split($\)
|
12
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
13
|
gem.name = "sidekiq-failures"
|
15
14
|
gem.require_paths = ["lib"]
|
16
15
|
gem.version = Sidekiq::Failures::VERSION
|
17
16
|
|
18
|
-
gem.add_dependency "sidekiq", ">=
|
17
|
+
gem.add_dependency "sidekiq", ">= 4.0.0"
|
19
18
|
|
19
|
+
# Redis 4.X is incompatible with Ruby < 2.3, but the Ruby version constraint
|
20
|
+
# wasn't added until 4.1.2, meaning you can get an incompatible version of the
|
21
|
+
# redis gem when running Ruby 2.2 without this constraint.
|
22
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.3.0")
|
23
|
+
gem.add_dependency "redis", "< 4.0"
|
24
|
+
end
|
25
|
+
|
26
|
+
gem.add_development_dependency "minitest"
|
20
27
|
gem.add_development_dependency "rake"
|
21
28
|
gem.add_development_dependency "rack-test"
|
22
29
|
gem.add_development_dependency "sprockets"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
describe Failures do
|
5
|
+
describe '.retry_middleware_class' do
|
6
|
+
it 'returns based on Sidekiq::VERISON' do
|
7
|
+
case Sidekiq::VERSION[0]
|
8
|
+
when '5'
|
9
|
+
assert_equal Failures.retry_middleware_class, Sidekiq::JobRetry
|
10
|
+
when '4'
|
11
|
+
assert_equal Failures.retry_middleware_class, Sidekiq::Middleware::Server::RetryJobs
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/test/middleware_test.rb
CHANGED
@@ -1,19 +1,72 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
+
class SidekiqPre6
|
4
|
+
def new_processor(boss)
|
5
|
+
num_options_calls.times { boss.expect(:options, {:queues => ['default'] }, []) }
|
6
|
+
::Sidekiq::Processor.new(boss)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def num_options_calls
|
12
|
+
if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('5.0.3')
|
13
|
+
3
|
14
|
+
else
|
15
|
+
2
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class SidekiqPre63
|
21
|
+
def new_processor(boss)
|
22
|
+
opts = {
|
23
|
+
queues: ['default'],
|
24
|
+
}
|
25
|
+
opts[:fetch] = Sidekiq::BasicFetch.new(opts)
|
26
|
+
::Sidekiq::Processor.new(boss, opts)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class SidekiqPost63
|
31
|
+
def new_processor(boss)
|
32
|
+
config = Sidekiq
|
33
|
+
config[:queues] = ['default']
|
34
|
+
config[:fetch] = Sidekiq::BasicFetch.new(config)
|
35
|
+
config[:error_handlers] << Sidekiq.method(:default_error_handler)
|
36
|
+
::Sidekiq::Processor.new(config) { |processor, reason = nil| }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
3
40
|
module Sidekiq
|
4
41
|
module Failures
|
5
42
|
describe "Middleware" do
|
43
|
+
def new_provider
|
44
|
+
version = Gem::Version.new(Sidekiq::VERSION)
|
45
|
+
if version >= Gem::Version.new('6.4.0')
|
46
|
+
SidekiqPost63
|
47
|
+
elsif version >= Gem::Version.new('6.0')
|
48
|
+
SidekiqPre63
|
49
|
+
else
|
50
|
+
SidekiqPre6
|
51
|
+
end.new
|
52
|
+
end
|
53
|
+
|
6
54
|
before do
|
7
|
-
Celluloid.boot
|
8
55
|
$invokes = 0
|
9
56
|
@boss = MiniTest::Mock.new
|
10
|
-
@
|
57
|
+
@provider = new_provider
|
58
|
+
@processor = @provider.new_processor(@boss)
|
59
|
+
|
11
60
|
Sidekiq.server_middleware {|chain| chain.add Sidekiq::Failures::Middleware }
|
12
61
|
Sidekiq.redis = REDIS
|
13
62
|
Sidekiq.redis { |c| c.flushdb }
|
14
63
|
Sidekiq.instance_eval { @failures_default_mode = nil }
|
15
64
|
end
|
16
65
|
|
66
|
+
after do
|
67
|
+
@boss.verify
|
68
|
+
end
|
69
|
+
|
17
70
|
TestException = Class.new(Exception)
|
18
71
|
ShutdownException = Class.new(Sidekiq::Shutdown)
|
19
72
|
|
@@ -42,11 +95,6 @@ module Sidekiq
|
|
42
95
|
|
43
96
|
assert_equal 0, failures_count
|
44
97
|
|
45
|
-
actor = MiniTest::Mock.new
|
46
|
-
actor.expect(:processor_done, nil, [@processor])
|
47
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
48
|
-
2.times { @boss.expect(:async, actor, []) }
|
49
|
-
|
50
98
|
assert_raises TestException do
|
51
99
|
@processor.process(msg)
|
52
100
|
end
|
@@ -60,11 +108,6 @@ module Sidekiq
|
|
60
108
|
|
61
109
|
assert_equal 0, failures_count
|
62
110
|
|
63
|
-
actor = MiniTest::Mock.new
|
64
|
-
actor.expect(:processor_done, nil, [@processor])
|
65
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
66
|
-
2.times { @boss.expect(:async, actor, []) }
|
67
|
-
|
68
111
|
assert_raises TestException do
|
69
112
|
@processor.process(msg)
|
70
113
|
end
|
@@ -78,13 +121,7 @@ module Sidekiq
|
|
78
121
|
|
79
122
|
assert_equal 0, failures_count
|
80
123
|
|
81
|
-
actor = MiniTest::Mock.new
|
82
|
-
actor.expect(:processor_done, nil, [@processor])
|
83
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
84
|
-
2.times { @boss.expect(:async, actor, []) }
|
85
|
-
|
86
124
|
@processor.process(msg)
|
87
|
-
@boss.verify
|
88
125
|
|
89
126
|
assert_equal 0, failures_count
|
90
127
|
assert_equal 1, $invokes
|
@@ -95,11 +132,6 @@ module Sidekiq
|
|
95
132
|
|
96
133
|
assert_equal 0, failures_count
|
97
134
|
|
98
|
-
actor = MiniTest::Mock.new
|
99
|
-
actor.expect(:processor_done, nil, [@processor])
|
100
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
101
|
-
2.times { @boss.expect(:async, actor, []) }
|
102
|
-
|
103
135
|
assert_raises TestException do
|
104
136
|
@processor.process(msg)
|
105
137
|
end
|
@@ -115,11 +147,6 @@ module Sidekiq
|
|
115
147
|
|
116
148
|
assert_equal 0, failures_count
|
117
149
|
|
118
|
-
actor = MiniTest::Mock.new
|
119
|
-
actor.expect(:processor_done, nil, [@processor])
|
120
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
121
|
-
2.times { @boss.expect(:async, actor, []) }
|
122
|
-
|
123
150
|
assert_raises TestException do
|
124
151
|
@processor.process(msg)
|
125
152
|
end
|
@@ -134,11 +161,6 @@ module Sidekiq
|
|
134
161
|
|
135
162
|
assert_equal 0, failures_count
|
136
163
|
|
137
|
-
actor = MiniTest::Mock.new
|
138
|
-
actor.expect(:processor_done, nil, [@processor])
|
139
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
140
|
-
2.times { @boss.expect(:async, actor, []) }
|
141
|
-
|
142
164
|
assert_raises TestException do
|
143
165
|
@processor.process(msg)
|
144
166
|
end
|
@@ -152,11 +174,6 @@ module Sidekiq
|
|
152
174
|
|
153
175
|
assert_equal 0, failures_count
|
154
176
|
|
155
|
-
actor = MiniTest::Mock.new
|
156
|
-
actor.expect(:processor_done, nil, [@processor])
|
157
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
158
|
-
2.times { @boss.expect(:async, actor, []) }
|
159
|
-
|
160
177
|
assert_raises TestException do
|
161
178
|
@processor.process(msg)
|
162
179
|
end
|
@@ -170,11 +187,6 @@ module Sidekiq
|
|
170
187
|
|
171
188
|
assert_equal 0, failures_count
|
172
189
|
|
173
|
-
actor = MiniTest::Mock.new
|
174
|
-
actor.expect(:processor_done, nil, [@processor])
|
175
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
176
|
-
2.times { @boss.expect(:async, actor, []) }
|
177
|
-
|
178
190
|
assert_raises TestException do
|
179
191
|
@processor.process(msg)
|
180
192
|
end
|
@@ -190,11 +202,6 @@ module Sidekiq
|
|
190
202
|
|
191
203
|
assert_equal 0, failures_count
|
192
204
|
|
193
|
-
actor = MiniTest::Mock.new
|
194
|
-
actor.expect(:processor_done, nil, [@processor])
|
195
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
196
|
-
2.times { @boss.expect(:async, actor, []) }
|
197
|
-
|
198
205
|
assert_raises TestException do
|
199
206
|
@processor.process(msg)
|
200
207
|
end
|
@@ -210,11 +217,6 @@ module Sidekiq
|
|
210
217
|
|
211
218
|
assert_equal 0, failures_count
|
212
219
|
|
213
|
-
actor = MiniTest::Mock.new
|
214
|
-
actor.expect(:processor_done, nil, [@processor])
|
215
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
216
|
-
2.times { @boss.expect(:async, actor, []) }
|
217
|
-
|
218
220
|
assert_raises TestException do
|
219
221
|
@processor.process(msg)
|
220
222
|
end
|
@@ -233,16 +235,13 @@ module Sidekiq
|
|
233
235
|
|
234
236
|
3.times do
|
235
237
|
boss = MiniTest::Mock.new
|
236
|
-
processor =
|
237
|
-
|
238
|
-
actor = MiniTest::Mock.new
|
239
|
-
actor.expect(:processor_done, nil, [processor])
|
240
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
241
|
-
2.times { boss.expect(:async, actor, []) }
|
238
|
+
processor = @provider.new_processor(boss)
|
242
239
|
|
243
240
|
assert_raises TestException do
|
244
241
|
processor.process(msg)
|
245
242
|
end
|
243
|
+
|
244
|
+
boss.verify
|
246
245
|
end
|
247
246
|
|
248
247
|
assert_equal 2, failures_count
|
@@ -259,11 +258,6 @@ module Sidekiq
|
|
259
258
|
|
260
259
|
assert_equal 0, Sidekiq::Failures.count
|
261
260
|
|
262
|
-
actor = MiniTest::Mock.new
|
263
|
-
actor.expect(:processor_done, nil, [@processor])
|
264
|
-
actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
|
265
|
-
@boss.expect(:async, actor, [])
|
266
|
-
|
267
261
|
assert_raises TestException do
|
268
262
|
@processor.process(msg)
|
269
263
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
Encoding.default_internal = Encoding::UTF_8
|
1
|
+
$TESTING = true
|
3
2
|
|
4
3
|
require "minitest/autorun"
|
5
4
|
require "minitest/spec"
|
@@ -7,14 +6,13 @@ require "minitest/mock"
|
|
7
6
|
|
8
7
|
require "rack/test"
|
9
8
|
|
10
|
-
require "celluloid"
|
11
9
|
require "sidekiq"
|
10
|
+
require "sidekiq-pro" if ENV['SIDEKIQ_PRO_VERSION']
|
12
11
|
require "sidekiq-failures"
|
13
12
|
require "sidekiq/processor"
|
14
13
|
require "sidekiq/fetch"
|
15
14
|
require "sidekiq/cli"
|
16
15
|
|
17
|
-
Celluloid.logger = nil
|
18
16
|
Sidekiq.logger.level = Logger::ERROR
|
19
17
|
|
20
|
-
REDIS = Sidekiq::RedisConnection.create(:
|
18
|
+
REDIS = Sidekiq::RedisConnection.create(url: "redis://localhost/15")
|
data/test/web_extension_test.rb
CHANGED
@@ -5,11 +5,15 @@ module Sidekiq
|
|
5
5
|
describe "WebExtension" do
|
6
6
|
include Rack::Test::Methods
|
7
7
|
|
8
|
+
TOKEN = SecureRandom.base64(32).freeze
|
9
|
+
|
8
10
|
def app
|
9
11
|
Sidekiq::Web
|
10
12
|
end
|
11
13
|
|
12
14
|
before do
|
15
|
+
env 'rack.session', { csrf: TOKEN }
|
16
|
+
env 'HTTP_X_CSRF_TOKEN', TOKEN
|
13
17
|
Sidekiq.redis = REDIS
|
14
18
|
Sidekiq.redis {|c| c.flushdb }
|
15
19
|
end
|
@@ -17,22 +21,22 @@ module Sidekiq
|
|
17
21
|
it 'can display home with failures tab' do
|
18
22
|
get '/'
|
19
23
|
|
20
|
-
last_response.status.must_equal 200
|
21
|
-
last_response.body.must_match
|
22
|
-
last_response.body.must_match
|
24
|
+
_(last_response.status).must_equal 200
|
25
|
+
_(last_response.body).must_match(/Sidekiq/)
|
26
|
+
_(last_response.body).must_match(/Failures/)
|
23
27
|
end
|
24
28
|
|
25
29
|
it 'can display failures page without any failures' do
|
26
30
|
get '/failures'
|
27
|
-
last_response.status.must_equal 200
|
28
|
-
last_response.body.must_match
|
29
|
-
last_response.body.must_match
|
31
|
+
_(last_response.status).must_equal 200
|
32
|
+
_(last_response.body).must_match(/Failed Jobs/)
|
33
|
+
_(last_response.body).must_match(/No failed jobs found/)
|
30
34
|
end
|
31
35
|
|
32
36
|
it 'has the reset counter form and action' do
|
33
37
|
get '/failures'
|
34
|
-
last_response.body.must_match
|
35
|
-
last_response.body.must_match
|
38
|
+
_(last_response.body).must_match(/failures\/all\/reset/)
|
39
|
+
_(last_response.body).must_match(/Reset Counter/)
|
36
40
|
end
|
37
41
|
|
38
42
|
describe 'when there are failures' do
|
@@ -42,97 +46,105 @@ module Sidekiq
|
|
42
46
|
end
|
43
47
|
|
44
48
|
it 'should be successful' do
|
45
|
-
last_response.status.must_equal 200
|
49
|
+
_(last_response.status).must_equal 200
|
46
50
|
end
|
47
51
|
|
48
52
|
it 'can display failures page with failures listed' do
|
49
|
-
last_response.body.must_match
|
50
|
-
last_response.body.must_match
|
51
|
-
last_response.body.must_match
|
52
|
-
last_response.body.wont_match
|
53
|
+
_(last_response.body).must_match(/Failed Jobs/)
|
54
|
+
_(last_response.body).must_match(/HardWorker/)
|
55
|
+
_(last_response.body).must_match(/ArgumentError/)
|
56
|
+
_(last_response.body).wont_match(/No failed jobs found/)
|
53
57
|
end
|
54
58
|
|
55
59
|
it 'can reset counter' do
|
56
60
|
assert_equal failed_count, "1"
|
57
61
|
|
58
|
-
last_response.body.must_match
|
62
|
+
_(last_response.body).must_match(/HardWorker/)
|
59
63
|
|
60
64
|
post '/failures/all/reset'
|
61
|
-
last_response.status.must_equal 302
|
62
|
-
last_response.location.must_match
|
65
|
+
_(last_response.status).must_equal 302
|
66
|
+
_(last_response.location).must_match(/failures$/)
|
63
67
|
|
64
68
|
get '/failures'
|
65
|
-
last_response.status.must_equal 200
|
66
|
-
last_response.body.must_match
|
69
|
+
_(last_response.status).must_equal 200
|
70
|
+
_(last_response.body).must_match(/HardWorker/)
|
67
71
|
|
68
72
|
assert_equal failed_count, "0"
|
69
73
|
end
|
70
74
|
|
71
75
|
it 'has the delete all form and action' do
|
72
|
-
last_response.body.must_match
|
73
|
-
last_response.body.must_match
|
76
|
+
_(last_response.body).must_match(/failures\/all\/delete/)
|
77
|
+
_(last_response.body).must_match(/Delete All/)
|
74
78
|
end
|
75
79
|
|
76
80
|
it 'can delete all failures' do
|
77
81
|
assert_equal failed_count, "1"
|
78
82
|
|
79
|
-
last_response.body.must_match
|
83
|
+
_(last_response.body).must_match(/HardWorker/)
|
80
84
|
|
81
85
|
post '/failures/all/delete'
|
82
|
-
last_response.status.must_equal 302
|
83
|
-
last_response.location.must_match
|
86
|
+
_(last_response.status).must_equal 302
|
87
|
+
_(last_response.location).must_match(/failures$/)
|
84
88
|
|
85
89
|
get '/failures'
|
86
|
-
last_response.status.must_equal 200
|
87
|
-
last_response.body.must_match
|
90
|
+
_(last_response.status).must_equal 200
|
91
|
+
_(last_response.body).must_match(/No failed jobs found/)
|
88
92
|
|
89
93
|
assert_equal failed_count, "1"
|
90
94
|
end
|
91
95
|
|
92
96
|
it 'has the retry all form and action' do
|
93
|
-
last_response.body.must_match
|
94
|
-
last_response.body.must_match
|
97
|
+
_(last_response.body).must_match(/failures\/all\/retry/)
|
98
|
+
_(last_response.body).must_match(/Retry All/)
|
95
99
|
end
|
96
100
|
|
97
101
|
it 'can retry all failures' do
|
98
102
|
assert_equal failed_count, "1"
|
99
103
|
|
100
|
-
last_response.body.must_match
|
104
|
+
_(last_response.body).must_match(/HardWorker/)
|
101
105
|
post '/failures/all/retry'
|
102
|
-
last_response.status.must_equal 302
|
103
|
-
last_response.location.must_match
|
106
|
+
_(last_response.status).must_equal 302
|
107
|
+
_(last_response.location).must_match(/failures/)
|
104
108
|
|
105
109
|
get '/failures'
|
106
|
-
last_response.status.must_equal 200
|
107
|
-
last_response.body.must_match(/No failed jobs found/)
|
110
|
+
_(last_response.status).must_equal 200
|
111
|
+
_(last_response.body).must_match(/No failed jobs found/)
|
108
112
|
end
|
109
113
|
|
110
114
|
it 'can delete failure from the list' do
|
111
115
|
assert_equal failed_count, "1"
|
112
116
|
|
113
|
-
last_response.body.must_match
|
117
|
+
_(last_response.body).must_match(/HardWorker/)
|
114
118
|
|
115
119
|
post '/failures', { :key => [failure_score], :delete => 'Delete' }
|
116
|
-
last_response.status.must_equal 302
|
117
|
-
last_response.location.must_match
|
120
|
+
_(last_response.status).must_equal 302
|
121
|
+
_(last_response.location).must_match(/failures/)
|
118
122
|
|
119
123
|
get '/failures'
|
120
|
-
last_response.status.must_equal 200
|
121
|
-
last_response.body.must_match
|
124
|
+
_(last_response.status).must_equal 200
|
125
|
+
_(last_response.body).must_match(/No failed jobs found/)
|
122
126
|
end
|
123
127
|
|
124
128
|
it 'can retry failure from the list' do
|
125
129
|
assert_equal failed_count, "1"
|
126
130
|
|
127
|
-
last_response.body.must_match
|
131
|
+
_(last_response.body).must_match(/HardWorker/)
|
128
132
|
|
129
133
|
post '/failures', { :key => [failure_score], :retry => 'Retry Now' }
|
130
|
-
last_response.status.must_equal 302
|
131
|
-
last_response.location.must_match
|
134
|
+
_(last_response.status).must_equal 302
|
135
|
+
_(last_response.location).must_match(/failures/)
|
136
|
+
|
137
|
+
get '/failures'
|
138
|
+
_(last_response.status).must_equal 200
|
139
|
+
_(last_response.body).must_match(/No failed jobs found/)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'can handle failures with nil error_message' do
|
143
|
+
create_sample_failure(error_message: nil)
|
132
144
|
|
133
145
|
get '/failures'
|
134
|
-
|
135
|
-
last_response.
|
146
|
+
|
147
|
+
_(last_response.status).must_equal 200
|
136
148
|
end
|
137
149
|
end
|
138
150
|
|
@@ -143,38 +155,47 @@ module Sidekiq
|
|
143
155
|
end
|
144
156
|
|
145
157
|
it 'should be successful' do
|
146
|
-
last_response.status.must_equal 200
|
158
|
+
_(last_response.status).must_equal 200
|
147
159
|
end
|
148
160
|
|
149
161
|
it 'can display failure page' do
|
150
|
-
last_response.body.must_match
|
151
|
-
last_response.body.must_match
|
152
|
-
last_response.body.must_match
|
153
|
-
last_response.body.must_match
|
162
|
+
_(last_response.body).must_match(/Job/)
|
163
|
+
_(last_response.body).must_match(/HardWorker/)
|
164
|
+
_(last_response.body).must_match(/ArgumentError/)
|
165
|
+
_(last_response.body).must_match(/file1/)
|
154
166
|
end
|
155
167
|
|
156
168
|
it 'can delete failure' do
|
157
|
-
last_response.body.must_match
|
169
|
+
_(last_response.body).must_match(/HardWorker/)
|
158
170
|
|
159
171
|
post "/failures/#{failure_score}", :delete => 'Delete'
|
160
|
-
last_response.status.must_equal 302
|
161
|
-
last_response.location.must_match
|
172
|
+
_(last_response.status).must_equal 302
|
173
|
+
_(last_response.location).must_match(/failures/)
|
162
174
|
|
163
175
|
get "/failures/#{failure_score}"
|
164
|
-
last_response.status.must_equal 302
|
165
|
-
last_response.location.must_match
|
176
|
+
_(last_response.status).must_equal 302
|
177
|
+
_(last_response.location).must_match(/failures/)
|
166
178
|
end
|
167
179
|
|
168
180
|
it 'can retry failure' do
|
169
|
-
last_response.body.must_match
|
181
|
+
_(last_response.body).must_match(/HardWorker/)
|
170
182
|
|
171
183
|
post "/failures/#{failure_score}", :retry => 'Retry Now'
|
172
|
-
last_response.status.must_equal 302
|
173
|
-
last_response.location.must_match
|
184
|
+
_(last_response.status).must_equal 302
|
185
|
+
_(last_response.location).must_match(/failures/)
|
174
186
|
|
175
187
|
get "/failures/#{failure_score}"
|
176
|
-
last_response.status.must_equal 302
|
177
|
-
last_response.location.must_match
|
188
|
+
_(last_response.status).must_equal 302
|
189
|
+
_(last_response.location).must_match(/failures/)
|
190
|
+
end
|
191
|
+
|
192
|
+
if defined? Sidekiq::Pro
|
193
|
+
it 'can filter failure' do
|
194
|
+
create_sample_failure
|
195
|
+
post '/filter/failures', substr: 'new'
|
196
|
+
|
197
|
+
_(last_response.status).must_equal 200
|
198
|
+
end
|
178
199
|
end
|
179
200
|
end
|
180
201
|
|
@@ -186,11 +207,11 @@ module Sidekiq
|
|
186
207
|
end
|
187
208
|
|
188
209
|
it 'can escape arguments' do
|
189
|
-
last_response.body.must_match
|
210
|
+
_(last_response.body).must_match(/"<h1>omg</h1>"/)
|
190
211
|
end
|
191
212
|
|
192
213
|
it 'can escape error message' do
|
193
|
-
last_response.body.must_match
|
214
|
+
_(last_response.body).must_match(/ArgumentError: <p>wow</p>/)
|
194
215
|
end
|
195
216
|
end
|
196
217
|
|
@@ -201,8 +222,8 @@ module Sidekiq
|
|
201
222
|
end
|
202
223
|
|
203
224
|
it 'should be successful' do
|
204
|
-
last_response.status.must_equal 200
|
205
|
-
last_response.body.wont_match
|
225
|
+
_(last_response.status).must_equal 200
|
226
|
+
_(last_response.body).wont_match(/No failed jobs found/)
|
206
227
|
end
|
207
228
|
end
|
208
229
|
|
@@ -213,8 +234,8 @@ module Sidekiq
|
|
213
234
|
end
|
214
235
|
|
215
236
|
it 'should be successful' do
|
216
|
-
last_response.status.must_equal 200
|
217
|
-
last_response.body.wont_match
|
237
|
+
_(last_response.status).must_equal 200
|
238
|
+
_(last_response.body).wont_match(/No failed jobs found/)
|
218
239
|
end
|
219
240
|
end
|
220
241
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-failures
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcelo Silveira
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -16,14 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 4.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rake
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,8 +101,9 @@ executables: []
|
|
87
101
|
extensions: []
|
88
102
|
extra_rdoc_files: []
|
89
103
|
files:
|
104
|
+
- ".github/dependabot.yml"
|
105
|
+
- ".github/workflows/ci.yml"
|
90
106
|
- ".gitignore"
|
91
|
-
- ".travis.yml"
|
92
107
|
- CHANGELOG.md
|
93
108
|
- Gemfile
|
94
109
|
- LICENSE
|
@@ -98,6 +113,9 @@ files:
|
|
98
113
|
- lib/sidekiq/failures.rb
|
99
114
|
- lib/sidekiq/failures/failure_set.rb
|
100
115
|
- lib/sidekiq/failures/locales/en.yml
|
116
|
+
- lib/sidekiq/failures/locales/ja.yml
|
117
|
+
- lib/sidekiq/failures/locales/pt-BR.yml
|
118
|
+
- lib/sidekiq/failures/locales/zh-cn.yml
|
101
119
|
- lib/sidekiq/failures/middleware.rb
|
102
120
|
- lib/sidekiq/failures/sorted_entry.rb
|
103
121
|
- lib/sidekiq/failures/version.rb
|
@@ -105,13 +123,15 @@ files:
|
|
105
123
|
- lib/sidekiq/failures/views/failures.erb
|
106
124
|
- lib/sidekiq/failures/web_extension.rb
|
107
125
|
- sidekiq-failures.gemspec
|
126
|
+
- test/failures_test.rb
|
108
127
|
- test/middleware_test.rb
|
109
128
|
- test/test_helper.rb
|
110
129
|
- test/web_extension_test.rb
|
111
130
|
homepage: https://github.com/mhfs/sidekiq-failures/
|
112
|
-
licenses:
|
131
|
+
licenses:
|
132
|
+
- MIT
|
113
133
|
metadata: {}
|
114
|
-
post_install_message:
|
134
|
+
post_install_message:
|
115
135
|
rdoc_options: []
|
116
136
|
require_paths:
|
117
137
|
- lib
|
@@ -126,13 +146,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
146
|
- !ruby/object:Gem::Version
|
127
147
|
version: '0'
|
128
148
|
requirements: []
|
129
|
-
|
130
|
-
|
131
|
-
signing_key:
|
149
|
+
rubygems_version: 3.3.7
|
150
|
+
signing_key:
|
132
151
|
specification_version: 4
|
133
152
|
summary: Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you
|
134
153
|
browse them. Makes use of Sidekiq's custom tabs and middleware chain.
|
135
|
-
test_files:
|
136
|
-
- test/middleware_test.rb
|
137
|
-
- test/test_helper.rb
|
138
|
-
- test/web_extension_test.rb
|
154
|
+
test_files: []
|
data/.travis.yml
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
services:
|
3
|
-
- redis-server
|
4
|
-
rvm:
|
5
|
-
- 1.9.3
|
6
|
-
- jruby-19mode
|
7
|
-
- 2.0.0
|
8
|
-
- 2.1
|
9
|
-
env:
|
10
|
-
matrix:
|
11
|
-
- SIDEKIQ_VERSION="~> 2.16"
|
12
|
-
- SIDEKIQ_VERSION="~> 2.17"
|
13
|
-
- SIDEKIQ_VERSION="~> 3.0"
|
14
|
-
- SIDEKIQ_VERSION="~> 3.1"
|
15
|
-
matrix:
|
16
|
-
allow_failures:
|
17
|
-
- rvm: 1.9.3
|
18
|
-
- rvm: jruby-19mode
|