-
-
Notifications
You must be signed in to change notification settings - Fork 756
Getting deadlock issues when mocking in specs (3.3.0) #1994
Comments
RSpec doesn't spin up any threads so I'd be surprised if it's causing this...although an internal change may have allowed a deadlock problem to surface. Is there any way you can provide a reproducible example of this? If you can't provide that, can you run your specs with |
Backtraces here; looks like it's an issue in rspec-mocks.
|
The below should be sufficient to reproduce the error: class EventPolicy < SimpleDelegator; end
# Public: A Policy object for interacting with Bookings.
class BookingPolicy < SimpleDelegator
# Public: Is this Booking ready for approval?
#
# Returns true or false.
def ready_for_approval?
events.any? && events.all? { |e| EventPolicy.new(e).ready_for_approval? }
end
end RSpec.describe BookingPolicy do
let(:booking) { instance_double(Booking) }
let(:policy) { BookingPolicy.new(booking) }
subject { policy }
describe "#ready_for_approval?" do
context "when there are no events" do
before { allow(booking).to receive(:events).and_return([]) }
it { is_expected.to_not be_ready_for_approval }
end
context "when there are events" do
let(:event) { instance_double(Event) }
before { allow(booking).to receive(:events).and_return([event]) }
context "and they are ready for approval" do
before do
allow_any_instance_of(EventPolicy).to receive(:ready_for_approval?).and_return(true)
end
it { is_expected.to be_ready_for_approval }
end
context "but they are not ready for approval" do
before do
allow_any_instance_of(EventPolicy).to receive(:ready_for_approval?).and_return(false)
end
it { is_expected.to_not be_ready_for_approval }
end
end
end
end |
Thanks. I've tried your snippet and I get So no, that snippet isn't sufficient to reproduce the error :(. Can you work on making it runnable in isolation? A common technique used to report bugs like this is to create a tiny little gist that I can clone and run, but pasting code in-line like you did is totally fine as well, as long as it does actually work to repro when run in isolation. |
After a ton of fidgeting, I was able to figure out how to recreate the issue. See: https://github.com/ahorner/deadlock-sample I documented my findings in the README for that repo, but in summary: it seems like something in |
Thanks for the repo @ahorner ❤️ I haven't dug too deeply, but I have a hunch this is related to rspec-rails using # Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
true
end I'll have to dig more to understand why the locks are deadlocking in this manner. |
I did confirm that commenting out the line in rspec-rails which calls |
I've ruled out rspec-rails being at fault here. This seems to be an issue with Here's a new simplified reproduction scenario: RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
class ChildPoll < SimpleDelegator
def asleep?
false
end
end
RSpec.describe "Deadlocks with SimpleDelegator and mock instances" do
RSpec::Mocks.configuration.when_declaring_verifying_double do |ref|
ref.target.respond_to?(:any_method)
end
around do |ex|
orig_callbacks = RSpec::Mocks.configuration.verifying_double_callbacks.dup
ex.call
RSpec::Mocks.configuration.verifying_double_callbacks.replace orig_callbacks
end
it "when the verify block causes delegation to a mock target a deadlock occurs" do
child = double
allow_any_instance_of(ChildPoll).to receive(:asleep?).and_return(true)
expect(ChildPoll.new(child)).to be_asleep
end
it "when the verify block causes delegation to a verifying mock target a deadlock occurs" do
child_klass = Class.new
child = instance_double(child_klass)
allow_any_instance_of(ChildPoll).to receive(:asleep?).and_return(true)
expect(ChildPoll.new(child)).to be_asleep
end
it "when the verify block does not forward to the delegate source no deadlock occurs" do
RSpec::Mocks.configuration.verifying_double_callbacks.clear
RSpec::Mocks.configuration.when_declaring_verifying_double do |possible_model|
# This does not access the simple delegator source object
possible_model.target.class
end
child = double
allow_any_instance_of(ChildPoll).to receive(:asleep?).and_return(true)
expect(ChildPoll.new(child)).to be_asleep
end
it "passing a non-mock to simple delegator does not cause a deadlock" do
child = double
allow_any_instance_of(ChildPoll).to receive(:asleep?).and_return(true)
expect(ChildPoll.new(child)).to be_asleep
end
end |
Though I have no idea what the actual root cause is at this time. |
This appears to not be specific to any instance as the following spec also causes the deadlock in the above context: it "when the verify block causes delegation to a mock target a deadlock occurs" do
child = double
poll = ChildPoll.new(child)
allow(poll).to receive(:asleep?).and_return(true)
# Note we do not need to do anything else the `allow` call trips the deadlock
end It seems consistent so far that we need:
For example, the following spec passes without deadlock: it "delegator to a mock by itself does not cause a deadlock" do
RSpec::Mocks.configuration.verifying_double_callbacks.clear
child = double
allow_any_instance_of(ChildPoll).to receive(:asleep?).and_return(true)
expect(ChildPoll.new(child)).to be_asleep
poll = ChildPoll.new(child)
allow(poll).to receive(:asleep?).and_return(true)
expect(poll).to be_asleep
end |
This is all really useful info. I plan to look into this later tonight if there's time or in the next couple days if there's not. |
I see what's going on. We protect proxy creation with a mutex. In #940 we started invoking the verifying double callback that is used by rspec-rails from the I see a few ways we can go about fixing this:
There's nothing mutually exclusive about these ideas. We may want to do multiple (or all) of them. |
rspec-rails 3.3.2 has been released with a fix. |
For future reference I've done two of @myronmarston's suggestions here, but I'm not sure how feasible the last one is, if anyone else fancies a try have at it! |
Closing. |
Thanks, @JonRowe! |
I just encountered this error, updated rspec, but no luck. Somehow (I didn't have time for more digging) rspec and Draper don't like each other. I kept getting deadlock errors from
after
(where subject is an instance of a Draper decorator) and then calling a decorator's method that calls current_job_period. By stack trace I'd say it's rather a problem with Draper, but in the |
Given we've gone to lengths to remove the deadlocks this could be a draper issue but I'm not going to rule out RSpec's involvement here, any chance you can reproduce the particular scenario draper is inducing? (Pull the bits of code out until it fails etc) |
After upgrading to 3.3.0, I started seeing a handful of consistent spec failures.
rspec-rails
version 3.3.0infer_spec_type_from_file_location!
The failing specs have the following in common: They are in the
spec/policies
directory, and use theexpect_any_instance_of
orallow_any_instance_of
methods (both seem to run into the same issue).The failures manifest as such:
The text was updated successfully, but these errors were encountered: