sidekiq-fairplay 0.0.1 → 0.0.2

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
2
  SHA256:
3
- metadata.gz: 8da6198834317f72485926227a1a0798f26fa2cb89506248b8430f36445c3d8e
4
- data.tar.gz: 1b09fd7ff17a68822a77d2944a22f9913a3048660e591aebb0fcdcf71ef69b8f
3
+ metadata.gz: 90c65bc475c5878775b1e24fee917f48c7f24021c43846d1d7d4d4659b9cbcca
4
+ data.tar.gz: 883a02caa000007968db911404f77798cc3f2252360550c8a00146b8ef7e794c
5
5
  SHA512:
6
- metadata.gz: 74bfa63effdd8815a733acb146f7acd5bfa6203eef103c22ba79f1b170fc3de9085b52cd6086b37d86d32d7da2110af2896e8ce562f191648c47c6e924b33292
7
- data.tar.gz: f06564822787aa2e4c01072a450f12dc7ba78539457bef069ed6c38708d8989bf03b1881b4bd9a26e3e0ae2ffa3002bf455bcc959b403424cafc019728be98fb
6
+ metadata.gz: 15705758d8ce779bb448d285a26ac8f79d97c074dd80758ad02eafd90fc2300296c6bfa3f2be28bc4f06fac96bb46f5601ab1f57b7e5684642070c7e7ae90088
7
+ data.tar.gz: 0ab155ded33d5eeafeb3c5719d88aca244af590f5596cd4e6326a2552f455ac96fffe4d33878a4d5ed90c6aaef45325d3d3654b58c4879f203219a06474f6a07
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Fairplay
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-fairplay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Baygeldin
@@ -179,32 +179,18 @@ dependencies:
179
179
  version: '7.0'
180
180
  email:
181
181
  - a.baygeldin@gmail.com
182
- executables:
183
- - console
184
- - setup
182
+ executables: []
185
183
  extensions: []
186
184
  extra_rdoc_files: []
187
185
  files:
188
- - ".github/workflows/ci.yml"
189
- - ".gitignore"
190
- - ".standard.yml"
191
- - Gemfile
192
186
  - LICENSE
193
187
  - README.md
194
- - Rakefile
195
- - bin/console
196
- - bin/setup
197
- - gemfiles/sidekiq_7.gemfile
198
- - gemfiles/sidekiq_8.gemfile
199
188
  - lib/sidekiq/fairplay.rb
200
189
  - lib/sidekiq/fairplay/config.rb
201
190
  - lib/sidekiq/fairplay/middleware.rb
202
191
  - lib/sidekiq/fairplay/planner.rb
203
192
  - lib/sidekiq/fairplay/redis.rb
204
193
  - lib/sidekiq/fairplay/version.rb
205
- - sidekiq-fairplay.gemspec
206
- - spec/sidekiq/fairplay_spec.rb
207
- - spec/spec_helper.rb
208
194
  homepage: http://github.com/baygeldin/sidekiq-fairplay
209
195
  licenses:
210
196
  - MIT
@@ -1,76 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ main, master ]
6
- pull_request:
7
- branches: [ main, master ]
8
-
9
- jobs:
10
- test:
11
- name: Test (Ruby ${{ matrix.ruby }}, Sidekiq ${{ matrix.sidekiq }})
12
- runs-on: ubuntu-latest
13
- timeout-minutes: 15
14
- strategy:
15
- fail-fast: false
16
- matrix:
17
- ruby: ['3.4']
18
- sidekiq: ['7', '8']
19
-
20
- services:
21
- redis:
22
- image: redis:7-alpine
23
- ports:
24
- - 6379:6379
25
- options: >-
26
- --health-cmd "redis-cli ping || exit 1"
27
- --health-interval 10s
28
- --health-timeout 5s
29
- --health-retries 5
30
-
31
- steps:
32
- - name: Checkout
33
- uses: actions/checkout@v4
34
-
35
- - name: Set up Ruby
36
- uses: ruby/setup-ruby@v1
37
- with:
38
- ruby-version: ${{ matrix.ruby }}
39
- bundler-cache: true
40
-
41
- - name: Select Gemfile for Sidekiq ${{ matrix.sidekiq }}
42
- run: |
43
- export BUNDLE_GEMFILE="gemfiles/sidekiq_${{ matrix.sidekiq }}.gemfile"
44
- echo "BUNDLE_GEMFILE=$BUNDLE_GEMFILE" >> $GITHUB_ENV
45
-
46
- - name: Bundle install
47
- run: bundle install --jobs 4 --retry 3
48
-
49
- - name: Run specs
50
- env:
51
- REDIS_URL: redis://localhost:6379/1
52
- run: |
53
- bundle exec rspec --format progress
54
-
55
- - uses: qltysh/qlty-action/coverage@v2
56
- if: matrix.sidekiq == '8' && matrix.ruby == '3.4'
57
- with:
58
- token: ${{secrets.QLTY_COVERAGE_TOKEN}}
59
- files: coverage/.resultset.json
60
-
61
- lint:
62
- name: Lint (standardrb)
63
- runs-on: ubuntu-latest
64
- continue-on-error: true
65
- steps:
66
- - name: Checkout
67
- uses: actions/checkout@v4
68
-
69
- - name: Set up Ruby
70
- uses: ruby/setup-ruby@v1
71
- with:
72
- ruby-version: '3.4'
73
- bundler-cache: true
74
-
75
- - name: Run StandardRB
76
- run: bundle exec standardrb
data/.gitignore DELETED
@@ -1,37 +0,0 @@
1
- *.gem
2
- *.rbc
3
- /.config
4
- /coverage/
5
- /InstalledFiles
6
- /pkg/
7
- /spec/reports/
8
- /spec/examples.txt
9
- /test/tmp/
10
- /test/version_tmp/
11
- /tmp/
12
- spec/examples.txt
13
- .byebug_history
14
-
15
- ## Documentation cache and generated files
16
- /.yardoc/
17
- /_yardoc/
18
- /doc/
19
- /rdoc/
20
-
21
- ## Environment normalization
22
- /.bundle/
23
- /vendor/bundle
24
- /lib/bundler/man/
25
-
26
- # System files
27
- .DS_Store
28
-
29
- # Editors
30
- .vscode
31
- .ruby-lsp
32
-
33
- # Unnecessary for Ruby gems
34
- Gemfile.lock
35
- .ruby-version
36
- .ruby-gemset
37
- .tool-versions
data/.standard.yml DELETED
@@ -1,9 +0,0 @@
1
- plugins:
2
- - standard-performance
3
- - standard-rspec
4
-
5
- ignore:
6
- - 'spec/**/*_spec.rb':
7
- - RSpec/MultipleMemoizedHelpers
8
- - RSpec/MultipleExpectations
9
- - RSpec/ExampleLength
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec
data/Rakefile DELETED
@@ -1,7 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
- require "standard/rake"
4
-
5
- RSpec::Core::RakeTask.new(:spec)
6
-
7
- task default: %i[spec standard]
data/bin/console DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "sidekiq/fairplay"
5
-
6
- require "pry"
7
- Pry.start
data/bin/setup DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
@@ -1,5 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec path: "../"
4
-
5
- gem "sidekiq", "~> 7.0"
@@ -1,5 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec path: "../"
4
-
5
- gem "sidekiq", "~> 8.0"
@@ -1,37 +0,0 @@
1
- lib = File.expand_path("lib", __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "sidekiq/fairplay/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "sidekiq-fairplay"
7
- spec.version = Sidekiq::Fairplay::VERSION
8
- spec.authors = ["Alexander Baygeldin"]
9
- spec.email = ["a.baygeldin@gmail.com"]
10
- spec.summary = <<~SUMMARY
11
- Make Sidekiq play fair — dynamic job prioritization for multi-tenant apps.
12
- SUMMARY
13
- spec.homepage = "http://github.com/baygeldin/sidekiq-fairplay"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.require_paths = ["lib"]
19
-
20
- spec.required_ruby_version = ">= 3.4.0"
21
-
22
- spec.add_development_dependency "bundler", "~> 2.0"
23
- spec.add_development_dependency "pry", "~> 0.15"
24
- spec.add_development_dependency "rake", "~> 13.0"
25
- spec.add_development_dependency "rspec", "~> 3.0"
26
- spec.add_development_dependency "rspec-sidekiq", "~> 5.0"
27
- spec.add_development_dependency "standard", "~> 1.0"
28
- spec.add_development_dependency "standard-performance", "~> 1.0"
29
- spec.add_development_dependency "standard-rspec", "~> 0.3"
30
- spec.add_development_dependency "simplecov", "~> 0.22"
31
- spec.add_development_dependency "timecop", "~> 0.9"
32
-
33
- spec.add_dependency "activesupport", "~> 7.0"
34
- spec.add_runtime_dependency "sidekiq", "~> 7.0"
35
-
36
- spec.metadata["rubygems_mfa_required"] = "true"
37
- end
@@ -1,301 +0,0 @@
1
- require "spec_helper"
2
-
3
- class RegularJob
4
- include Sidekiq::Job
5
-
6
- def perform(foo)
7
- end
8
- end
9
-
10
- class FairplayJob
11
- include Sidekiq::Job
12
- include Sidekiq::Fairplay::Job
13
-
14
- def perform(tenant_key, foo)
15
- end
16
- end
17
-
18
- RSpec.describe Sidekiq::Fairplay do
19
- before do
20
- FairplayJob.sidekiq_fairplay_options \
21
- enqueue_interval:,
22
- enqueue_jobs:,
23
- planner_queue:,
24
- planner_lock_ttl:,
25
- latency_threshold:,
26
- tenant_key:,
27
- tenant_weights:
28
- end
29
-
30
- let(:enqueue_interval) { 1 }
31
- let(:enqueue_jobs) { 10 }
32
- let(:planner_queue) { "default" }
33
- let(:planner_lock_ttl) { 60 }
34
- let(:latency_threshold) { 60 }
35
- let(:tenant_key) { ->(tenant_key, *_args) { tenant_key } }
36
- let(:tenant_weights) { ->(tenant_keys) { tenant_keys.to_h { |tid| [tid, 1] } } }
37
-
38
- describe "fairness (probabilistic)" do
39
- # Seed Ruby's PRNG for deterministic results
40
- around do |example|
41
- prev = srand(1234)
42
- example.run
43
- ensure
44
- srand(prev)
45
- end
46
-
47
- let(:enqueue_jobs) { 1000 }
48
- let(:tenant_weights) do
49
- ->(tenant_ids) do
50
- mapping = {"t1" => 1, "t2" => 3, "t3" => 6}
51
- tenant_ids.to_h { |tid| [tid, mapping.fetch(tid, 1)] }
52
- end
53
- end
54
-
55
- it "enqueues approximately proportional to weights" do
56
- enqueue_jobs.times do |i|
57
- FairplayJob.perform_async("t1", "a#{i}")
58
- FairplayJob.perform_async("t2", "b#{i}")
59
- FairplayJob.perform_async("t3", "c#{i}")
60
- end
61
-
62
- Sidekiq::Fairplay::Planner.new.perform("FairplayJob")
63
-
64
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(enqueue_jobs)
65
-
66
- jobs_per_tenant = FairplayJob.jobs.each_with_object(Hash.new(0)) do |job, memo|
67
- memo[job["args"].first] += 1
68
- end
69
-
70
- expected_jobs_per_tenant = {"t1" => 100, "t2" => 300, "t3" => 600}
71
- tolerance = 0.25 # 25% tolerance to avoid flakiness across Ruby versions
72
-
73
- expected_jobs_per_tenant.each do |tid, exp|
74
- low = (exp * (1 - tolerance)).floor
75
- high = (exp * (1 + tolerance)).ceil
76
-
77
- expect(jobs_per_tenant[tid]).to be_between(low, high).inclusive
78
- end
79
- end
80
- end
81
-
82
- describe "basic functionality" do
83
- it "intercepts fairplay jobs and enqueues them later" do
84
- FairplayJob.perform_async("t1", "a")
85
- FairplayJob.perform_async("t2", "b")
86
- FairplayJob.perform_async("t3", "c")
87
-
88
- expect(FairplayJob).not_to have_enqueued_sidekiq_job
89
- expect(Sidekiq::Fairplay::Planner)
90
- .to have_enqueued_sidekiq_job("FairplayJob")
91
- .exactly(1)
92
- .immediately
93
-
94
- Sidekiq::Fairplay::Planner.perform_one
95
-
96
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(3)
97
- expect(FairplayJob).to have_enqueued_sidekiq_job("t1", "a")
98
- expect(FairplayJob).to have_enqueued_sidekiq_job("t2", "b")
99
- expect(FairplayJob).to have_enqueued_sidekiq_job("t3", "c")
100
- end
101
-
102
- context "with custom planner queue" do
103
- let(:planner_queue) { "whatever" }
104
-
105
- it "enqueues the planner job on the configured queue" do
106
- FairplayJob.perform_async("t1", "a")
107
-
108
- Sidekiq::Fairplay::Planner.perform_one
109
-
110
- expect(Sidekiq::Fairplay::Planner)
111
- .to have_enqueued_sidekiq_job("FairplayJob")
112
- .on(planner_queue)
113
- .in(enqueue_interval.to_i)
114
-
115
- expect(FairplayJob)
116
- .to have_enqueued_sidekiq_job("t1", "a")
117
- .on("default") # default queue for FairplayJob
118
- end
119
- end
120
-
121
- context "when latency threshold exceeded" do
122
- let(:queue) { instance_double(Sidekiq::Queue) }
123
-
124
- before do
125
- allow(Sidekiq::Queue).to receive(:new).and_return(queue)
126
- allow(queue).to receive(:latency).and_return(latency_threshold.to_i + 1)
127
- end
128
-
129
- it "reschedules the planner without enqueuing jobs" do
130
- FairplayJob.perform_async("t1", "a")
131
-
132
- Sidekiq::Fairplay::Planner.perform_one
133
-
134
- expect(FairplayJob).not_to have_enqueued_sidekiq_job
135
- expect(Sidekiq::Fairplay::Planner)
136
- .to have_enqueued_sidekiq_job("FairplayJob")
137
- .in(enqueue_interval.to_i)
138
- end
139
- end
140
-
141
- context "with custom weights" do
142
- let(:tenant_weights) do
143
- ->(tenant_ids) do
144
- tenant_ids.to_h do |tid|
145
- [tid, (tid == "t1") ? 1 : 0]
146
- end
147
- end
148
- end
149
-
150
- it "uses weights to prefer specific tenant" do
151
- FairplayJob.perform_async("t1", "a")
152
- FairplayJob.perform_async("t2", "b")
153
-
154
- Sidekiq::Fairplay::Planner.perform_one
155
-
156
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(1)
157
- expect(FairplayJob).to have_enqueued_sidekiq_job("t1", "a")
158
- expect(FairplayJob).not_to have_enqueued_sidekiq_job("t2", "b")
159
- end
160
- end
161
-
162
- context "when too many jobs in the queue" do
163
- let(:enqueue_jobs) { 1 }
164
-
165
- it "respects the enqueue_jobs limit" do
166
- FairplayJob.perform_async("t1", "a")
167
- FairplayJob.perform_async("t1", "b")
168
-
169
- Sidekiq::Fairplay::Planner.perform_one
170
-
171
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(1)
172
- expect(FairplayJob)
173
- .to have_enqueued_sidekiq_job("t1", "a")
174
- .or have_enqueued_sidekiq_job("t1", "b")
175
- end
176
- end
177
- end
178
-
179
- describe "edge cases" do
180
- it "ignores unknown job class" do
181
- Sidekiq::Fairplay::Planner.new.perform("UnknownJob")
182
-
183
- expect(Sidekiq::Fairplay::Planner).not_to have_enqueued_sidekiq_job
184
- expect(FairplayJob).not_to have_enqueued_sidekiq_job
185
- end
186
-
187
- it "has no effect on regular jobs" do
188
- RegularJob.perform_async("foo")
189
-
190
- expect(RegularJob).to have_enqueued_sidekiq_job("foo")
191
- expect(Sidekiq::Fairplay::Planner).not_to have_enqueued_sidekiq_job
192
- end
193
-
194
- it "has no effect on scheduled jobs" do
195
- FairplayJob.perform_in(5, "t1", "a")
196
-
197
- expect(FairplayJob).to have_enqueued_sidekiq_job("t1", "a").in(5)
198
- expect(Sidekiq::Fairplay::Planner).not_to have_enqueued_sidekiq_job
199
- end
200
-
201
- context "with zero weights for all tenants" do
202
- let(:tenant_weights) do
203
- ->(tenant_ids) { tenant_ids.to_h { |tid| [tid, 0] } }
204
- end
205
-
206
- it "enqueues no jobs" do
207
- FairplayJob.perform_async("t1", "a")
208
- FairplayJob.perform_async("t2", "b")
209
-
210
- Sidekiq::Fairplay::Planner.perform_one
211
-
212
- expect(FairplayJob).not_to have_enqueued_sidekiq_job
213
- end
214
- end
215
- end
216
-
217
- describe "errors" do
218
- let(:tenant_key) { ->(_tid, *_args) {} }
219
-
220
- it "raises when tenant key resolves to nil" do
221
- tenant_key
222
-
223
- expect { FairplayJob.perform_async("t1", "a") }
224
- .to raise_error(ArgumentError, /tenant key cannot be nil/)
225
- end
226
- end
227
-
228
- describe "implementation details" do
229
- it "reschedules planning for the next interval" do
230
- FairplayJob.perform_async("t1", "a")
231
-
232
- Sidekiq::Fairplay::Planner.perform_one
233
-
234
- expect(Sidekiq::Fairplay::Planner)
235
- .to have_enqueued_sidekiq_job("FairplayJob")
236
- .in(enqueue_interval.to_i)
237
- end
238
-
239
- context "when planner_lock_ttl is being held" do
240
- let(:planner_lock_ttl) { 42 }
241
-
242
- before do
243
- redis = Sidekiq::Fairplay::Redis.new
244
- redis.try_acquire_planner_lock(FairplayJob, "some_jid")
245
- end
246
-
247
- it "blocks planning until the TTL expires" do
248
- FairplayJob.perform_async("t1", "a")
249
-
250
- Sidekiq::Fairplay::Planner.perform_one
251
-
252
- expect(FairplayJob).not_to have_enqueued_sidekiq_job
253
- expect(Sidekiq::Fairplay::Planner)
254
- .to have_enqueued_sidekiq_job("FairplayJob")
255
- .in(enqueue_interval.to_i)
256
- end
257
- end
258
-
259
- context "when tenant_key and tenant_weights refer to class methods" do
260
- before do
261
- class << FairplayJob
262
- def static_tenant_key(tid, *_args) = tid
263
- def static_tenant_weights(tids) = tids.to_h { |tid| [tid, 1] }
264
- end
265
- end
266
-
267
- let(:tenant_key) { ->(tid, *args) { static_tenant_key(tid, *args) } }
268
- let(:tenant_weights) { ->(tids) { static_tenant_weights(tids) } }
269
-
270
- it "works as expected" do
271
- FairplayJob.perform_async("t1", "a")
272
- FairplayJob.perform_async("t2", "b")
273
-
274
- Sidekiq::Fairplay::Planner.perform_one
275
-
276
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(2)
277
- expect(FairplayJob).to have_enqueued_sidekiq_job("t1", "a")
278
- expect(FairplayJob).to have_enqueued_sidekiq_job("t2", "b")
279
- end
280
- end
281
-
282
- context "when using ActiveSupport::Duration" do
283
- let(:enqueue_interval) { 1.minute }
284
- let(:latency_threshold) { 1.hour }
285
- let(:planner_lock_ttl) { 10.seconds }
286
-
287
- it "handles durations correctly" do
288
- FairplayJob.perform_async("t1", "a")
289
-
290
- Sidekiq::Fairplay::Planner.perform_one
291
-
292
- expect(Sidekiq::Fairplay::Planner)
293
- .to have_enqueued_sidekiq_job("FairplayJob")
294
- .in(enqueue_interval.to_i)
295
-
296
- expect(FairplayJob).to have_enqueued_sidekiq_job.exactly(1)
297
- expect(FairplayJob).to have_enqueued_sidekiq_job("t1", "a")
298
- end
299
- end
300
- end
301
- end
data/spec/spec_helper.rb DELETED
@@ -1,63 +0,0 @@
1
- $LOAD_PATH << "." unless $LOAD_PATH.include?(".")
2
-
3
- require "rubygems"
4
- require "bundler/setup"
5
- require "timecop"
6
- require "simplecov"
7
-
8
- require "sidekiq"
9
- require "rspec-sidekiq"
10
- require "sidekiq/fairplay"
11
- require "pry"
12
-
13
- SimpleCov.start do
14
- add_filter "spec"
15
- end
16
-
17
- Sidekiq::Fairplay.logger = nil
18
-
19
- Sidekiq.configure_client do |config|
20
- config.redis = {db: 1}
21
- config.logger = nil
22
-
23
- config.client_middleware do |chain|
24
- chain.add Sidekiq::Fairplay::Middleware
25
- end
26
- end
27
-
28
- Sidekiq.configure_server do |config|
29
- config.redis = {db: 1}
30
- config.logger = nil
31
-
32
- config.client_middleware do |chain|
33
- chain.add Sidekiq::Fairplay::Middleware
34
- end
35
- end
36
-
37
- RSpec::Sidekiq.configure do |config|
38
- config.clear_all_enqueued_jobs = true
39
- config.warn_when_jobs_not_processed_by_sidekiq = false
40
- end
41
-
42
- RSpec.configure do |config|
43
- config.order = :random
44
- config.run_all_when_everything_filtered = true
45
- config.example_status_persistence_file_path = "spec/examples.txt"
46
-
47
- config.before do
48
- Sidekiq.redis do |conn|
49
- keys = conn.call("KEYS", "fairplay*")
50
- keys.each { |key| conn.call("DEL", key) }
51
- end
52
- end
53
-
54
- config.before do
55
- Timecop.freeze
56
- end
57
-
58
- config.after do
59
- Timecop.return
60
- end
61
- end
62
-
63
- $LOAD_PATH << File.join(File.dirname(__FILE__), "..", "lib")