gitlab-sidekiq-fetcher 0.1.0 → 0.3.0

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
- 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