sidekiq-expected_failures 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-version
19
+ .ruby-gemset
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+
7
+ notifications:
8
+ email: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.0.1
2
+
3
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sidekiq-expected_failures.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Rafal Wojsznis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Sidekiq::ExpectedFailures
2
+
3
+ [![Code Climate](https://codeclimate.com/github/emq/sidekiq-expected_failures.png)](https://codeclimate.com/github/emq/sidekiq-expected_failures)
4
+ [![Build Status](https://travis-ci.org/emq/sidekiq-expected_failures.png?branch=master)](https://travis-ci.org/emq/sidekiq-expected_failures)
5
+
6
+ If you don't rely on standard sidekiq's retry behavior and you want to track exceptions, that will happen one way, or another - this thing is for you.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'sidekiq-expected_failures'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install sidekiq-expected_failures
21
+
22
+ ## Usage
23
+
24
+ Let's say you do a lot of API requests to some not reliable reliable service. Inside your worker you handle `Timeout::Error` exception - you delay it's execution, maybe modify parameters somehow, it doesn't really matter, what matter is that you want to log that it happen in a convenient way. Describe that case using ruby:
25
+
26
+ ``` ruby
27
+ class ApiCallWorker
28
+ include ::Sidekiq::Worker
29
+ sidekiq_options expected_failures: [Timeout::Error]
30
+
31
+ def perform(arguments)
32
+ # do some work
33
+ # ...
34
+
35
+ # this service sucks, try again in 10 minutes
36
+ rescue Timeout::Error => e
37
+ Sidekiq::Client.enqueue_in(10.minutes, self.class, arguments)
38
+ raise e # this will be handled by sidekiq-expected_failures middleware
39
+
40
+ # ensure block or some other stuff
41
+ # ...
42
+ end
43
+ ```
44
+
45
+ You can pass array of exceptions to handle inside `sidekiq_options`. This is how web interface looks like:
46
+
47
+ ![](http://i.imgur.com/7Fe8voD.jpg)
48
+
49
+ It logs each failed jobs to to redis list (per day) and keep global counters (per exception class as a single redis hash).
50
+
51
+ ### Usage with sidekiq-failures
52
+
53
+ Just be sure to load this one after `sidekiq-failures`, otherwise failed jobs will end up logged twice - and you probably don't want that.
54
+
55
+ ### Clearing failures
56
+
57
+ At the moment you have 3 public methods in `Sidekiq::ExpectedFailures` module:
58
+
59
+ - `clear_counters` - clears all counters (as I mentioned - it's stored inside single redis hash, but I doubt anyone would like to log more than 500 different exceptions, right?)
60
+ - `clear_old` - clears failed jobs older than today
61
+ - `clear_all` - clears all failed jobs
62
+
63
+ This might change in the future to something more sane.
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ task :default => :test
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << "lib"
9
+ t.libs << "test"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ t.verbose = true
12
+ end
@@ -0,0 +1,42 @@
1
+ module Sidekiq
2
+ module ExpectedFailures
3
+ class Middleware
4
+ include Sidekiq::Util
5
+
6
+ def call(worker, msg, queue)
7
+ exceptions = worker.class.get_sidekiq_options['expected_failures'].to_a
8
+
9
+ yield
10
+
11
+ rescue *exceptions => e
12
+ data = {
13
+ failed_at: Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
14
+ args: msg['args'],
15
+ exception: e.class.to_s,
16
+ error: e.message,
17
+ worker: msg['class'],
18
+ processor: "#{hostname}:#{process_id}-#{Thread.current.object_id}",
19
+ queue: queue
20
+ }
21
+
22
+ log_exception(data)
23
+ end
24
+
25
+ private
26
+
27
+ def log_exception(data)
28
+ Sidekiq.redis do |conn|
29
+ conn.multi do |m|
30
+ m.lpush("expected:#{today}", Sidekiq.dump_json(data))
31
+ m.sadd("expected:dates", today)
32
+ m.hincrby("expected:count", data[:exception], 1)
33
+ end
34
+ end
35
+ end
36
+
37
+ def today
38
+ Date.today.to_s
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Sidekiq
2
+ module ExpectedFailures
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ require 'sinatra/assetpack'
2
+
3
+ module Sidekiq
4
+ module ExpectedFailures
5
+ module Web
6
+
7
+ def self.registered(app)
8
+ web_dir = File.expand_path("../../../../web", __FILE__)
9
+ assets_dir = File.join(web_dir, "assets")
10
+
11
+ app.register Sinatra::AssetPack
12
+
13
+ app.assets do
14
+ serve '/js', from: assets_dir
15
+ js 'expected', ['/js/expected.js']
16
+ js 'bootstrap', ['/js/bootstrap.js']
17
+ end
18
+
19
+ app.helpers do
20
+ def link_to_details(job)
21
+ data = []
22
+ job["args"].each_with_index { |argument, index| data << "data-#{index+1}='#{h argument.inspect}'" }
23
+ "<a href='#' #{data.join(' ')} title='#{job["worker"]}'>Details</a>"
24
+ end
25
+ end
26
+
27
+ app.get "/expected_failures/clear/:what" do
28
+ case params[:what]
29
+ when 'old'
30
+ Sidekiq::ExpectedFailures.clear_old
31
+ when 'all'
32
+ Sidekiq::ExpectedFailures.clear_all
33
+ when 'counters'
34
+ Sidekiq::ExpectedFailures.clear_counters
35
+ end
36
+
37
+ redirect "#{root_path}expected_failures"
38
+ end
39
+
40
+ app.get "/expected_failures/?:date?" do
41
+ @dates = Sidekiq::ExpectedFailures.dates
42
+ @count = (params[:count] || 50).to_i
43
+
44
+ if @dates
45
+ @date = params[:date] || @dates.keys[0]
46
+ (@current_page, @total_size, @jobs) = page("expected:#{@date}", params[:page], @count)
47
+ @jobs = @jobs.map { |msg| Sidekiq.load_json(msg) }
48
+ @counters = Sidekiq::ExpectedFailures.counters
49
+ end
50
+
51
+ erb File.read(File.join(web_dir, "views/expected_failures.erb"))
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ require "sidekiq/web"
2
+ require "sidekiq/expected_failures/version"
3
+ require "sidekiq/expected_failures/middleware"
4
+ require "sidekiq/expected_failures/web"
5
+
6
+ module Sidekiq
7
+ module ExpectedFailures
8
+
9
+ def self.dates
10
+ Sidekiq.redis do |c|
11
+ c.smembers "expected:dates"
12
+ end.sort.reverse.each_with_object({}) do |d, hash|
13
+ hash[d] = Sidekiq.redis { |c| c.llen("expected:#{d}") }
14
+ end
15
+ end
16
+
17
+ def self.counters
18
+ Sidekiq.redis { |r| r.hgetall("expected:count") }
19
+ end
20
+
21
+ def self.clear_all
22
+ clear(dates.keys)
23
+ end
24
+
25
+ def self.clear_old
26
+ range = dates.keys.delete_if { |d| Date.parse(d) > Date.today.prev_day }
27
+ clear(range)
28
+ end
29
+
30
+ def self.clear_counters
31
+ Sidekiq.redis { |r| r.del("expected:count") }
32
+ end
33
+
34
+ private
35
+
36
+ def self.clear(dates)
37
+ dates.each do |date|
38
+ Sidekiq.redis do |c|
39
+ c.multi do |m|
40
+ m.srem("expected:dates", date)
41
+ m.del("expected:#{date}")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ Sidekiq::Web.register Sidekiq::ExpectedFailures::Web
50
+ Sidekiq::Web.tabs["Expected Failures"] = "expected_failures"
51
+
52
+ Sidekiq.configure_server do |config|
53
+ config.server_middleware do |chain|
54
+ chain.add Sidekiq::ExpectedFailures::Middleware
55
+ end
56
+ end
@@ -0,0 +1 @@
1
+ require "sidekiq/expected_failures"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sidekiq/expected_failures/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sidekiq-expected_failures"
8
+ spec.version = Sidekiq::ExpectedFailures::VERSION
9
+ spec.authors = ["Rafal Wojsznis"]
10
+ spec.email = ["rafal.wojsznis@gmail.com"]
11
+ spec.description = spec.summary = "If you don't rely on sidekiq' retry behavior, you handle exceptions on your own and want to keep track of them - this thing is for you."
12
+ spec.homepage = "https://github.com/emq/sidekiq-expected_failures"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "sidekiq", ">= 2.15.0"
21
+ spec.add_dependency "sinatra-assetpack", "~> 0.3.1"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "sinatra"
26
+ spec.add_development_dependency "rack-test"
27
+ spec.add_development_dependency "timecop", "~> 0.6.3"
28
+ end
@@ -0,0 +1,105 @@
1
+ require "test_helper"
2
+
3
+ module Sidekiq
4
+ module ExpectedFailures
5
+ describe "Middleware" do
6
+ before do
7
+ $invokes = 0
8
+ Sidekiq.redis = REDIS
9
+ Sidekiq.redis { |c| c.flushdb }
10
+ Timecop.freeze(Time.local(2013, 1, 10))
11
+ end
12
+
13
+ after { Timecop.return }
14
+
15
+ let(:msg) { {'class' => 'RandomStuff', 'args' => ['custom_argument'], 'retry' => false} }
16
+ let(:handler){ Sidekiq::ExpectedFailures::Middleware.new }
17
+
18
+ it 'does not handle exception by default' do
19
+ assert_raises RuntimeError do
20
+ handler.call(RegularWorker.new, msg, 'default') do
21
+ raise "Whooo, hold on there!"
22
+ end
23
+ end
24
+ end
25
+
26
+ it 'respects build-in rescue and ensure blocks' do
27
+ assert_equal 0, $invokes
28
+ assert_block do
29
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
30
+ begin
31
+ raise ZeroDivisionError.new("We go a problem, sir")
32
+ rescue ZeroDivisionError => e
33
+ $invokes += 1
34
+ raise e # and now this should be caught by middleware
35
+ ensure
36
+ $invokes += 1
37
+ end
38
+ end
39
+ end
40
+ assert_equal 2, $invokes
41
+ end
42
+
43
+ it 'handles all specified exceptions' do
44
+ assert_block do
45
+ handler.call(MultipleExceptionWorker.new, msg, 'default') do
46
+ raise NotImplementedError
47
+ end
48
+ end
49
+
50
+ assert_block do
51
+ handler.call(MultipleExceptionWorker.new, msg, 'default') do
52
+ raise VeryOwn::CustomException
53
+ end
54
+ end
55
+ end
56
+
57
+ it 'logs exceptions' do
58
+ assert_block do
59
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
60
+ raise ZeroDivisionError
61
+ end
62
+ end
63
+
64
+ assert_equal(['2013-01-10'], redis("smembers", "expected:dates"))
65
+ assert_match(/custom_argument/, redis("lrange", "expected:2013-01-10", 0, -1)[0])
66
+ end
67
+
68
+ it 'increments own counters per exception class' do
69
+ 2.times do
70
+ handler.call(MultipleExceptionWorker.new, msg, 'default') do
71
+ raise VeryOwn::CustomException
72
+ end
73
+ end
74
+
75
+ 5.times do
76
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
77
+ raise ZeroDivisionError
78
+ end
79
+ end
80
+
81
+ assert_equal 2, redis("hget", "expected:count", "VeryOwn::CustomException").to_i
82
+ assert_equal 5, redis("hget", "expected:count", "ZeroDivisionError").to_i
83
+ end
84
+
85
+ it 'logs multiple exceptions' do
86
+ make_some_noise = lambda do |x|
87
+ x.times do
88
+ handler.call(SingleExceptionWorker.new, msg, 'default') do
89
+ raise ZeroDivisionError
90
+ end
91
+ end
92
+ end
93
+
94
+ make_some_noise.call(10)
95
+
96
+ Timecop.freeze(Time.local(2013, 5, 15))
97
+ make_some_noise.call(5)
98
+
99
+ assert_equal 10, redis("llen", "expected:2013-01-10")
100
+ assert_equal 5, redis("llen", "expected:2013-05-15")
101
+ assert_equal(['2013-05-15', '2013-01-10'].sort, redis("smembers", "expected:dates").sort)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,143 @@
1
+ require "test_helper"
2
+ require "sidekiq/web"
3
+
4
+ module Sidekiq
5
+ describe "WebExtension" do
6
+ include Rack::Test::Methods
7
+
8
+ def app
9
+ Sidekiq::Web
10
+ end
11
+
12
+ def failed_count
13
+ Sidekiq.redis { |c| c.get("stat:failed") }
14
+ end
15
+
16
+ def create_sample_counter
17
+ redis("hset", "expected:count", "StandardError", 5)
18
+ redis("hset", "expected:count", "Custom::Error", 10)
19
+ end
20
+
21
+ def create_sample_failure
22
+ data = {
23
+ failed_at: Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
24
+ args: [{"hash" => "options", "more" => "options"}, 123],
25
+ exception: "ArgumentError",
26
+ error: "Some error message",
27
+ worker: "HardWorker",
28
+ queue: "api_calls"
29
+ }
30
+
31
+ Sidekiq.redis do |c|
32
+ c.lpush("expected:2013-09-10", Sidekiq.dump_json(data))
33
+ c.sadd("expected:dates", "2013-09-10")
34
+ end
35
+
36
+ Sidekiq.redis do |c|
37
+ c.lpush("expected:2013-09-09", Sidekiq.dump_json(data))
38
+ c.sadd("expected:dates", "2013-09-09")
39
+ end
40
+ end
41
+
42
+ before do
43
+ Sidekiq.redis = REDIS
44
+ Sidekiq.redis {|c| c.flushdb }
45
+ Timecop.freeze(Time.local(2013, 9, 10))
46
+ end
47
+
48
+ after { Timecop.return }
49
+
50
+ it 'can display home with failures tab' do
51
+ get '/'
52
+ last_response.status.must_equal(200)
53
+ last_response.body.must_match(/Sidekiq/)
54
+ last_response.body.must_match(/Expected Failures/)
55
+ end
56
+
57
+ it 'can display failures page without any failures' do
58
+ get '/expected_failures'
59
+ last_response.status.must_equal(200)
60
+ last_response.body.must_match(/Expected Failures/)
61
+ last_response.body.must_match(/No failed jobs found/)
62
+ end
63
+
64
+ describe 'when there are failures' do
65
+ before do
66
+ create_sample_failure
67
+ get '/expected_failures'
68
+ end
69
+
70
+ it 'should be successful' do
71
+ last_response.status.must_equal(200)
72
+ end
73
+
74
+ it 'lists failed jobs' do
75
+ last_response.body.must_match(/HardWorker/)
76
+ last_response.body.must_match(/api_calls/)
77
+ end
78
+
79
+ it 'can remove all failed jobs' do
80
+ get '/expected_failures'
81
+ last_response.body.must_match(/HardWorker/)
82
+
83
+ get '/expected_failures/clear/all'
84
+ last_response.status.must_equal(302)
85
+ last_response.location.must_match(/expected_failures$/)
86
+
87
+ get '/expected_failures'
88
+ last_response.body.must_match(/No failed jobs found/)
89
+ end
90
+
91
+ it 'can remove failed jobs older than 1 day' do
92
+ get '/expected_failures'
93
+ last_response.body.must_match(/2013-09-10/)
94
+ last_response.body.must_match(/2013-09-09/)
95
+
96
+ get '/expected_failures/clear/old'
97
+ last_response.status.must_equal(302)
98
+ last_response.location.must_match(/expected_failures$/)
99
+
100
+ get '/expected_failures'
101
+ last_response.body.wont_match(/2013-09-09/)
102
+ last_response.body.must_match(/2013-09-10/)
103
+
104
+ assert_nil redis("get", "expected:2013-09-09")
105
+ end
106
+ end
107
+
108
+ describe 'counter' do
109
+ describe 'when empty' do
110
+ it 'does not display counter div' do
111
+ create_sample_failure
112
+ get '/expected_failures'
113
+ last_response.body.wont_match(/dl-horizontal/)
114
+ last_response.body.wont_match(/Clear counters/i)
115
+ end
116
+ end
117
+
118
+ describe 'when not empty' do
119
+ before { create_sample_counter }
120
+
121
+ it 'displays counters' do
122
+ get '/expected_failures'
123
+ last_response.body.must_match(/dl-horizontal/)
124
+ last_response.body.must_match(/Clear counters/i)
125
+ end
126
+
127
+ it 'can clear counters' do
128
+ get '/expected_failures'
129
+ last_response.body.must_match(/Custom::Error/)
130
+
131
+ get '/expected_failures/clear/counters'
132
+ last_response.status.must_equal(302)
133
+ last_response.location.must_match(/expected_failures$/)
134
+
135
+ get '/expected_failures'
136
+ last_response.body.wont_match(/Custom::Error/)
137
+
138
+ assert_nil redis("get", "expected:count")
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,23 @@
1
+ Encoding.default_external = Encoding::UTF_8
2
+ Encoding.default_internal = Encoding::UTF_8
3
+
4
+ require "minitest/autorun"
5
+ require "minitest/spec"
6
+ require "minitest/mock"
7
+
8
+ require "timecop"
9
+ require "rack/test"
10
+
11
+ require "sidekiq"
12
+ require "sidekiq-expected_failures"
13
+ require "test_workers"
14
+
15
+ Sidekiq.logger.level = Logger::ERROR
16
+
17
+ REDIS = Sidekiq::RedisConnection.create(url: "redis://localhost/15", namespace: "sidekiq_expected_failures")
18
+
19
+ def redis(command, *args)
20
+ Sidekiq.redis do |c|
21
+ c.send(command, *args)
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module VeryOwn
2
+ class CustomException < StandardError; end
3
+ end
4
+
5
+ class RegularWorker
6
+ include ::Sidekiq::Worker
7
+ end
8
+
9
+ class SingleExceptionWorker
10
+ include ::Sidekiq::Worker
11
+ sidekiq_options expected_failures: [ZeroDivisionError]
12
+ end
13
+
14
+ class MultipleExceptionWorker
15
+ include ::Sidekiq::Worker
16
+ sidekiq_options expected_failures: [NotImplementedError, VeryOwn::CustomException]
17
+ end
@@ -0,0 +1,246 @@
1
+ /* ========================================================================
2
+ * Bootstrap: modal.js v3.0.0
3
+ * http://twbs.github.com/bootstrap/javascript.html#modals
4
+ * ========================================================================
5
+ * Copyright 2012 Twitter, Inc.
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ * ======================================================================== */
19
+
20
+
21
+ +function ($) { "use strict";
22
+
23
+ // MODAL CLASS DEFINITION
24
+ // ======================
25
+
26
+ var Modal = function (element, options) {
27
+ this.options = options
28
+ this.$element = $(element)
29
+ this.$backdrop =
30
+ this.isShown = null
31
+
32
+ if (this.options.remote) this.$element.load(this.options.remote)
33
+ }
34
+
35
+ Modal.DEFAULTS = {
36
+ backdrop: true
37
+ , keyboard: true
38
+ , show: true
39
+ }
40
+
41
+ Modal.prototype.toggle = function (_relatedTarget) {
42
+ return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
43
+ }
44
+
45
+ Modal.prototype.show = function (_relatedTarget) {
46
+ var that = this
47
+ var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
48
+
49
+ this.$element.trigger(e)
50
+
51
+ if (this.isShown || e.isDefaultPrevented()) return
52
+
53
+ this.isShown = true
54
+
55
+ this.escape()
56
+
57
+ this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
58
+
59
+ this.backdrop(function () {
60
+ var transition = $.support.transition && that.$element.hasClass('fade')
61
+
62
+ if (!that.$element.parent().length) {
63
+ that.$element.appendTo(document.body) // don't move modals dom position
64
+ }
65
+
66
+ that.$element.show()
67
+
68
+ if (transition) {
69
+ that.$element[0].offsetWidth // force reflow
70
+ }
71
+
72
+ that.$element
73
+ .addClass('in')
74
+ .attr('aria-hidden', false)
75
+
76
+ that.enforceFocus()
77
+
78
+ var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
79
+
80
+ transition ?
81
+ that.$element.find('.modal-dialog') // wait for modal to slide in
82
+ .one($.support.transition.end, function () {
83
+ that.$element.focus().trigger(e)
84
+ })
85
+ .emulateTransitionEnd(300) :
86
+ that.$element.focus().trigger(e)
87
+ })
88
+ }
89
+
90
+ Modal.prototype.hide = function (e) {
91
+ if (e) e.preventDefault()
92
+
93
+ e = $.Event('hide.bs.modal')
94
+
95
+ this.$element.trigger(e)
96
+
97
+ if (!this.isShown || e.isDefaultPrevented()) return
98
+
99
+ this.isShown = false
100
+
101
+ this.escape()
102
+
103
+ $(document).off('focusin.bs.modal')
104
+
105
+ this.$element
106
+ .removeClass('in')
107
+ .attr('aria-hidden', true)
108
+ .off('click.dismiss.modal')
109
+
110
+ $.support.transition && this.$element.hasClass('fade') ?
111
+ this.$element
112
+ .one($.support.transition.end, $.proxy(this.hideModal, this))
113
+ .emulateTransitionEnd(300) :
114
+ this.hideModal()
115
+ }
116
+
117
+ Modal.prototype.enforceFocus = function () {
118
+ $(document)
119
+ .off('focusin.bs.modal') // guard against infinite focus loop
120
+ .on('focusin.bs.modal', $.proxy(function (e) {
121
+ if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
122
+ this.$element.focus()
123
+ }
124
+ }, this))
125
+ }
126
+
127
+ Modal.prototype.escape = function () {
128
+ if (this.isShown && this.options.keyboard) {
129
+ this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
130
+ e.which == 27 && this.hide()
131
+ }, this))
132
+ } else if (!this.isShown) {
133
+ this.$element.off('keyup.dismiss.bs.modal')
134
+ }
135
+ }
136
+
137
+ Modal.prototype.hideModal = function () {
138
+ var that = this
139
+ this.$element.hide()
140
+ this.backdrop(function () {
141
+ that.removeBackdrop()
142
+ that.$element.trigger('hidden.bs.modal')
143
+ })
144
+ }
145
+
146
+ Modal.prototype.removeBackdrop = function () {
147
+ this.$backdrop && this.$backdrop.remove()
148
+ this.$backdrop = null
149
+ }
150
+
151
+ Modal.prototype.backdrop = function (callback) {
152
+ var that = this
153
+ var animate = this.$element.hasClass('fade') ? 'fade' : ''
154
+
155
+ if (this.isShown && this.options.backdrop) {
156
+ var doAnimate = $.support.transition && animate
157
+
158
+ this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
159
+ .appendTo(document.body)
160
+
161
+ this.$element.on('click.dismiss.modal', $.proxy(function (e) {
162
+ if (e.target !== e.currentTarget) return
163
+ this.options.backdrop == 'static'
164
+ ? this.$element[0].focus.call(this.$element[0])
165
+ : this.hide.call(this)
166
+ }, this))
167
+
168
+ if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
169
+
170
+ this.$backdrop.addClass('in')
171
+
172
+ if (!callback) return
173
+
174
+ doAnimate ?
175
+ this.$backdrop
176
+ .one($.support.transition.end, callback)
177
+ .emulateTransitionEnd(150) :
178
+ callback()
179
+
180
+ } else if (!this.isShown && this.$backdrop) {
181
+ this.$backdrop.removeClass('in')
182
+
183
+ $.support.transition && this.$element.hasClass('fade')?
184
+ this.$backdrop
185
+ .one($.support.transition.end, callback)
186
+ .emulateTransitionEnd(150) :
187
+ callback()
188
+
189
+ } else if (callback) {
190
+ callback()
191
+ }
192
+ }
193
+
194
+
195
+ // MODAL PLUGIN DEFINITION
196
+ // =======================
197
+
198
+ var old = $.fn.modal
199
+
200
+ $.fn.modal = function (option, _relatedTarget) {
201
+ return this.each(function () {
202
+ var $this = $(this)
203
+ var data = $this.data('bs.modal')
204
+ var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
205
+
206
+ if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
207
+ if (typeof option == 'string') data[option](_relatedTarget)
208
+ else if (options.show) data.show(_relatedTarget)
209
+ })
210
+ }
211
+
212
+ $.fn.modal.Constructor = Modal
213
+
214
+
215
+ // MODAL NO CONFLICT
216
+ // =================
217
+
218
+ $.fn.modal.noConflict = function () {
219
+ $.fn.modal = old
220
+ return this
221
+ }
222
+
223
+
224
+ // MODAL DATA-API
225
+ // ==============
226
+
227
+ $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
228
+ var $this = $(this)
229
+ var href = $this.attr('href')
230
+ var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
231
+ var option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
232
+
233
+ e.preventDefault()
234
+
235
+ $target
236
+ .modal(option, this)
237
+ .one('hide', function () {
238
+ $this.is(':visible') && $this.focus()
239
+ })
240
+ })
241
+
242
+ $(document)
243
+ .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
244
+ .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
245
+
246
+ }(window.jQuery);
@@ -0,0 +1,21 @@
1
+ $(function() {
2
+ function mapDataAttributes(element) {
3
+ html = "";
4
+ $.each(element.data(), function( key, value ) {
5
+ html += "<tr><th>Argument #" + key + "</th><td>" + value + "</td></tr>";
6
+ });
7
+ return html;
8
+ }
9
+
10
+ $('#expected tbody td:last-child > a').live('click', function(e){
11
+ $('#job-details .modal-body table tbody').html(mapDataAttributes($(this)));
12
+ $('#job-details .modal-title').text($(this).attr('title'));
13
+ $('#job-details').modal('show');
14
+
15
+ e.preventDefault();
16
+ });
17
+
18
+ $('#filter-jobs select').live('change', function(e){
19
+ $(this).parent('form').submit();
20
+ });
21
+ });
@@ -0,0 +1,91 @@
1
+ <%= js :expected %>
2
+ <%= js :bootstrap %>
3
+
4
+ <h3>Expected failures log
5
+ <% if @date %>
6
+ <small>(<%= @date %>)</small>
7
+ <% end %>
8
+ </h3>
9
+
10
+ <% unless @counters.empty? %>
11
+ <div class="well well-sm clearfix">
12
+ <a href="<%= root_path %>expected_failures/clear/counters" class="btn btn-default btn-sm pull-right">Clear counters</span></a>
13
+
14
+ <dl class="dl-horizontal" style="margin: 0">
15
+ <% @counters.each do |exception, count| %>
16
+ <dt><%= exception %></dt>
17
+ <dd><%= count %></dd>
18
+ <% end %>
19
+ </dl>
20
+
21
+ </div>
22
+ <% end %>
23
+
24
+ <% if @jobs.any? %>
25
+ <div class="modal fade" id="job-details">
26
+ <div class="modal-dialog">
27
+ <div class="modal-content">
28
+
29
+ <div class="modal-header">
30
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
31
+ <h4 class="modal-title"><!-- title --></h4>
32
+ </div>
33
+
34
+ <div class="modal-body">
35
+ <table class="table table-condensed table-striped">
36
+ <tbody>
37
+ <!-- loaded via js -->
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+
42
+ <div class="modal-footer">
43
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
44
+ </div>
45
+
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <form id="filter-jobs" class="form-inline pull-left" action="<%= root_path %>expected_failures" method="get">
51
+ <label>Choose date:</label>
52
+ <select name="date">
53
+ <% @dates.each do |date, count| %>
54
+ <option value="<%= date %>" <%= "selected" if date == @date %>><%= date %> (<%= count %>)</option>
55
+ <% end %>
56
+ </select>
57
+ </form>
58
+
59
+ <div class="pull-right">
60
+ <a href="<%= root_path %>expected_failures/clear/old" class="btn btn-default btn-sm">Clear older than today</a>&nbsp;
61
+ <a href="<%= root_path %>expected_failures/clear/all" class="btn btn-default btn-sm">Clear all failed</a>
62
+ </div>
63
+
64
+ <p class="clearfix"></p>
65
+
66
+ <table id="expected" class="queues table table-hover table-bordered table-striped table-white">
67
+ <thead>
68
+ <th>Datetime</th>
69
+ <th>Worker</th>
70
+ <th>Exception</th>
71
+ <th>Queue</th>
72
+ <th>Arguments</th>
73
+ </thead>
74
+ <% @jobs.each do |job| %>
75
+ <tr>
76
+ <td><%= Time.parse(job['failed_at']).strftime('%m/%d/%Y %H:%M:%S') %></td>
77
+ <td><%= job["worker"] %></td>
78
+ <td><%= job["exception"] %> <small>(<%= job["error"]%>)</small></td>
79
+ <td><a href="<%= "#{root_path}/queues/#{job["queue"]}"%>"><%= job["queue"] %></a></td>
80
+ <td><%= link_to_details(job) %></td>
81
+ </tr>
82
+ <% end %>
83
+ </table>
84
+
85
+ <%= erb :_paging, :locals => { :url => "#{root_path}expected_failures/#{@date}" } %>
86
+
87
+ <% else %>
88
+ <div class="alert alert-success">
89
+ No failed jobs found.
90
+ </div>
91
+ <% end %>
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-expected_failures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rafal Wojsznis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sidekiq
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.15.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.15.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra-assetpack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.3.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.3.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: sinatra
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rack-test
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: timecop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.6.3
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 0.6.3
126
+ description: If you don't rely on sidekiq' retry behavior, you handle exceptions on
127
+ your own and want to keep track of them - this thing is for you.
128
+ email:
129
+ - rafal.wojsznis@gmail.com
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - .gitignore
135
+ - .travis.yml
136
+ - CHANGELOG.md
137
+ - Gemfile
138
+ - LICENSE.txt
139
+ - README.md
140
+ - Rakefile
141
+ - lib/sidekiq-expected_failures.rb
142
+ - lib/sidekiq/expected_failures.rb
143
+ - lib/sidekiq/expected_failures/middleware.rb
144
+ - lib/sidekiq/expected_failures/version.rb
145
+ - lib/sidekiq/expected_failures/web.rb
146
+ - sidekiq-expected_failures.gemspec
147
+ - test/lib/middleware_test.rb
148
+ - test/lib/web_test.rb
149
+ - test/test_helper.rb
150
+ - test/test_workers.rb
151
+ - web/assets/bootstrap.js
152
+ - web/assets/expected.js
153
+ - web/views/expected_failures.erb
154
+ homepage: https://github.com/emq/sidekiq-expected_failures
155
+ licenses:
156
+ - MIT
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ! '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ segments:
168
+ - 0
169
+ hash: -3544806089198755601
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ! '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ segments:
177
+ - 0
178
+ hash: -3544806089198755601
179
+ requirements: []
180
+ rubyforge_project:
181
+ rubygems_version: 1.8.25
182
+ signing_key:
183
+ specification_version: 3
184
+ summary: If you don't rely on sidekiq' retry behavior, you handle exceptions on your
185
+ own and want to keep track of them - this thing is for you.
186
+ test_files:
187
+ - test/lib/middleware_test.rb
188
+ - test/lib/web_test.rb
189
+ - test/test_helper.rb
190
+ - test/test_workers.rb