-
Notifications
You must be signed in to change notification settings - Fork 166
Avoid duplicating ActiveJob's retry machinery #11061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| require 'rails_helper' | ||
|
|
||
| RSpec.describe RiscDeliveryJob do | ||
| include ActiveJob::TestHelper | ||
| around do |ex| | ||
| REDIS_THROTTLE_POOL.with { |client| client.flushdb } | ||
| ex.run | ||
|
|
@@ -36,8 +37,7 @@ | |
|
|
||
| before do | ||
| allow(job).to receive(:analytics).and_return(job_analytics) | ||
| allow(job).to receive(:queue_adapter). | ||
| and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) | ||
| ActiveJob::Base.queue_adapter = :test | ||
| end | ||
|
|
||
| it 'POSTs the jwt to the given URL' do | ||
|
|
@@ -62,23 +62,78 @@ | |
| ) | ||
| end | ||
|
|
||
| context 'SSL network errors' do | ||
| context 'when the job fails due to a Faraday::SSLError' do | ||
| before do | ||
| stub_request(:post, push_notification_url).to_raise(Faraday::SSLError) | ||
| allow_any_instance_of(described_class).to receive(:analytics).and_return(job_analytics) | ||
| end | ||
|
|
||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Faraday::SSLError) | ||
| end | ||
| context 'when the job fails for the 1st time' do | ||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Faraday::SSLError) | ||
|
|
||
| context 'it has already failed twice' do | ||
| before do | ||
| allow(job).to receive(:executions).and_return 2 | ||
| expect(job_analytics).not_to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'Exception from WebMock', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the job fails past the configured retry attempts' do | ||
| it 'logs an event' do | ||
| expect { perform }.to_not raise_error | ||
| perform_enqueued_jobs do | ||
| RiscDeliveryJob.perform_later( | ||
| push_notification_url: push_notification_url, | ||
| jwt: jwt, | ||
| event_type: event_type, | ||
| issuer: issuer, | ||
| ) | ||
| end | ||
|
|
||
| expect(a_request(:post, push_notification_url)).to have_been_made.times(2) | ||
| expect(job_analytics).to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'Exception from WebMock', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| context 'when the job fails due to a Faraday::ConnectionFailed' do | ||
| before do | ||
| stub_request(:post, push_notification_url).to_raise(Faraday::ConnectionFailed) | ||
| allow_any_instance_of(described_class).to receive(:analytics).and_return(job_analytics) | ||
| end | ||
|
|
||
| context 'when the job fails for the 1st time' do | ||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Faraday::ConnectionFailed) | ||
|
|
||
| expect(job_analytics).not_to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'Exception from WebMock', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the job fails past the configured retry attempts' do | ||
| it 'logs an event' do | ||
| perform_enqueued_jobs do | ||
| RiscDeliveryJob.perform_later( | ||
| push_notification_url: push_notification_url, | ||
| jwt: jwt, | ||
| event_type: event_type, | ||
| issuer: issuer, | ||
| ) | ||
| end | ||
|
|
||
| expect(a_request(:post, push_notification_url)).to have_been_made.times(2) | ||
| expect(job_analytics).to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
|
|
@@ -89,25 +144,41 @@ | |
| end | ||
| end | ||
|
|
||
| context 'Errno::ECONNREFUSED error' do | ||
| context 'when the job fails due to an Errno::ECONNREFUSED error' do | ||
| before do | ||
| allow_any_instance_of(described_class).to receive(:analytics).and_return(job_analytics) | ||
| # stub_request().to_raise wraps this in Faraday::ConnectionFailed, but | ||
| # in actual usage, the original error is unwrapped | ||
| expect(job.faraday).to receive(:post).and_raise(Errno::ECONNREFUSED) | ||
| @connection = instance_double(Faraday::Connection) | ||
| allow(@connection).to receive(:post).and_raise(Errno::ECONNREFUSED) | ||
| allow(Faraday).to receive(:new).and_return(@connection) | ||
|
Comment on lines
153
to
154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a way to use webmock to return the right error instead of manually stubbing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIANM, the comments on lines 114-115 were from you and seemed to indicate that it would not simulate the real world behavior. Should I investigate further?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOL I had completely forgotten, you can investigate more if you want, but it's probably fine as is |
||
| end | ||
|
|
||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Errno::ECONNREFUSED) | ||
| end | ||
| context 'when the job fails for the 1st time' do | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't personally really like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My preference has been to use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since they are designed for reuse, it is confusing to me that you would add them and then not use them for all the similar specs -- it makes me think these are different in some way. am i missing something about them that makes them different? i would prefer to either write out the longer specs explicitly (my actual preference, since i find shared examples hard to read, especially when there are failures), or use them for all examples because of that confusion it creates. does that make sense?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you mean. The mocking fixture was different, the call count expectation was different and the risc event payload error message was different. I'll get rid of the shared_examples as they seem to add to the confusion in this case. |
||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Errno::ECONNREFUSED) | ||
|
|
||
| context 'it has already failed twice' do | ||
| before do | ||
| allow(job).to receive(:executions).and_return 2 | ||
| expect(job_analytics).not_to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'Connection refused', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the job fails past the configured retry attempts' do | ||
| it 'logs an event' do | ||
| expect { perform }.to_not raise_error | ||
|
|
||
| perform_enqueued_jobs do | ||
| RiscDeliveryJob.perform_later( | ||
| push_notification_url: push_notification_url, | ||
| jwt: jwt, | ||
| event_type: event_type, | ||
| issuer: issuer, | ||
| ) | ||
| end | ||
|
|
||
| expect(@connection).to have_received(:post).exactly(2) | ||
| expect(job_analytics).to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
|
|
@@ -153,27 +224,43 @@ | |
| end | ||
| end | ||
|
|
||
| context 'slow network errors' do | ||
| context 'when the job encounters rate limiting' do | ||
| before do | ||
| stub_request(:post, push_notification_url).to_timeout | ||
| allow_any_instance_of(described_class).to receive(:analytics).and_return(job_analytics) | ||
| @redis_rate_limiter = instance_double(RedisRateLimiter) | ||
| allow(@redis_rate_limiter).to receive(:attempt!).and_raise(RedisRateLimiter::LimitError) | ||
| allow(RedisRateLimiter).to receive(:new).and_return(@redis_rate_limiter) | ||
| end | ||
|
|
||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(Faraday::ConnectionFailed) | ||
| end | ||
| context 'when the job fails for the 1st time' do | ||
| it 'raises and retries via ActiveJob' do | ||
| expect { perform }.to raise_error(RedisRateLimiter::LimitError) | ||
|
|
||
| context 'it has already failed twice' do | ||
| before do | ||
| allow(job).to receive(:executions).and_return 2 | ||
| expect(job_analytics).not_to have_logged_event( | ||
|
vrajmohan marked this conversation as resolved.
Outdated
|
||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'RedisRateLimiter::LimitError', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the job fails past the configured retry attempts' do | ||
| it 'logs an event' do | ||
| expect { perform }.to_not raise_error | ||
|
|
||
| perform_enqueued_jobs do | ||
| RiscDeliveryJob.perform_later( | ||
| push_notification_url: push_notification_url, | ||
| jwt: jwt, | ||
| event_type: event_type, | ||
| issuer: issuer, | ||
| ) | ||
| end | ||
|
|
||
| expect(@redis_rate_limiter).to have_received(:attempt!).exactly(10) | ||
| expect(job_analytics).to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'execution expired', | ||
| error: 'RedisRateLimiter::LimitError', | ||
| ), | ||
| ) | ||
| end | ||
|
|
@@ -187,27 +274,6 @@ | |
| end | ||
| end | ||
|
|
||
| it 'raises on rate limit errors (and retries via ActiveJob)' do | ||
| expect { perform }.to raise_error(RedisRateLimiter::LimitError) | ||
| end | ||
|
|
||
| context 'it has already failed ten times' do | ||
| before do | ||
| allow(job).to receive(:executions).and_return 10 | ||
| end | ||
|
|
||
| it 'logs an event' do | ||
| expect { perform }.to_not raise_error | ||
|
|
||
| expect(job_analytics).to have_logged_event( | ||
| :risc_security_event_pushed, | ||
| risc_event_payload.merge( | ||
| error: 'rate limit for push-notification-https://push.example.gov has maxed out', | ||
| ), | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the rate limit is overridden' do | ||
| before do | ||
| allow(IdentityConfig.store).to receive(:risc_notifications_rate_limit_overrides). | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.