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 +5 -5
- data/.gitignore +1 -1
- data/{RELEASE-GITLAB.md → README-GITLAB.md} +0 -0
- data/README.md +4 -15
- data/gitlab-sidekiq-fetcher.gemspec +4 -2
- data/lib/sidekiq-reliable-fetch.rb +2 -3
- data/lib/sidekiq-reliable-fetch/api.rb +56 -0
- data/lib/sidekiq-reliable-fetch/web.rb +24 -0
- data/lib/sidekiq/reliable_fetcher.rb +143 -0
- data/web/views/working_queue.erb +25 -0
- data/web/views/working_queues.erb +17 -0
- metadata +10 -21
- data/.gitlab-ci.yml +0 -53
- data/.rspec +0 -1
- data/Gemfile +0 -12
- data/Gemfile.lock +0 -50
- data/lib/sidekiq/base_reliable_fetch.rb +0 -185
- data/lib/sidekiq/reliable_fetch.rb +0 -40
- data/lib/sidekiq/semi_reliable_fetch.rb +0 -44
- data/spec/base_reliable_fetch_spec.rb +0 -73
- data/spec/fetch_shared_examples.rb +0 -118
- data/spec/reliable_fetch_spec.rb +0 -7
- data/spec/semi_reliable_fetch_spec.rb +0 -7
- data/spec/spec_helper.rb +0 -115
- data/test/README.md +0 -34
- data/test/config.rb +0 -31
- data/test/reliability_test.rb +0 -116
- data/test/worker.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 00056b1430ab40c06094813fcf06296ef24eef40
|
4
|
+
data.tar.gz: 1ee1138aeaed7c353e2eaddab6982d7a1d384d82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c141c0c16201c5b788268b3d829cae2b1820682fb907f56f61e75aa205c223d83c519c544ac3bf0d0c8f7dd5758c3f754e93bbfe184e92b80c2bd9e134b06a3
|
7
|
+
data.tar.gz: d1cef7c3751a5b6dd04155a21d4ef9742bb274552eb66ab41cf2044cbcc94f21ca494aac7ee3e6db01fd9891e259e8bb2e52bc26c5d3299dd72b84f5088fe0f0
|
data/.gitignore
CHANGED
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
|
-
|
10
|
-
|
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::
|
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.
|
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://
|
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
|
@@ -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.
|
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
|
+
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/
|
45
|
-
- lib/sidekiq/
|
46
|
-
- lib/sidekiq/
|
47
|
-
-
|
48
|
-
-
|
49
|
-
|
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.
|
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
|