gitlab-sidekiq-fetcher 0.1.0 → 0.3.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
- SHA256:
3
- metadata.gz: f10159f679879ed622c3bbd336bb04d0da0c54d536b7270c8c81dcc1243cedfc
4
- data.tar.gz: cbca779aacb710b4f8de222799ca88860f3745666d22b51ea3d7da7f5677f8a0
2
+ SHA1:
3
+ metadata.gz: 00056b1430ab40c06094813fcf06296ef24eef40
4
+ data.tar.gz: 1ee1138aeaed7c353e2eaddab6982d7a1d384d82
5
5
  SHA512:
6
- metadata.gz: dd8376c3c379c325db87519e43f3ea36f96063b03b8884dc45363ff949cf3cda5341b20641f27dbb90be8a2188c0588ec92eb048b4055702aac6f7419f595a49
7
- data.tar.gz: 02b0b4746cbe42960b89fc3b1b7f861f87b45317f6ae1f1f76d737333f80ac67c1553f6a8fdd3d44cacda87c08de88f9dee9b227a6d4b021d8bd58cacaa7fdee
6
+ metadata.gz: 5c141c0c16201c5b788268b3d829cae2b1820682fb907f56f61e75aa205c223d83c519c544ac3bf0d0c8f7dd5758c3f754e93bbfe184e92b80c2bd9e134b06a3
7
+ data.tar.gz: d1cef7c3751a5b6dd04155a21d4ef9742bb274552eb66ab41cf2044cbcc94f21ca494aac7ee3e6db01fd9891e259e8bb2e52bc26c5d3299dd72b84f5088fe0f0
data/.gitignore CHANGED
@@ -1,2 +1,2 @@
1
+ Gemfile.lock
1
2
  *.gem
2
- coverage
File without changes
data/README.md CHANGED
@@ -5,11 +5,10 @@ gitlab-sidekiq-fetcher
5
5
  fetches from Redis.
6
6
 
7
7
  It's based on https://github.com/TEA-ebook/sidekiq-reliable-fetch.
8
+ At this time we only added Sidekiq 5+ support to it.
8
9
 
9
- There are two strategies implemented: [Reliable fetch](http://redis.io/commands/rpoplpush#pattern-reliable-queue) using `rpoplpush` command and
10
- semi-reliable fetch that uses regular `brpop` and `lpush` to pick the job and put it to working queue. The main benefit of "Reliable" strategy is that `rpoplpush` is atomic, eliminating a race condition in which jobs can be lost.
11
- However, it comes at a cost because `rpoplpush` can't watch multiple lists at the same time so we need to iterate over the entire queue list which significantly increases pressure on Redis when there are more than a few queues. The "semi-reliable" strategy is much more reliable than the default Sidekiq fetcher, though. Compared to the reliable fetch strategy, it does not increase pressure on Redis significantly.
12
-
10
+ It implements in Sidekiq the reliable queue pattern using [Redis' rpoplpush
11
+ command](http://redis.io/commands/rpoplpush#pattern-reliable-queue).
13
12
 
14
13
  ## Installation
15
14
 
@@ -25,22 +24,12 @@ Enable reliable fetches by calling this gem from your Sidekiq configuration:
25
24
 
26
25
  ```ruby
27
26
  Sidekiq.configure_server do |config|
28
- Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
27
+ Sidekiq::ReliableFetcher.setup_reliable_fetch!(config)
29
28
 
30
29
  # …
31
30
  end
32
31
  ```
33
32
 
34
- There is an additional parameter `config.options[:semi_reliable_fetch]` you can use to switch between two strategies:
35
-
36
- ```ruby
37
- Sidekiq.configure_server do |config|
38
- config.options[:semi_reliable_fetch] = true # Default value is false
39
-
40
- Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
41
- end
42
- ```
43
-
44
33
  ## License
45
34
 
46
35
  LGPL-3.0, see the LICENSE file.
@@ -1,14 +1,16 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'gitlab-sidekiq-fetcher'
3
- s.version = '0.1.0'
3
+ s.version = '0.3.0'
4
4
  s.authors = ['TEA', 'GitLab']
5
5
  s.email = 'valery@gitlab.com'
6
6
  s.license = 'LGPL-3.0'
7
- s.homepage = 'https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/'
7
+ s.homepage = 'https://github.com/TEA-ebook/sidekiq-reliable-fetch'
8
8
  s.summary = 'Reliable fetch extension for Sidekiq'
9
9
  s.description = 'Redis reliable queue pattern implemented in Sidekiq'
10
10
  s.require_paths = ['lib']
11
+
11
12
  s.files = `git ls-files`.split($\)
12
13
  s.test_files = []
14
+
13
15
  s.add_dependency 'sidekiq', '~> 5'
14
16
  end
@@ -1,5 +1,4 @@
1
1
  require 'sidekiq'
2
2
 
3
- require_relative 'sidekiq/base_reliable_fetch'
4
- require_relative 'sidekiq/reliable_fetch'
5
- require_relative 'sidekiq/semi_reliable_fetch'
3
+ require_relative 'sidekiq/reliable_fetcher'
4
+ require_relative 'sidekiq-reliable-fetch/web'
@@ -0,0 +1,56 @@
1
+ module SidekiqReliableFetch
2
+ ##
3
+ # Encapsulates a working queue within Sidekiq.
4
+ # Allows enumeration of all jobs within the queue.
5
+ #
6
+ # queue = SidekiqReliableFetch::WorkingQueue.new("mailer")
7
+ # queue.each do |job|
8
+ # job.klass # => 'MyWorker'
9
+ # job.args # => [1, 2, 3]
10
+ # end
11
+ #
12
+ class WorkingQueue
13
+ include Enumerable
14
+
15
+ def self.all
16
+ Sidekiq.redis { |c| c.keys('queue:*:working') }
17
+ .sort
18
+ .map { |q| SidekiqReliableFetch::WorkingQueue.new(q) }
19
+ end
20
+
21
+ attr_reader :name
22
+
23
+ def initialize(name)
24
+ @name = name
25
+ end
26
+
27
+ def size
28
+ Sidekiq.redis { |con| con.llen(@name) }
29
+ end
30
+
31
+ def each
32
+ initial_size = size
33
+ deleted_size = 0
34
+ page = 0
35
+ page_size = 50
36
+
37
+ loop do
38
+ range_start = page * page_size - deleted_size
39
+ range_end = page * page_size - deleted_size + (page_size - 1)
40
+ entries = Sidekiq.redis do |conn|
41
+ conn.lrange @name, range_start, range_end
42
+ end
43
+ break if entries.empty?
44
+ page += 1
45
+ entries.each do |entry|
46
+ yield Sidekiq::Job.new(entry, @name)
47
+ end
48
+ deleted_size = initial_size - size
49
+ end
50
+ end
51
+
52
+ def find_job(jid)
53
+ detect { |j| j.jid == jid }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'api'
2
+
3
+ module SidekiqReliableFetch
4
+ # Hook into *Sidekiq::Web* Sinatra app which adds a new '/working' page
5
+ module Web
6
+ VIEW_PATH = File.expand_path('../../../web/views', __FILE__)
7
+
8
+ def self.registered(app)
9
+ app.get '/working' do
10
+ @queues = SidekiqReliableFetch::WorkingQueue.all
11
+ erb File.read(File.join(VIEW_PATH, 'working_queues.erb'))
12
+ end
13
+
14
+ app.get '/working/:queue' do
15
+ @queue = SidekiqReliableFetch::WorkingQueue.new(params[:queue])
16
+ erb File.read(File.join(VIEW_PATH, 'working_queue.erb'))
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ require 'sidekiq/web' unless defined?(Sidekiq::Web)
23
+ Sidekiq::Web.register(SidekiqReliableFetch::Web)
24
+ Sidekiq::Web.tabs['Working'] = 'working'
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+ module Sidekiq
3
+ class ReliableFetcher
4
+ WORKING_QUEUE = 'working'
5
+ DEFAULT_DEAD_AFTER = 60 * 60 * 24 # 24 hours
6
+ DEFAULT_CLEANING_INTERVAL = 5000 # clean each N processed jobs
7
+ IDLE_TIMEOUT = 5 # seconds
8
+
9
+ UnitOfWork = Struct.new(:queue, :job) do
10
+ def acknowledge
11
+ # NOTE LREM is O(n), so depending on the type of jobs and their average
12
+ # duration, another data structure might be more suited.
13
+ # But as there should not be too much jobs in this queue in the same time,
14
+ # it's probably ok.
15
+ Sidekiq.redis { |conn| conn.lrem("#{queue}:#{WORKING_QUEUE}", 1, job) }
16
+ end
17
+
18
+ def queue_name
19
+ queue.sub(/.*queue:/, '')
20
+ end
21
+
22
+ def requeue
23
+ Sidekiq.redis do |conn|
24
+ conn.pipelined do
25
+ conn.lpush(queue, job)
26
+ conn.lrem("#{queue}:#{WORKING_QUEUE}", 1, job)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.setup_reliable_fetch!(config)
33
+ config.options[:fetch] = Sidekiq::ReliableFetcher
34
+ config.on(:startup) do
35
+ requeue_on_startup!(config.options[:queues])
36
+ end
37
+ end
38
+
39
+ def initialize(options)
40
+ queues = options[:queues].map { |q| "queue:#{q}" }
41
+
42
+ @unique_queues = queues.uniq
43
+ @queues_iterator = queues.shuffle.cycle
44
+ @queues_size = queues.size
45
+
46
+ @nb_fetched_jobs = 0
47
+ @cleaning_interval = options[:cleaning_interval] || DEFAULT_CLEANING_INTERVAL
48
+ @consider_dead_after = options[:consider_dead_after] || DEFAULT_DEAD_AFTER
49
+
50
+ Sidekiq.logger.info { "GitLab reliable fetch activated!" }
51
+ end
52
+
53
+ def retrieve_work
54
+ clean_working_queues! if @cleaning_interval != -1 && @nb_fetched_jobs >= @cleaning_interval
55
+
56
+ @queues_size.times do
57
+ queue = @queues_iterator.next
58
+ work = Sidekiq.redis { |conn| conn.rpoplpush(queue, "#{queue}:#{WORKING_QUEUE}") }
59
+
60
+ if work
61
+ @nb_fetched_jobs += 1
62
+ return UnitOfWork.new(queue, work)
63
+ end
64
+ end
65
+
66
+ # We didn't find a job in any of the configured queues. Let's sleep a bit
67
+ # to avoid uselessly burning too much CPU
68
+ sleep(IDLE_TIMEOUT)
69
+
70
+ nil
71
+ end
72
+
73
+ def self.requeue_on_startup!(queues)
74
+ Sidekiq.logger.debug { "Re-queueing working jobs" }
75
+
76
+ counter = 0
77
+
78
+ Sidekiq.redis do |conn|
79
+ queues.uniq.each do |queue|
80
+ while conn.rpoplpush("queue:#{queue}:#{WORKING_QUEUE}", "queue:#{queue}")
81
+ counter += 1
82
+ end
83
+ end
84
+ end
85
+
86
+ Sidekiq.logger.debug { "Re-queued #{counter} jobs" }
87
+ end
88
+
89
+ # By leaving this as a class method, it can be pluggable and used by the Manager actor. Making it
90
+ # an instance method will make it async to the Fetcher actor
91
+ def self.bulk_requeue(inprogress, options)
92
+ return if inprogress.empty?
93
+
94
+ Sidekiq.logger.debug { "Re-queueing terminated jobs" }
95
+
96
+ Sidekiq.redis do |conn|
97
+ conn.pipelined do
98
+ inprogress.each do |unit_of_work|
99
+ conn.lpush("#{unit_of_work.queue}", unit_of_work.job)
100
+ conn.lrem("#{unit_of_work.queue}:#{WORKING_QUEUE}", 1, unit_of_work.job)
101
+ end
102
+ end
103
+ end
104
+
105
+ Sidekiq.logger.info("Pushed #{inprogress.size} messages back to Redis")
106
+ rescue => ex
107
+ Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
108
+ end
109
+
110
+ private
111
+
112
+ # Detect "old" jobs and requeue them because the worker they were assigned
113
+ # to probably failed miserably.
114
+ # NOTE Potential problem here if a specific job always make a worker
115
+ # really fail.
116
+ def clean_working_queues!
117
+ Sidekiq.logger.debug "Cleaning working queues"
118
+
119
+ @unique_queues.each do |queue|
120
+ clean_working_queue!(queue)
121
+ end
122
+
123
+ @nb_fetched_jobs = 0
124
+ end
125
+
126
+ def clean_working_queue!(queue)
127
+ Sidekiq.redis do |conn|
128
+ working_jobs = conn.lrange("#{queue}:#{WORKING_QUEUE}", 0, -1)
129
+ working_jobs.each do |job|
130
+ enqueued_at = Sidekiq.load_json(job)['enqueued_at'].to_i
131
+ job_duration = Time.now.to_i - enqueued_at
132
+
133
+ next if job_duration < @consider_dead_after
134
+
135
+ Sidekiq.logger.info "Requeued a dead job from #{queue}:#{WORKING_QUEUE}"
136
+
137
+ conn.lpush("#{queue}", job)
138
+ conn.lrem("#{queue}:#{WORKING_QUEUE}", 1, job)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,25 @@
1
+ <header class="row">
2
+ <div class="col-sm-5">
3
+ <h3><%= t('Working jobs') %></h3>
4
+ </div>
5
+ </header>
6
+
7
+ <% if @queue.size > 0 %>
8
+ <table class="table table-striped table-bordered table-white">
9
+ <thead>
10
+ <tr>
11
+ <th><%= t('Job') %></th>
12
+ <th><%= t('Arguments') %></th>
13
+ </tr>
14
+ </thead>
15
+ <% @queue.each do |entry| %>
16
+ <td><%= entry.display_class %></td>
17
+ <td>
18
+ <div class="args"><%= display_args(entry.display_args) %></div>
19
+ </td>
20
+ </tr>
21
+ <% end %>
22
+ </table>
23
+ <% else %>
24
+ <div class="alert alert-success"><%= t('No working job found') %></div>
25
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <h3><%= t('Working queues') %></h3>
2
+
3
+ <div class="table_container">
4
+ <table class="queues table table-hover table-bordered table-striped table-white">
5
+ <thead>
6
+ <th><%= t('Queue') %></th>
7
+ <th><%= t('Size') %></th>
8
+ </thead>
9
+ <% @queues.each do |queue| %>
10
+ <tr>
11
+ <td>
12
+ <a href="<%= root_path %>working/<%= queue.name %>"><%= queue.name %></a>
13
+ </td>
14
+ <td><%= number_with_delimiter(queue.size) %></td>
15
+ </tr>
16
+ <% end %>
17
+ </table>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-sidekiq-fetcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TEA
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-12-14 00:00:00.000000000 Z
12
+ date: 2018-09-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -32,28 +32,17 @@ extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
34
  - ".gitignore"
35
- - ".gitlab-ci.yml"
36
- - ".rspec"
37
- - Gemfile
38
- - Gemfile.lock
39
35
  - LICENSE
36
+ - README-GITLAB.md
40
37
  - README.md
41
- - RELEASE-GITLAB.md
42
38
  - gitlab-sidekiq-fetcher.gemspec
43
39
  - lib/sidekiq-reliable-fetch.rb
44
- - lib/sidekiq/base_reliable_fetch.rb
45
- - lib/sidekiq/reliable_fetch.rb
46
- - lib/sidekiq/semi_reliable_fetch.rb
47
- - spec/base_reliable_fetch_spec.rb
48
- - spec/fetch_shared_examples.rb
49
- - spec/reliable_fetch_spec.rb
50
- - spec/semi_reliable_fetch_spec.rb
51
- - spec/spec_helper.rb
52
- - test/README.md
53
- - test/config.rb
54
- - test/reliability_test.rb
55
- - test/worker.rb
56
- homepage: https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/
40
+ - lib/sidekiq-reliable-fetch/api.rb
41
+ - lib/sidekiq-reliable-fetch/web.rb
42
+ - lib/sidekiq/reliable_fetcher.rb
43
+ - web/views/working_queue.erb
44
+ - web/views/working_queues.erb
45
+ homepage: https://github.com/TEA-ebook/sidekiq-reliable-fetch
57
46
  licenses:
58
47
  - LGPL-3.0
59
48
  metadata: {}
@@ -73,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
62
  version: '0'
74
63
  requirements: []
75
64
  rubyforge_project:
76
- rubygems_version: 2.7.6
65
+ rubygems_version: 2.5.2
77
66
  signing_key:
78
67
  specification_version: 4
79
68
  summary: Reliable fetch extension for Sidekiq
data/.gitlab-ci.yml DELETED
@@ -1,53 +0,0 @@
1
- image: "ruby:2.5"
2
-
3
- before_script:
4
- - ruby -v
5
- - which ruby
6
- - gem install bundler --no-ri --no-rdoc
7
- - bundle install --jobs $(nproc) "${FLAGS[@]}"
8
-
9
- variables:
10
- REDIS_URL: "redis://redis"
11
-
12
- rspec:
13
- stage: test
14
- coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
15
- script:
16
- - bundle exec rspec
17
- services:
18
- - redis:alpine
19
- artifacts:
20
- expire_in: 31d
21
- when: always
22
- paths:
23
- - coverage/
24
-
25
- .integration:
26
- stage: test
27
- script:
28
- - cd test
29
- - bundle exec ruby reliability_test.rb
30
- services:
31
- - redis:alpine
32
-
33
- integration_semi:
34
- extends: .integration
35
- variables:
36
- JOB_FETCHER: semi
37
-
38
- integration_reliable:
39
- extends: .integration
40
- variables:
41
- JOB_FETCHER: reliable
42
-
43
-
44
- integration_basic:
45
- extends: .integration
46
- allow_failure: yes
47
- variables:
48
- JOB_FETCHER: basic
49
-
50
-
51
- # rubocop:
52
- # script:
53
- # - bundle exec rubocop