-
Notifications
You must be signed in to change notification settings - Fork 48
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
Remove thread-unsafe runtime requires #43
base: master
Are you sure you want to change the base?
Conversation
Dynamically requiring implementations at runtime in this way is not safe in a multithreaded program, even in MRI with the GIL. We can simplify this by just using autoload and turning the @@class_map into a simple case statement. Fixes sparklemotion#27
Repro: require 'http/cookie' # We only care about the first exception, not all of them Thread.report_on_exception = false 100.times.map do Thread.new do HTTP::CookieJar::HashStore.new end end.each(&:join) The above will reliably trigger an exception on all versions of TruffleRuby and many versions of JRuby. For example: $ ruby -v truffleruby 22.3.1, like ruby 3.0.3, GraalVM CE Native [x86_64-linux] $ ruby repro.rb /home/cbrasic/repos/http-cookie/lib/http/cookie_jar/abstract_store.rb:50:in `initialize': undefined method `each_pair' for nil:NilClass (NoMethodError) from repro.rb:10:in `block (2 levels) in <main>' $ jruby --version jruby 9.1.2.0 (2.3.0) 2016-05-26 7357c8f OpenJDK 64-Bit Server VM 25.372-b07 on 1.8.0_372-b07 +jit [linux-x86_64] $ jruby repro.rb NameError: can't resolve constant HashStore after loading http/cookie_jar/hash_store const_missing at /home/cbrasic/.asdf/installs/ruby/jruby-9.1.2.0/lib/ruby/gems/shared/gems/http-cookie-1.0.5/lib/http/cookie_jar.rb:25 block in repro.rb at repro.rb:10
gemspec excludes sqlite3 from installing under jruby, so the mozilla_store.rb file will raise LoadError under JRuby. To avoid an undefined constant under jruby define a fake one that just raises.
@@ -4,10 +4,7 @@ | |||
require 'uri' | |||
require 'domain_name' | |||
require 'http/cookie/ruby_compat' | |||
|
|||
module HTTP | |||
autoload :CookieJar, 'http/cookie_jar' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autoload
is fine - there should be no need to change this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought so too, but I tested the first commit (which left autoload in) on all truffleruby versions and older jrubies and it still exhibited thread unsafety. Here's a repro:
require 'http/cookie'
# We only care about the first exception, not all of them
Thread.report_on_exception = false
100.times.map do
Thread.new do
HTTP::CookieJar::HashStore.new
end
end.each(&:join)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be related to other changes I do not see this reproducing in 9.2/9.3/9.4,
what I did on top of the PR:
diff --git lib/http/cookie.rb lib/http/cookie.rb
index 278036a..3e8f421 100644
--- lib/http/cookie.rb
+++ lib/http/cookie.rb
@@ -4,7 +4,10 @@ require 'time'
require 'uri'
require 'domain_name'
require 'http/cookie/ruby_compat'
-require 'http/cookie_jar'
+#require 'http/cookie_jar'
+module HTTP
+ autoload :CookieJar, 'http/cookie_jar'
+end
# This class is used to represent an HTTP Cookie.
class HTTP::Cookie
again I am fine with the require here given there isn't a lot going on
but maybe the autoload
should be kept (due compatibility) at the nested level for these:
require 'http/cookie_jar/abstract_store'
require 'http/cookie_jar/abstract_saver'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took another look and found that I had a bug in the initial implementation. 2d5b65a (on another branch for now) is a commit on top of bf6391b that should work properly by making all six classes under HTTP::CookieJar
autoloaded. I thought that addressed everything but in my testing truffleruby still has a (much rarer) issue with it. I added a task to verify safety automatically in 99dd9e4 and I get inconsistent results for truffle sometimes with the autoload approach.
~/repos/http-cookie $ rake thread_safety_check
checking for thread safety bug with 1000 threads on truffleruby 22.3.1, like ruby 3.0.3, GraalVM CE Native [x86_64-linux]
no issues detected
~/repos/http-cookie $ rake thread_safety_check
checking for thread safety bug with 1000 threads on truffleruby 22.3.1, like ruby 3.0.3, GraalVM CE Native [x86_64-linux]
no issues detected
~/repos/http-cookie $ rake thread_safety_check
checking for thread safety bug with 1000 threads on truffleruby 22.3.1, like ruby 3.0.3, GraalVM CE Native [x86_64-linux]
no issues detected
~/repos/http-cookie $ rake thread_safety_check
checking for thread safety bug with 1000 threads on truffleruby 22.3.1, like ruby 3.0.3, GraalVM CE Native [x86_64-linux]
1 of 1000 threads saw exceptions: ["NameError"]
rake aborted!
NameError: uninitialized constant HTTP::CookieJar::HashStore
/home/cbrasic/repos/http-cookie/Rakefile:34:in `const_missing'
/home/cbrasic/repos/http-cookie/Rakefile:34:in `block (3 levels) in <top (required)>'
Tasks: TOP => thread_safety_check
(See full trace by running task with --trace)
OTOH 2d5b65a does fix this for all jrubies I've tried. I've been unable to reproduce the issue with MRI anywhere even though internally we have encountered this issue with it in real workloads.
@@ -342,3 +322,6 @@ def cleanup(session = false) | |||
self | |||
end | |||
end | |||
|
|||
require 'http/cookie_jar/abstract_store' | |||
require 'http/cookie_jar/abstract_saver' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these could be autoload
-ed esp. if require 'http/cookie_jar'
is eager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, although after trying that I am still trying to understand why this rake task sometimes indicates thread safety issues under recent versions of truffleruby. autoloading is atomic in ruby, but only with respect to the individual constant being autoloaded. I thought I removed any mutation of other classes during autoloading but I might have missed something. Will dig further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for tackling this. It's okay for me to remove the problematic autoloading of http-cookie sub-modules, but I just don't want external libraries like psych/yaml and sqlite3 to be eager-loaded. Can you get them autoloaded?
@knu I'm happy to! |
<internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85: warning: <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85: warning: loading in progress, circular require considered harmful - /home/cbrasic/repos/http-cookie/lib/http/cookie_jar.rb from /home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/gems/3.3.0+0/gems/rake-13.0.6/lib/rake/rake_test_loader.rb:6:in `<main>' from /home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/gems/3.3.0+0/gems/rake-13.0.6/lib/rake/rake_test_loader.rb:6:in `select' from /home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/gems/3.3.0+0/gems/rake-13.0.6/lib/rake/rake_test_loader.rb:21:in `block in <main>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/test/test_http_cookie.rb:2:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/test/helper.rb:4:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/lib/http/cookie.rb:7:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/lib/http/cookie_jar.rb:325:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/lib/http/cookie_jar/abstract_store.rb:115:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from /home/cbrasic/repos/http-cookie/lib/http/cookie_jar/hash_store.rb:2:in `<top (required)>' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require' from <internal:/home/cbrasic/.asdf/installs/ruby/3.3.0-dev/lib/ruby/3.3.0+0/rubygems/core_ext/kernel_require.rb>:85:in `require'
This should be ready to review again. @knu, let me know if e8dcdb492a46c49049b30a4d399cdd3510e236e0 addresses your concern. |
def load_yaml(yaml) | ||
YAML.load(yaml) | ||
end | ||
def load_yaml(yaml) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rewrote this method to avoid negating the requested introduction of autoload :YAML
. It adds an extra level of indirection to every load, but only on extremely old and unsupported rubies.
if defined?(JRUBY_VERSION) | ||
class HTTP::CookieJar::MozillaStore | ||
def initialize(*) | ||
raise ArgumentError, "MozillaStore is not supported on JRuby" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this isn't that of a nice "idiomatic" Ruby approach.
if a constant is not supported it should not be defined (and I would rather prefer HTTP::CookieJar::MozillaStore
to raise a missing constant, instead of an ArgumentError
on initialize
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Change this to just unless defined?(JRUBY_VERSION)
for now and expect someone to come up with an implementation for JRuby.
Is there any chance this can get merged soon? |
Dynamically requiring implementations at runtime in this way is not safe in a multithreaded program, even in MRI with the GIL. We can simplify this
while retaining identical performance by just using autoload for the AbstractStore combinedwith plain requires, turning the@@class_map
into a simple case statement.(edit: I tested this more thoroughly under several JRuby and TruffleRuby versions and my original fix did not fully solve the issue, the autoloads were still racy. So I pushed a subsequent commit to just require all implementations up front)
I'm not sure dynamic selection of the implementation is even really necessary but I left it in to avoid changing too much in one PR.
Fixes #27
Fixes #6