-
-
Notifications
You must be signed in to change notification settings - Fork 763
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
New threadsafe let
implementation can cause NoMethodError
when closures retain access to the example
#1921
Comments
We should definitely fix this before shipping the next release :) can we somehow restore the original behaviour or was that merely masking this bug? |
I'm planning to fix this before next release -- that's why it's labeled "release blocker" :). |
Looks like it does both, but my understanding is questionable. |
Also, my suite passes when I delete that code: # I deleted the code that resets ivars
$ git diff
diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb
index c38a260..e7685ae 100644
--- a/lib/rspec/core/example.rb
+++ b/lib/rspec/core/example.rb
@@ -225,9 +225,6 @@ module RSpec
rescue Exception => e
set_exception(e)
ensure
- ExampleGroup.each_instance_variable_for_example(@example_group_instance) do |ivar|
- @example_group_instance.instance_variable_set(ivar, nil)
- end
@example_group_instance = nil
end
# The entire suite passes
$ ruby --disable-gem -S bundle/bin/rspec
# ...
Finished in 7.65 seconds (files took 0.73697 seconds to load)
1696 examples, 0 failures, 1 pending
# I'm on master as of this commit 3 days ago https://github.com/rspec/rspec-core/commit/35106b316e589d12b1661e3478d2b8210ad06693
$ git log --pretty=format:'%H' -1
35106b316e589d12b1661e3478d2b8210ad06693
# Add a spec that illustrates the issue
$ cat <<SPEC > spec/zomg_spec.rb
blocks = []
RSpec.describe 'whatevz' do
let(:a) { 123 }
before { blocks << lambda { a } }
2.times do
example { blocks.each(&:call) if blocks.size == 2 }
end
end
SPEC
# The spec passes
$ ruby --disable-gems -S bundle/bin/rspec spec/zomg_spec.rb
# ...
2 examples, 0 failures
# Back to the current code
$ git stash
Saved working directory and index state WIP on master: 35106b3 Merge pull request #1940 from rspec/tell-users-where-invalid-option-came-from
HEAD is now at 35106b3 Merge pull request #1940 from rspec/tell-users-where-invalid-option-came-from
# The spec fails
$ ruby --disable-gems -S bundle/bin/rspec spec/zomg_spec.rb
# ...
2 examples, 1 failure Also, I think this spec intends to verify it. But I don't understand the context well enough to really understand why it passes with this code deleted. Might, further, be worth noting that when I also delete the $ git diff lib
diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb
index c38a260..f61d2a8 100644
--- a/lib/rspec/core/example.rb
+++ b/lib/rspec/core/example.rb
@@ -224,11 +224,6 @@ module RSpec
end
rescue Exception => e
set_exception(e)
- ensure
- ExampleGroup.each_instance_variable_for_example(@example_group_instance) do |ivar|
- @example_group_instance.instance_variable_set(ivar, nil)
- end
- @example_group_instance = nil
end
finish(reporter)
$ ruby --disable-gem -S bundle/bin/rspec
1) RSpec::Core::Example#run sets its reference to the example group instance to nil
Failure/Error: expect(group.examples.first.instance_variable_get("@example_group_instance")).to be_nil
expected: nil
got: #<RSpec::ExampleGroups::Anonymous_120 "example" (./spec/rspec/core/example_spec.rb:256)>
# ./spec/rspec/core/example_spec.rb:259:in `block (3 levels) in <top (required)>'
# ...
1698 examples, 1 failure, 1 pending |
Sorry for not responding sooner - didn't realize I had been mentioned here - but no, I don't remember :) |
Yep, it does both. My question was why both were done and not just the example group instance.
I believe the spec passes because it instantiates a new group instance and looks for the ivars on that. That spec, as originally written in b51be2c, did not do that. Looks like the extra
Yep. So here's my thoughts on what we should do:
@JoshCheek, do you want to take a stab at this since you've already started looking into it? Should be a much quicker change than your earlier PR :). |
lol, sure (my bad on missing this, was a bit crazy last week :P) |
Accomplish this by no longer clearing the example's ivars. Context: * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Uhm... lol, I kept squashing and force pushing, which I don't normally do. Hope y'all's inboxes don't get spammed >.< |
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * This pull request: rspec#1950 * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
Accomplish this by no longer clearing the example's ivars. Fixes rspec#1921 Context ------- * This pull request: rspec#1950 * Originally clearing ivars due to memory leak: rspec#321 * Threadsafe memoized helpers caused `__memoized` to stop lazily initializing: rspec#321 * This caused it to be permawiped by by the resetting of the example's ivars: rspec#1921 However, this patch tests the behaviour of the memory leak, rather than its mechanics, which shows that it was fixed at some point. So we simply remove that code.
I updated a work project to HEAD and immediately ran into a bunch of
NoMethodError
:I traced it down to a poorly written spec of mine that did the equivalent of:
Calling
define_redis_instance_for
mutatesMyJobPipeline
global state. I should be resetting it back after the examples but forgot to. Thedefine_redis_instance_for
blocks retains a reference to the example, and callsredis
on it each timeMyJobPipeline
invokes the configured block.Before #1858 was merged, this poorly written spec didn't cause problems, since
__memoized
was deifned as@__memoized ||= {}
-- so if the variable wasnil
, it would merely get re-initialized to a blank hash. As of #1858, we no longer have the||= {}
logic -- instead@__memoized
is eagerly initialized. After the example completes, all instance variables are set tonil
(see b51be2c which addressed #321), so the when thelet
method is invoked after the example has completed, it raises aNoMethodError
.Ultimately, my spec is buggy and should be fixed (and I will) but the experience of RSpec raising a
NoMethodError
in this case is poor and I think we should address that, too. If I can accidentally forget to reset the global state like this, users can, too.The question is...how do we address this w/o bringing back the memory leak of #321?
I'm thinking that maybe we can remove the clearing of the ivars entirely, and instead figure out what's retaining a reference to the example group instance and clear that instead. That should be faster, anyway as it's only 1 thing to clear instead of N things (for N ivars).
Any reason that wouldn't work? @dchelimsky, when fixing #321, did you consider the route of clearing the reference to the example group instance rather than clearing the instance's references to its ivars? (If you don't remember, as would be totally reasonable, that's fine, but figured I'd ask).
The text was updated successfully, but these errors were encountered: