sidekiq-status 0.6.0 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yaml +53 -0
  3. data/.gitignore +4 -1
  4. data/.gitlab-ci.yml +17 -0
  5. data/Appraisals +11 -0
  6. data/CHANGELOG.md +36 -18
  7. data/Gemfile +0 -4
  8. data/README.md +129 -50
  9. data/Rakefile +2 -0
  10. data/gemfiles/sidekiq_6.1.gemfile +7 -0
  11. data/gemfiles/sidekiq_6.x.gemfile +7 -0
  12. data/gemfiles/sidekiq_7.x.gemfile +7 -0
  13. data/lib/sidekiq-status/client_middleware.rb +46 -8
  14. data/lib/sidekiq-status/redis_adapter.rb +18 -0
  15. data/lib/sidekiq-status/redis_client_adapter.rb +14 -0
  16. data/lib/sidekiq-status/server_middleware.rb +76 -20
  17. data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
  18. data/lib/sidekiq-status/storage.rb +19 -9
  19. data/lib/sidekiq-status/testing/inline.rb +10 -0
  20. data/lib/sidekiq-status/version.rb +1 -1
  21. data/lib/sidekiq-status/web.rb +116 -25
  22. data/lib/sidekiq-status/worker.rb +12 -5
  23. data/lib/sidekiq-status.rb +40 -5
  24. data/sidekiq-status.gemspec +7 -4
  25. data/spec/environment.rb +1 -0
  26. data/spec/lib/sidekiq-status/client_middleware_spec.rb +15 -12
  27. data/spec/lib/sidekiq-status/server_middleware_spec.rb +66 -22
  28. data/spec/lib/sidekiq-status/web_spec.rb +62 -15
  29. data/spec/lib/sidekiq-status/worker_spec.rb +19 -1
  30. data/spec/lib/sidekiq-status_spec.rb +94 -21
  31. data/spec/spec_helper.rb +104 -26
  32. data/spec/support/test_jobs.rb +71 -6
  33. data/web/sidekiq-status-single-web.png +0 -0
  34. data/web/views/status.erb +118 -0
  35. data/web/views/status_not_found.erb +6 -0
  36. data/web/views/statuses.erb +108 -22
  37. metadata +74 -13
  38. data/.travis.yml +0 -16
  39. data/gemfiles/Gemfile.sidekiq-2 +0 -5
@@ -5,20 +5,19 @@ describe Sidekiq::Status::ServerMiddleware do
5
5
  let!(:redis) { Sidekiq.redis { |conn| conn } }
6
6
  let!(:job_id) { SecureRandom.hex(12) }
7
7
 
8
- # Clean Redis before each test
9
- # Seems like flushall has no effect on recently published messages,
10
- # so we should wait till they expire
11
- before { redis.flushall; sleep 0.1 }
12
-
13
- describe "#call" do
8
+ describe "without :expiration parameter" do
14
9
  it "sets working/complete status" do
15
- thread = confirmations_thread 4, "status_updates", "job_messages_#{job_id}"
16
10
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
17
11
  start_server do
18
- expect(ConfirmationJob.perform_async(:arg1 => 'val1')).to eq(job_id)
19
- expect(thread.value).to eq([job_id, job_id,
20
- "while in #perform, status = working",
21
- job_id])
12
+ thread = branched_redis_thread 4, "status_updates", "job_messages_#{job_id}" do
13
+ expect(ConfirmationJob.perform_async 'arg1' => 'val1').to eq(job_id)
14
+ end
15
+ expect(thread.value).to eq([
16
+ job_id,
17
+ job_id,
18
+ "while in #perform, status = working",
19
+ job_id
20
+ ])
22
21
  end
23
22
  expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('complete')
24
23
  expect(Sidekiq::Status::complete?(job_id)).to be_truthy
@@ -35,19 +34,64 @@ describe Sidekiq::Status::ServerMiddleware do
35
34
  expect(Sidekiq::Status::failed?(job_id)).to be_truthy
36
35
  end
37
36
 
38
- context "sets interrupted status" do
39
- it "on system exit signal" do
37
+ it "sets failed status when Exception raised" do
38
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
39
+ start_server do
40
+ expect(capture_status_updates(3) {
41
+ expect(FailingHardJob.perform_async).to eq(job_id)
42
+ }).to eq([job_id]*3)
43
+ end
44
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('failed')
45
+ expect(Sidekiq::Status::failed?(job_id)).to be_truthy
46
+ end
47
+
48
+ context "when Sidekiq::Status::Worker is not included in the job" do
49
+ it "should not set a failed status" do
40
50
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
41
51
  start_server do
42
- expect(capture_status_updates(3) {
43
- expect(ExitedJob.perform_async).to eq(job_id)
44
- }).to eq([job_id]*3)
52
+ expect(FailingNoStatusJob.perform_async).to eq(job_id)
53
+ end
54
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to be_nil
55
+ end
56
+
57
+ it "should not set any status when Exception raised" do
58
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
59
+ start_server do
60
+ expect(FailingHardNoStatusJob.perform_async).to eq(job_id)
61
+ end
62
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to be_nil
63
+ end
64
+
65
+ it "should not set any status on system exit signal" do
66
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
67
+ start_server do
68
+ expect(ExitedNoStatusJob.perform_async).to eq(job_id)
69
+ end
70
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to be_nil
71
+ end
72
+
73
+ it "should not set any status on interrupt signal" do
74
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
75
+ start_server do
76
+ expect(InterruptedNoStatusJob.perform_async).to eq(job_id)
77
+ end
78
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to be_nil
79
+ end
80
+ end
81
+
82
+ context "sets interrupted status" do
83
+ it "on system exit signal" do
84
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
85
+ start_server do
86
+ expect(capture_status_updates(3) {
87
+ expect(ExitedJob.perform_async).to eq(job_id)
88
+ }).to eq([job_id]*3)
45
89
  end
46
90
  expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('interrupted')
47
91
  expect(Sidekiq::Status::interrupted?(job_id)).to be_truthy
48
92
  end
49
93
 
50
- it "on interrupt signal" do
94
+ it "on interrupt signal" do
51
95
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
52
96
  start_server do
53
97
  expect(capture_status_updates(3) {
@@ -63,13 +107,13 @@ describe Sidekiq::Status::ServerMiddleware do
63
107
  it "sets status hash ttl" do
64
108
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
65
109
  start_server do
66
- expect(StubJob.perform_async(:arg1 => 'val1')).to eq(job_id)
110
+ expect(StubJob.perform_async 'arg1' => 'val1').to eq(job_id)
67
111
  end
68
112
  expect(1..Sidekiq::Status::DEFAULT_EXPIRY).to cover redis.ttl("sidekiq:status:#{job_id}")
69
113
  end
70
114
  end
71
115
 
72
- describe ":expiration parameter" do
116
+ describe "with :expiration parameter" do
73
117
  let(:huge_expiration) { Sidekiq::Status::DEFAULT_EXPIRY * 100 }
74
118
  before do
75
119
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
@@ -77,16 +121,16 @@ describe Sidekiq::Status::ServerMiddleware do
77
121
 
78
122
  it "overwrites default expiry value" do
79
123
  start_server(:expiration => huge_expiration) do
80
- StubJob.perform_async(:arg1 => 'val1')
124
+ StubJob.perform_async 'arg1' => 'val1'
81
125
  end
82
- expect((Sidekiq::Status::DEFAULT_EXPIRY+1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
126
+ expect((Sidekiq::Status::DEFAULT_EXPIRY-1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
83
127
  end
84
128
 
85
129
  it "can be overwritten by worker expiration method" do
86
130
  overwritten_expiration = huge_expiration * 100
87
131
  allow_any_instance_of(StubJob).to receive(:expiration).and_return(overwritten_expiration)
88
132
  start_server(:expiration => huge_expiration) do
89
- StubJob.perform_async(:arg1 => 'val1')
133
+ StubJob.perform_async 'arg1' => 'val1'
90
134
  end
91
135
  expect((huge_expiration+1)..overwritten_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
92
136
  end
@@ -8,30 +8,77 @@ describe 'sidekiq status web' do
8
8
  let!(:redis) { Sidekiq.redis { |conn| conn } }
9
9
  let!(:job_id) { SecureRandom.hex(12) }
10
10
 
11
- # Clean Redis before each test
12
- # Seems like flushall has no effect on recently published messages,
13
- # so we should wait till they expire
14
- before { redis.flushall; sleep 0.1 }
15
-
16
11
  def app
17
12
  Sidekiq::Web
18
13
  end
19
14
 
20
- it 'shows a job in progress' do
15
+ before do
16
+ env 'rack.session', csrf: Base64.urlsafe_encode64('token')
21
17
  client_middleware
22
18
  allow(SecureRandom).to receive(:hex).and_return(job_id)
19
+ end
20
+
21
+ around { |example| start_server(&example) }
22
+
23
+ it 'shows the list of jobs in progress' do
24
+ capture_status_updates(2) do
25
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
26
+ end
23
27
 
24
- start_server do
25
- capture_status_updates(2) do
26
- expect(LongJob.perform_async(1)).to eq(job_id)
27
- end
28
+ get '/statuses'
29
+ expect(last_response).to be_ok
30
+ expect(last_response.body).to match(/#{job_id}/)
31
+ expect(last_response.body).to match(/LongJob/)
32
+ expect(last_response.body).to match(/working/)
33
+ end
34
+
35
+ it 'allows filtering the list of jobs by status' do
36
+ capture_status_updates(2) do
37
+ LongJob.perform_async(0.5)
38
+ end
39
+
40
+ get '/statuses?status=working'
41
+ expect(last_response).to be_ok
42
+ expect(last_response.body).to match(/#{job_id}/)
43
+ expect(last_response.body).to match(/LongJob/)
44
+ expect(last_response.body).to match(/working/)
45
+ end
28
46
 
29
- get '/statuses'
30
- expect(last_response).to be_ok
31
- expect(last_response.body).to match(/#{job_id}/)
32
- expect(last_response.body).to match(/LongJob/)
33
- expect(last_response.body).to match(/working/)
47
+ it 'allows filtering the list of jobs by completed status' do
48
+ capture_status_updates(2) do
49
+ LongJob.perform_async(0.5)
34
50
  end
51
+ get '/statuses?status=completed'
52
+ expect(last_response).to be_ok
53
+ expect(last_response.body).to_not match(/LongJob/)
35
54
  end
36
55
 
56
+ it 'shows a single job in progress' do
57
+ capture_status_updates(2) do
58
+ LongJob.perform_async(1, 'another argument')
59
+ end
60
+
61
+ get "/statuses/#{job_id}"
62
+ expect(last_response).to be_ok
63
+ expect(last_response.body).to match(/#{job_id}/)
64
+ expect(last_response.body).to match(/1,"another argument"/)
65
+ expect(last_response.body).to match(/working/)
66
+ end
67
+
68
+ it 'shows custom data for a single job' do
69
+ capture_status_updates(3) do
70
+ CustomDataJob.perform_async
71
+ end
72
+
73
+ get "/statuses/#{job_id}"
74
+ expect(last_response).to be_ok
75
+ expect(last_response.body).to match(/mister_cat/)
76
+ expect(last_response.body).to match(/meow/)
77
+ end
78
+
79
+ it 'show an error when the requested job ID is not found' do
80
+ get '/statuses/12345'
81
+ expect(last_response).to be_not_found
82
+ expect(last_response.body).to match(/That job can't be found/)
83
+ end
37
84
  end
@@ -13,11 +13,29 @@ describe Sidekiq::Status::Worker do
13
13
 
14
14
  describe ".expiration" do
15
15
  subject { StubJob.new }
16
-
16
+
17
17
  it "allows to set/get expiration" do
18
18
  expect(subject.expiration).to be_nil
19
19
  subject.expiration = :val
20
20
  expect(subject.expiration).to eq(:val)
21
21
  end
22
22
  end
23
+
24
+ describe ".at" do
25
+ subject { StubJob.new }
26
+
27
+ it "records when the worker has started" do
28
+ expect { subject.at(0) }.to(change { subject.retrieve('working_at') })
29
+ end
30
+
31
+ context "when setting the total for the worker" do
32
+ it "records when the worker has started" do
33
+ expect { subject.total(100) }.to(change { subject.retrieve('working_at') })
34
+ end
35
+ end
36
+
37
+ it "records when the worker last worked" do
38
+ expect { subject.at(0) }.to(change { subject.retrieve('update_time') })
39
+ end
40
+ end
23
41
  end
@@ -8,11 +8,7 @@ describe Sidekiq::Status do
8
8
  let!(:unused_id) { SecureRandom.hex(12) }
9
9
  let!(:plain_sidekiq_job_id) { SecureRandom.hex(12) }
10
10
  let!(:retried_job_id) { SecureRandom.hex(12) }
11
-
12
- # Clean Redis before each test
13
- # Seems like flushall has no effect on recently published messages,
14
- # so we should wait till they expire
15
- before { redis.flushall; sleep 0.1 }
11
+ let!(:retry_and_fail_job_id) { SecureRandom.hex(12) }
16
12
 
17
13
  describe ".status, .working?, .complete?" do
18
14
  it "gets job status by id as symbol" do
@@ -20,11 +16,12 @@ describe Sidekiq::Status do
20
16
 
21
17
  start_server do
22
18
  expect(capture_status_updates(2) {
23
- expect(LongJob.perform_async(1)).to eq(job_id)
19
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
24
20
  }).to eq([job_id]*2)
25
21
  expect(Sidekiq::Status.status(job_id)).to eq(:working)
26
22
  expect(Sidekiq::Status.working?(job_id)).to be_truthy
27
23
  expect(Sidekiq::Status::queued?(job_id)).to be_falsey
24
+ expect(Sidekiq::Status::retrying?(job_id)).to be_falsey
28
25
  expect(Sidekiq::Status::failed?(job_id)).to be_falsey
29
26
  expect(Sidekiq::Status::complete?(job_id)).to be_falsey
30
27
  expect(Sidekiq::Status::stopped?(job_id)).to be_falsey
@@ -54,9 +51,9 @@ describe Sidekiq::Status do
54
51
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
55
52
 
56
53
  start_server do
57
- expect(capture_status_updates(3) {
54
+ expect(capture_status_updates(4) {
58
55
  expect(ProgressJob.perform_async).to eq(job_id)
59
- }).to eq([job_id]*3)
56
+ }).to eq([job_id]*4)
60
57
  end
61
58
  expect(Sidekiq::Status.at(job_id)).to be(100)
62
59
  expect(Sidekiq::Status.total(job_id)).to be(500)
@@ -72,7 +69,7 @@ describe Sidekiq::Status do
72
69
 
73
70
  start_server do
74
71
  expect(capture_status_updates(2) {
75
- expect(LongJob.perform_async(1)).to eq(job_id)
72
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
76
73
  }).to eq([job_id]*2)
77
74
  expect(hash = Sidekiq::Status.get_all(job_id)).to include 'status' => 'working'
78
75
  expect(hash).to include 'update_time'
@@ -82,6 +79,23 @@ describe Sidekiq::Status do
82
79
  end
83
80
  end
84
81
 
82
+ describe '.delete' do
83
+ it 'deletes the status hash for given job id' do
84
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
85
+ start_server do
86
+ expect(capture_status_updates(2) {
87
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
88
+ }).to eq([job_id]*2)
89
+ end
90
+ expect(Sidekiq::Status.delete(job_id)).to eq(1)
91
+ end
92
+
93
+ it 'should not raise error while deleting status hash if invalid job id' do
94
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id)
95
+ expect(Sidekiq::Status.delete(job_id)).to eq(0)
96
+ end
97
+ end
98
+
85
99
  describe ".cancel" do
86
100
  it "cancels a job by id" do
87
101
  allow(SecureRandom).to receive(:hex).twice.and_return(job_id, job_id_1)
@@ -91,7 +105,7 @@ describe Sidekiq::Status do
91
105
  second_job = LongJob.perform_in(3600)
92
106
  expect(second_job).to eq(job_id_1)
93
107
 
94
- initial_schedule = redis.zrange "schedule", 0, -1, {withscores: true}
108
+ initial_schedule = redis.zrange "schedule", 0, -1, withscores: true
95
109
  expect(initial_schedule.size).to be(2)
96
110
  expect(initial_schedule.select {|scheduled_job| JSON.parse(scheduled_job[0])["jid"] == job_id }.size).to be(1)
97
111
 
@@ -99,7 +113,7 @@ describe Sidekiq::Status do
99
113
  # Unused, therefore unfound => false
100
114
  expect(Sidekiq::Status.cancel(unused_id)).to be_falsey
101
115
 
102
- remaining_schedule = redis.zrange "schedule", 0, -1, {withscores: true}
116
+ remaining_schedule = redis.zrange "schedule", 0, -1, withscores: true
103
117
  expect(remaining_schedule.size).to be(initial_schedule.size - 1)
104
118
  expect(remaining_schedule.select {|scheduled_job| JSON.parse(scheduled_job[0])["jid"] == job_id }.size).to be(0)
105
119
  end
@@ -112,14 +126,14 @@ describe Sidekiq::Status do
112
126
  returned_job_id = LongJob.perform_at(scheduled_time)
113
127
  expect(returned_job_id).to eq(job_id)
114
128
 
115
- initial_schedule = redis.zrange "schedule", 0, -1, {withscores: true}
129
+ initial_schedule = redis.zrange "schedule", 0, -1, withscores: true
116
130
  expect(initial_schedule.size).to be(1)
117
131
  # wrong time, therefore unfound => false
118
132
  expect(Sidekiq::Status.cancel(returned_job_id, (scheduled_time + 1))).to be_falsey
119
- expect((redis.zrange "schedule", 0, -1, {withscores: true}).size).to be(1)
133
+ expect((redis.zrange "schedule", 0, -1, withscores: true).size).to be(1)
120
134
  # same id, same time, deletes
121
135
  expect(Sidekiq::Status.cancel(returned_job_id, (scheduled_time))).to be_truthy
122
- expect(redis.zrange "schedule", 0, -1, {withscores: true}).to be_empty
136
+ expect(redis.zrange "schedule", 0, -1, withscores: true).to be_empty
123
137
  end
124
138
  end
125
139
  end
@@ -134,14 +148,63 @@ describe Sidekiq::Status do
134
148
  expect_2_jobs_ttl_covers 1..Sidekiq::Status::DEFAULT_EXPIRY
135
149
  end
136
150
 
151
+ it "does jobs without a known class" do
152
+ seed_secure_random_with_job_ids
153
+ start_server(:expiration => expiration_param) do
154
+ expect {
155
+ Sidekiq::Client.new(Sidekiq.redis_pool).
156
+ push("class" => "NotAKnownClass", "args" => [])
157
+ }.to_not raise_error
158
+ end
159
+ end
160
+
137
161
  it "retries failed jobs" do
138
- allow(SecureRandom).to receive(:hex).once.and_return(retried_job_id)
162
+ allow(SecureRandom).to receive(:hex).exactly(3).times.and_return(retried_job_id)
139
163
  start_server do
140
- expect(capture_status_updates(5) {
164
+ expect(capture_status_updates(3) {
141
165
  expect(RetriedJob.perform_async()).to eq(retried_job_id)
142
- }).to eq([retried_job_id] * 5)
166
+ }).to eq([retried_job_id] * 3)
167
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:retrying)
168
+ expect(Sidekiq::Status.working?(retried_job_id)).to be_falsey
169
+ expect(Sidekiq::Status::queued?(retried_job_id)).to be_falsey
170
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_truthy
171
+ expect(Sidekiq::Status::failed?(retried_job_id)).to be_falsey
172
+ expect(Sidekiq::Status::complete?(retried_job_id)).to be_falsey
173
+ expect(Sidekiq::Status::stopped?(retried_job_id)).to be_falsey
174
+ expect(Sidekiq::Status::interrupted?(retried_job_id)).to be_falsey
175
+ end
176
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:retrying)
177
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_truthy
178
+
179
+ # restarting and waiting for the job to complete
180
+ start_server do
181
+ expect(capture_status_updates(3) {}).to eq([retried_job_id] * 3)
182
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:complete)
183
+ expect(Sidekiq::Status.complete?(retried_job_id)).to be_truthy
184
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_falsey
185
+ end
186
+ end
187
+
188
+ it "marks retried jobs as failed once they do eventually fail" do
189
+ allow(SecureRandom).to receive(:hex).and_return(retry_and_fail_job_id)
190
+ start_server do
191
+ expect(
192
+ capture_status_updates(3) {
193
+ expect(RetryAndFailJob.perform_async).to eq(retry_and_fail_job_id)
194
+ }
195
+ ).to eq([retry_and_fail_job_id] * 3)
196
+
197
+ expect(Sidekiq::Status.status(retry_and_fail_job_id)).to eq(:retrying)
198
+ end
199
+
200
+ # restarting and waiting for the job to fail
201
+ start_server do
202
+ expect(capture_status_updates(3) {}).to eq([retry_and_fail_job_id] * 3)
203
+
204
+ expect(Sidekiq::Status.status(retry_and_fail_job_id)).to eq(:failed)
205
+ expect(Sidekiq::Status.failed?(retry_and_fail_job_id)).to be_truthy
206
+ expect(Sidekiq::Status::retrying?(retry_and_fail_job_id)).to be_falsey
143
207
  end
144
- expect(Sidekiq::Status.status(retried_job_id)).to eq(:complete)
145
208
  end
146
209
 
147
210
  context ":expiration param" do
@@ -154,7 +217,7 @@ describe Sidekiq::Status do
154
217
  expect_2_jobs_ttl_covers (Sidekiq::Status::DEFAULT_EXPIRY+1)..expiration_param
155
218
  end
156
219
 
157
- it "allow to overwrite :expiration parameter by .expiration method from worker" do
220
+ it "allow to overwrite :expiration parameter by #expiration method from worker" do
158
221
  overwritten_expiration = expiration_param * 100
159
222
  allow_any_instance_of(NoStatusConfirmationJob).to receive(:expiration).
160
223
  and_return(overwritten_expiration)
@@ -164,6 +227,16 @@ describe Sidekiq::Status do
164
227
  expect_2_jobs_are_done_and_status_eq :complete
165
228
  expect_2_jobs_ttl_covers (expiration_param+1)..overwritten_expiration
166
229
  end
230
+
231
+ it "reads #expiration from a method when defined" do
232
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id, job_id_1)
233
+ start_server do
234
+ expect(StubJob.perform_async).to eq(job_id)
235
+ expect(ExpiryJob.perform_async).to eq(job_id_1)
236
+ expect(redis.ttl("sidekiq:status:#{job_id}")).to eq(30 * 60)
237
+ expect(redis.ttl("sidekiq:status:#{job_id_1}")).to eq(15)
238
+ end
239
+ end
167
240
  end
168
241
 
169
242
  def seed_secure_random_with_job_ids
@@ -173,12 +246,12 @@ describe Sidekiq::Status do
173
246
 
174
247
  def run_2_jobs!
175
248
  start_server(:expiration => expiration_param) do
176
- expect(capture_status_updates(12) {
249
+ expect(capture_status_updates(6) {
177
250
  expect(StubJob.perform_async).to eq(plain_sidekiq_job_id)
178
251
  NoStatusConfirmationJob.perform_async(1)
179
252
  expect(StubJob.perform_async).to eq(job_id_1)
180
253
  NoStatusConfirmationJob.perform_async(2)
181
- }).to match_array([plain_sidekiq_job_id, job_id_1] * 6)
254
+ }).to match_array([plain_sidekiq_job_id, job_id_1] * 3)
182
255
  end
183
256
  end
184
257
 
data/spec/spec_helper.rb CHANGED
@@ -1,71 +1,149 @@
1
1
  require "rspec"
2
-
3
- require 'celluloid'
2
+ require 'colorize'
4
3
  require 'sidekiq'
4
+
5
+ # Celluloid should only be manually required before Sidekiq versions 4.+
6
+ require 'sidekiq/version'
7
+ require 'celluloid' if Gem::Version.new(Sidekiq::VERSION) < Gem::Version.new('4.0')
8
+
5
9
  require 'sidekiq/processor'
6
10
  require 'sidekiq/manager'
7
11
  require 'sidekiq-status'
8
12
 
13
+ # Clears jobs before every test
14
+ RSpec.configure do |config|
15
+ config.before(:each) do
16
+ Sidekiq.redis { |conn| conn.flushall }
17
+ client_middleware
18
+ sleep 0.05
19
+ end
20
+ end
9
21
 
10
22
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
23
 
12
- def client_middleware(client_middleware_options={})
24
+ # Configures client middleware
25
+ def client_middleware client_middleware_options = {}
13
26
  Sidekiq.configure_client do |config|
14
- config.client_middleware do |chain|
15
- chain.add Sidekiq::Status::ClientMiddleware, client_middleware_options
16
- end
27
+ Sidekiq::Status.configure_client_middleware config, client_middleware_options
17
28
  end
18
29
  end
19
30
 
20
- def confirmations_thread(messages_limit, *channels)
31
+ def redis_thread messages_limit, *channels
32
+
21
33
  parent = Thread.current
22
34
  thread = Thread.new {
23
- confirmations = []
35
+ messages = []
24
36
  Sidekiq.redis do |conn|
25
- conn.subscribe *channels do |on|
37
+ puts "Subscribing to #{channels} for #{messages_limit.to_s.bold} messages".cyan if ENV['DEBUG']
38
+ conn.subscribe_with_timeout 60, *channels do |on|
26
39
  on.subscribe do |ch, subscriptions|
40
+ puts "Subscribed to #{ch}".cyan if ENV['DEBUG']
27
41
  if subscriptions == channels.size
28
42
  sleep 0.1 while parent.status != "sleep"
29
43
  parent.run
30
44
  end
31
45
  end
32
46
  on.message do |ch, msg|
33
- confirmations << msg
34
- conn.unsubscribe if confirmations.length >= messages_limit
47
+ puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
48
+ messages << msg
49
+ conn.unsubscribe if messages.length >= messages_limit
35
50
  end
36
51
  end
37
52
  end
38
- confirmations
53
+ puts "Returing from thread".cyan if ENV['DEBUG']
54
+ messages
39
55
  }
56
+
40
57
  Thread.stop
41
58
  yield if block_given?
42
59
  thread
60
+
61
+ end
62
+
63
+ def redis_client_thread message_limit, *channels
64
+ thread = Thread.new {
65
+ messages = []
66
+ Sidekiq.redis do |conn|
67
+ puts "Subscribing to #{channels} for #{message_limit.to_s.bold} messages".cyan if ENV['DEBUG']
68
+ pubsub = conn.pubsub
69
+ pubsub.call("SUBSCRIBE", *channels)
70
+
71
+ timeouts = 0
72
+ loop do
73
+ type, ch, msg = pubsub.next_event(2)
74
+ next if type == "subscribe"
75
+ if msg
76
+ puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
77
+ messages << msg
78
+ break if messages.length >= message_limit
79
+ else
80
+ # no new message was received in the allocated timeout
81
+ timeouts += 1
82
+ break if timeouts >= 30
83
+ end
84
+ end
85
+ end
86
+ puts "Returing from thread".cyan if ENV['DEBUG']
87
+ messages
88
+ }
89
+ sleep 0.1
90
+ yield if block_given?
91
+ thread.join
92
+ end
93
+
94
+ def branched_redis_thread n, *channels, &block
95
+ if Sidekiq.major_version < 7
96
+ redis_thread(n, *channels, &block)
97
+ else
98
+ redis_client_thread(n, *channels, &block)
99
+ end
43
100
  end
44
101
 
45
- def capture_status_updates(n, &block)
46
- confirmations_thread(n, "status_updates", &block).value
102
+ def capture_status_updates n, &block
103
+ branched_redis_thread(n, "status_updates", &block).value
47
104
  end
48
105
 
49
- def start_server(server_middleware_options={})
106
+ # Configures server middleware and launches a sidekiq server
107
+ def start_server server_middleware_options = {}
108
+
109
+ # Creates a process for the Sidekiq server
50
110
  pid = Process.fork do
51
- $stdout.reopen File::NULL, 'w'
52
- $stderr.reopen File::NULL, 'w'
111
+
112
+ # Redirect the server's outputs
113
+ $stdout.reopen File::NULL, 'w' unless ENV['DEBUG']
114
+ $stderr.reopen File::NULL, 'w' unless ENV['DEBUG']
115
+
116
+ # Load and configure server options
53
117
  require 'sidekiq/cli'
54
- Sidekiq.options[:queues] << 'default'
55
- Sidekiq.options[:require] = File.expand_path('../support/test_jobs.rb', __FILE__)
118
+
119
+ # Add the server middleware
56
120
  Sidekiq.configure_server do |config|
57
- config.redis = Sidekiq::RedisConnection.create
58
- config.server_middleware do |chain|
59
- chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
60
- end
121
+ config.concurrency = 5
122
+ config.redis = Sidekiq::RedisConnection.create if Sidekiq.major_version < 7
123
+ Sidekiq::Status.configure_server_middleware config, server_middleware_options
61
124
  end
62
- Sidekiq::CLI.instance.run
125
+
126
+ # Launch
127
+ puts "Server starting".yellow if ENV['DEBUG']
128
+ instance = Sidekiq::CLI.instance
129
+ instance.parse(['-r', File.expand_path('environment.rb', File.dirname(__FILE__))])
130
+ instance.run
131
+
63
132
  end
64
133
 
134
+ # Run the client-side code
65
135
  yield
66
- sleep 0.1
136
+
137
+ # Pause to ensure all jobs are picked up & started before TERM is sent
138
+ sleep 0.2
139
+
140
+ # Attempt to shut down the server normally
67
141
  Process.kill 'TERM', pid
68
- Timeout::timeout(10) { Process.wait pid } rescue Timeout::Error
142
+ Process.wait pid
143
+
69
144
  ensure
145
+
146
+ # Ensure the server is actually dead
70
147
  Process.kill 'KILL', pid rescue "OK" # it's OK if the process is gone already
148
+
71
149
  end