atomic-sidekiq 1.1.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3071f88191cdf61f92a11d1ffde7f5cab21db595
4
- data.tar.gz: 850929d672c23bbfdd6ed645467ec2e49c4b4a99
3
+ metadata.gz: af06b1517443b34e0a8e08127c1d730c81658c73
4
+ data.tar.gz: 01ad8568be11fa6c4e9c67da7422efe903d2b189
5
5
  SHA512:
6
- metadata.gz: cd7312265115acc03dd613e0623a74fdbc8ec3996de8177966af68300fad1a6d6e6a0aaf4e21d24a219ad0c189078fea097ce1d9d76550902fd87f0189076ef2
7
- data.tar.gz: 5720a6e7b38226ee37cae38df9c94d49fe459d3890da868c3185415c822ff3dce8cb90dfbed4ade9d263b2f0ee418b868973995c85d9e6d232df638b3ad773ed
6
+ metadata.gz: 4e9aa27c6fd049d84c0a8010ae8a0a636e3bbfe15ee39a5ad194ef5f7193dcca5779ea5bfe80e257ff67f87f0717da967cf8bf0e802bcb1a4ea2e7a8f45dd561
7
+ data.tar.gz: 340e2d9f8997c905d0e6c0df0c11baff6aa7d1218e447fd3770daacc61d513fabad4662a03b2bd36ec437c96b476fd098d5ed644a1338af64296a8142873d39c
@@ -14,8 +14,10 @@ GEM
14
14
  url
15
15
  concurrent-ruby (1.0.5)
16
16
  connection_pool (2.2.1)
17
+ daemons (1.2.6)
17
18
  diff-lcs (1.3)
18
19
  docile (1.3.0)
20
+ eventmachine (1.2.5)
19
21
  json (2.1.0)
20
22
  parallel (1.12.1)
21
23
  parser (2.5.0.5)
@@ -58,6 +60,10 @@ GEM
58
60
  json (>= 1.8, < 3)
59
61
  simplecov-html (~> 0.10.0)
60
62
  simplecov-html (0.10.2)
63
+ thin (1.7.2)
64
+ daemons (~> 1.0, >= 1.0.9)
65
+ eventmachine (~> 1.0, >= 1.0.4)
66
+ rack (>= 1, < 3)
61
67
  timecop (0.9.1)
62
68
  unicode-display_width (1.3.0)
63
69
  url (0.3.2)
@@ -72,6 +78,7 @@ DEPENDENCIES
72
78
  rake (~> 11.3)
73
79
  rspec
74
80
  rubocop
81
+ thin
75
82
  timecop
76
83
 
77
84
  BUNDLED WITH
data/README.md CHANGED
@@ -73,6 +73,15 @@ The performance test uses the default settings for both fetchers (default and At
73
73
 
74
74
  The reliability improvements of AtomicSidekiq come at the cost of less throughput. AtomicSidekiq's algorithm is linear instead of constant like Sidekiq's default, meaning that the cost of performance increases linearly as more jobs are added to the queue.
75
75
 
76
+ ## Web
77
+ AtomicSidekiq provides two different pages for checking stats on the job reliability. One shows which jobs are currently "in-flight" status (even if they might have exited uexpectedly) and how long before they expire. A second page shows how many jobs have been recovered by queue and by worker class.
78
+
79
+ ![In-flight Web UI](https://raw.githubusercontent.com/Colex/atomic-sidekiq/master/images/in_flight_web.png)
80
+
81
+ _"Estimated Lost"_ shows how many jobs in-flight might have been lost (this is calculated by looking how many jobs are in "Busy" and how many are "In-flight").
82
+
83
+ ![Recovered Stats Web UI](https://raw.githubusercontent.com/Colex/atomic-sidekiq/master/images/recovered_web.png)
84
+
76
85
  ## Tests
77
86
  ```sh
78
87
  bundle exec rspec
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "atomic-sidekiq"
5
- s.version = "1.1.0"
5
+ s.version = "1.1.2"
6
6
  s.date = "2018-04-02"
7
7
  s.summary = "Reliable fetcher for Sidekiq"
8
8
  s.description = "Reliable fetcher for Sidekiq"
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency "rubocop", "~> 0.54"
22
22
  s.add_development_dependency "timecop", "~> 0.9"
23
23
  s.add_development_dependency "codecov", ">= 0.1.10"
24
+ s.add_development_dependency "thin"
24
25
 
25
26
  s.add_runtime_dependency "sidekiq", "~> 5.0"
26
27
  end
@@ -0,0 +1,10 @@
1
+ require "sidekiq/web"
2
+ require_relative "lib/atomic-sidekiq"
3
+
4
+ Sidekiq.configure_client do |config|
5
+ config.redis = { db: 13 }
6
+ end
7
+
8
+ map "/sidekiq" do
9
+ run Sidekiq::Web
10
+ end
@@ -8,6 +8,8 @@ services:
8
8
  - docker.env
9
9
  depends_on:
10
10
  - redis
11
+ ports:
12
+ - 3000:3000
11
13
  volumes:
12
14
  - .:/sidekiq-atomic
13
15
  image: sidekiq-atomic
Binary file
Binary file
@@ -1,10 +1,13 @@
1
1
  require "sidekiq"
2
2
  require_relative "atomic_sidekiq/sidekiq/sidekiq"
3
+ require_relative "atomic_sidekiq/in_flight_queue"
3
4
  require_relative "atomic_sidekiq/in_flight_keymaker"
4
5
  require_relative "atomic_sidekiq/unit_of_work"
5
6
  require_relative "atomic_sidekiq/atomic_fetch"
6
7
  require_relative "atomic_sidekiq/dead_job_collector"
7
8
  require_relative "atomic_sidekiq/heartbeat"
9
+ require_relative "atomic_sidekiq/recovered_stats"
10
+ require_relative "atomic_sidekiq/web"
8
11
  require_relative "atomic_sidekiq/atomic_operation/base"
9
12
  require_relative "atomic_sidekiq/atomic_operation/acknowledge"
10
13
  require_relative "atomic_sidekiq/atomic_operation/requeue"
@@ -9,6 +9,7 @@ module AtomicSidekiq
9
9
  end
10
10
 
11
11
  def initialize(queue, in_flight_keymaker:)
12
+ @recovered_stats = RecoveredStats.new
12
13
  @queue = queue
13
14
  @in_flight_keymaker = in_flight_keymaker
14
15
  @expire_op = AtomicOperation::Expire.new
@@ -20,10 +21,14 @@ module AtomicSidekiq
20
21
 
21
22
  private
22
23
 
23
- attr_reader :queue, :in_flight_keymaker, :expire_op
24
+ attr_reader :queue, :in_flight_keymaker, :expire_op, :recovered_stats
24
25
 
25
26
  def expire!(job_key)
26
- expire_op.perform(queue, job_key)
27
+ recovered = expire_op.perform(queue, job_key)
28
+ return if recovered.nil?
29
+ job = JSON.parse(recovered[1])
30
+ recovered_stats.increment!(job)
31
+ job
27
32
  end
28
33
 
29
34
  def each_keys
@@ -4,6 +4,10 @@ module AtomicSidekiq
4
4
  @key_prefix = key_prefix
5
5
  end
6
6
 
7
+ def matcher
8
+ "#{key_prefix}:*"
9
+ end
10
+
7
11
  def queue_prefix(queue)
8
12
  normalized_name = queue.gsub(/queue:/, "")
9
13
  "#{key_prefix}:#{normalized_name}:"
@@ -0,0 +1,35 @@
1
+ module AtomicSidekiq
2
+ class InFlightQueue
3
+ def initialize
4
+ @keymaker = InFlightKeymaker.new(AtomicFetch::IN_FLIGHT_KEY_PREFIX)
5
+ end
6
+
7
+ def list
8
+ keys = list_keys
9
+ retrieve_jobs(keys)
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :keymaker
15
+
16
+ def list_keys
17
+ matcher = keymaker.matcher
18
+ result = []
19
+ it = 0
20
+ loop do
21
+ it, keys = Sidekiq.redis { |conn| conn.scan(it, match: matcher) }
22
+ result.concat(keys)
23
+ it = it.to_i
24
+ break if it.zero?
25
+ end
26
+ result
27
+ end
28
+
29
+ def retrieve_jobs(keys)
30
+ Sidekiq.redis do |conn|
31
+ keys.map { |key| JSON.parse(conn.get(key)) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ module AtomicSidekiq
2
+ class RecoveredStats
3
+ def increment!(job)
4
+ puts "INCREMENTING"
5
+ increment_by_job!(job["class"])
6
+ increment_by_queue!(job["queue"])
7
+ end
8
+
9
+ def stats_by_queue
10
+ iterate_stats(queue_prefix)
11
+ end
12
+
13
+ def stats_by_job
14
+ iterate_stats(job_prefix)
15
+ end
16
+
17
+ private
18
+
19
+ def iterate_stats(prefix)
20
+ iterate_keys(prefix).map do |key|
21
+ value = Sidekiq.redis { |conn| conn.get(key) }
22
+ [key.gsub(Regexp.new("#{prefix}:"), ""), value]
23
+ end.to_h
24
+ end
25
+
26
+ def iterate_keys(prefix)
27
+ it = 0
28
+ result = []
29
+ loop do
30
+ it, keys = Sidekiq.redis { |conn| conn.scan(it, match: "#{prefix}:*") }
31
+ result.concat(keys)
32
+ it = it.to_i
33
+ return result if it.zero?
34
+ end
35
+ end
36
+
37
+ def increment_by_job!(job_name)
38
+ Sidekiq.redis { |conn| conn.incr("#{job_prefix}:#{job_name}") }
39
+ end
40
+
41
+ def increment_by_queue!(queue)
42
+ Sidekiq.redis { |conn| conn.incr("#{queue_prefix}:#{queue}") }
43
+ end
44
+
45
+ def prefix
46
+ "atomic_sidekiq"
47
+ end
48
+
49
+ def queue_prefix
50
+ "#{prefix}:queue"
51
+ end
52
+
53
+ def job_prefix
54
+ "#{prefix}:job"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ module AtomicSidekiq
2
+ module Web
3
+ VIEW_PATH = File.expand_path("../../web/views", __dir__)
4
+
5
+ def self.registered(app)
6
+ app.get "/in-flight" do
7
+ Web.render_in_flight
8
+ end
9
+
10
+ app.get "/recovered" do
11
+ Web.render_recovered
12
+ end
13
+ end
14
+
15
+ def self.render_in_flight
16
+ @jobs = AtomicSidekiq::InFlightQueue.new.list
17
+ @total_size = @jobs.count
18
+ @count = 25
19
+ @current_page = (params[:page] || 1).to_i
20
+ @jobs = @jobs[@current_page..(@current_page + @count)]
21
+ erb File.read(File.join(VIEW_PATH, "in_flight.erb"))
22
+ end
23
+
24
+ def self.render_recovered
25
+ @queues = AtomicSidekiq::RecoveredStats.new.stats_by_queue
26
+ @jobs = AtomicSidekiq::RecoveredStats.new.stats_by_job
27
+ erb File.read(File.join(VIEW_PATH, "recovered.erb"))
28
+ end
29
+ end
30
+ end
31
+
32
+ require "sidekiq/web" unless defined?(Sidekiq::Web)
33
+ Sidekiq::Web.register(AtomicSidekiq::Web)
34
+ Sidekiq::Web.tabs["In-flight"] = "in-flight"
35
+ Sidekiq::Web.tabs["Recovered"] = "recovered"
@@ -0,0 +1,23 @@
1
+ <% if @total_size > @count %>
2
+ <ul class="pagination pull-right flip">
3
+ <li class="<%= "disabled" if @current_page == 1 %>">
4
+ <a href="<%= url %>?page=1">&laquo;</a>
5
+ </li>
6
+ <% if @current_page > 1 %>
7
+ <li>
8
+ <a href="<%= url %>?<%= qparams(page: @current_page - 1) %>"><%= @current_page - 1 %></a>
9
+ </li>
10
+ <% end %>
11
+ <li class="disabled">
12
+ <a href="<%= url %>?<%= qparams(page: @current_page) %>"><%= @current_page %></a>
13
+ </li>
14
+ <% if @total_size > @current_page * @count %>
15
+ <li>
16
+ <a href="<%= url %>?<%= qparams(page: @current_page + 1) %>"><%= @current_page + 1 %></a>
17
+ </li>
18
+ <% end %>
19
+ <li class="<%= "disabled" if @total_size <= @current_page * @count %>">
20
+ <a href="<%= url %>?<%= qparams(page: (@total_size.to_f / @count).ceil) %>">&raquo;</a>
21
+ </li>
22
+ </ul>
23
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <header class="row">
2
+ <div class="col-sm-6">
3
+ <h3><%= t("In-flight") %></h3>
4
+ </div>
5
+ <div class="col-sm-4">
6
+ <%= erb :_paging, locals: { url: "#{root_path}in-flight" } %>
7
+ </div>
8
+ </header>
9
+
10
+ <div class="table_container">
11
+ <table class="inflight table table-hover table-bordered table-striped table-white">
12
+ <thead>
13
+ <th><%= t("Total In-flight") %></th>
14
+ <th><%= t("Estimated Lost") %></th>
15
+ </thead>
16
+ <tr>
17
+ <td><%= number_with_delimiter(@total_size) %> </td>
18
+ <td><%= number_with_delimiter(@total_size - workers.size) %> </td>
19
+ </tr>
20
+ </table>
21
+ </div>
22
+
23
+ <div class="table_container">
24
+ <table class="inflight table table-hover table-bordered table-striped table-white">
25
+ <thead>
26
+ <th><%= t("Job") %></th>
27
+ <th><%= t("Args") %></th>
28
+ <th><%= t("Queue") %></th>
29
+ <th><%= t("Expires at") %></th>
30
+ </thead>
31
+ <% @jobs.each do |job| %>
32
+ <tr>
33
+ <td><%= h job["class"] %> </td>
34
+ <td><%= h job["args"] %> </td>
35
+ <td>
36
+ <a href="<%= root_path %>queues/<%= CGI.escape(job["queue"]) %>"><%= h job["queue"] %></a>
37
+ </td>
38
+ <td>
39
+ <%= relative_time(Time.at(job["expire_at"])) %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </table>
44
+ </div>
@@ -0,0 +1,35 @@
1
+ <header class="row">
2
+ <div class="col-sm-12">
3
+ <h3><%= t("Recovered Jobs") %></h3>
4
+ </div>
5
+ </header>
6
+
7
+ <div class="table_container">
8
+ <table class="recovered table table-hover table-bordered table-striped table-white">
9
+ <thead>
10
+ <th><%= t("Queue") %></th>
11
+ <th><%= t("Count") %></th>
12
+ </thead>
13
+ <% @queues.each do |key, count| %>
14
+ <tr>
15
+ <td><%= h key %> </td>
16
+ <td><%= h count %> </td>
17
+ </tr>
18
+ <% end %>
19
+ </table>
20
+ </div>
21
+
22
+ <div class="table_container">
23
+ <table class="recovered table table-hover table-bordered table-striped table-white">
24
+ <thead>
25
+ <th><%= t("Job Name") %></th>
26
+ <th><%= t("Count") %></th>
27
+ </thead>
28
+ <% @jobs.each do |key, count| %>
29
+ <tr>
30
+ <td><%= h key %> </td>
31
+ <td><%= h count %> </td>
32
+ </tr>
33
+ <% end %>
34
+ </table>
35
+ </div>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic-sidekiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Correia Santos
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 0.1.10
97
+ - !ruby/object:Gem::Dependency
98
+ name: thin
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: sidekiq
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -129,8 +143,11 @@ files:
129
143
  - bin/sidekiqfail
130
144
  - bin/sidekiqload
131
145
  - bin/test
146
+ - config.ru
132
147
  - docker-compose.yml
133
148
  - docker.env
149
+ - images/in_flight_web.png
150
+ - images/recovered_web.png
134
151
  - lib/atomic-sidekiq.rb
135
152
  - lib/atomic_sidekiq.rb
136
153
  - lib/atomic_sidekiq/atomic_fetch.rb
@@ -146,8 +163,14 @@ files:
146
163
  - lib/atomic_sidekiq/dead_job_collector.rb
147
164
  - lib/atomic_sidekiq/heartbeat.rb
148
165
  - lib/atomic_sidekiq/in_flight_keymaker.rb
166
+ - lib/atomic_sidekiq/in_flight_queue.rb
167
+ - lib/atomic_sidekiq/recovered_stats.rb
149
168
  - lib/atomic_sidekiq/sidekiq/sidekiq.rb
150
169
  - lib/atomic_sidekiq/unit_of_work.rb
170
+ - lib/atomic_sidekiq/web.rb
171
+ - web/views/_paging.erb
172
+ - web/views/in_flight.erb
173
+ - web/views/recovered.erb
151
174
  homepage: https://github.com/Colex/atomic-sidekiq
152
175
  licenses:
153
176
  - MIT