Skip to content
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

Thread safety problems #71

Closed
sb8244 opened this issue Jan 24, 2017 · 5 comments
Closed

Thread safety problems #71

sb8244 opened this issue Jan 24, 2017 · 5 comments
Assignees

Comments

@sb8244
Copy link

sb8244 commented Jan 24, 2017

Hello, I wanted to bring up a problem that I've recently found. The stock implementation of NetHTTPClient is not thread safe with the global lookup objects. I identified this in an older gem version, but confirmed it is also present in the latest master version.

The problem's root is demonstrated here, by the lack of thread safety to the same http instance:

http = Net::HTTP.new("github.com", 80)
threads = [
  Thread.new { p http.request(Net::HTTP::Get.new("/")) },
  Thread.new { p http.request(Net::HTTP::Get.new("/")) },
]
threads.map(&:join)

You will get a variety of errors (randomly) if you run this locally:

Errno::EBADF: Bad file descriptor
from /opt/rubies/2.2.4/lib/ruby/2.2.0/net/protocol.rb:155:in `select'
IOError: attempt to read body out of block
from /opt/rubies/2.2.4/lib/ruby/2.2.0/net/http/response.rb:330:in `stream_check'
IOError: stream closed
from /opt/rubies/2.2.4/lib/ruby/2.2.0/net/protocol.rb:153:in `read_nonblock'

Here is the reproduction with Timezone gem (master)

require 'timezone'
Timezone::Lookup.config(:geonames) do |c|
  c.username = 'your_name'
end
threads = [
  Thread.new { p Timezone.lookup(33.7489954, -84.3879824).name },
  Thread.new { p Timezone.lookup(33.7489954, -84.3879824).name },
]
threads.map(&:join)

This will provide a variety of errors as well:

Timezone::Error::GeoNames: undefined method `closed?' for nil:NilClass
	from /opt/rubies/2.2.4/lib/ruby/gems/2.2.0/gems/timezone-1.2.4/lib/timezone/lookup/geonames.rb:43:in `rescue in lookup'
	from /opt/rubies/2.2.4/lib/ruby/gems/2.2.0/gems/timezone-1.2.4/lib/timezone/lookup/geonames.rb:28:in `lookup'
	from /opt/rubies/2.2.4/lib/ruby/gems/2.2.0/gems/timezone-1.2.4/lib/timezone.rb:75:in `lookup'
	from (irb):63:in `block in irb_binding'

Proposed Fix

I'm locally going to use a wrapper around NetHTTPClient that provides thread safety. Here's a rough working version of it:

class SafeHttp
  attr_reader :protocol, :url

  def initialize(protocol, url)
    @protocol = protocol
    @url = url
  end

  def get(get_url)
    client.get(get_url)
  end

  def client
    Thread.current[:SafeHttpClient] ||= Timezone::NetHTTPClient.new(protocol, url)
  end
end
panthomakos added a commit that referenced this issue Jan 27, 2017
`Net::HTTP`, in Ruby's standard library, is not threadsafe. The issue is
easy to sidestep by creating a new `Net::HTTP` object for every lookup.
The overhead of creating a new object compared to the network lookup
call is trivial.
@panthomakos
Copy link
Owner

@sb8244 great find! I have a fix coming. The issue is really that the http client is cached, so the same client is used for every lookup. Using Thread.current can actually cause some issues. I was pretty consistently able to get tests to fail. I am simply going to remove the client cache so that each lookup call uses a new client - this will resolve the threadsafety issue you are seeing.

@panthomakos panthomakos self-assigned this Jan 27, 2017
panthomakos added a commit that referenced this issue Jan 27, 2017
`Net::HTTP`, in Ruby's standard library, is not threadsafe. The issue is
easy to sidestep by creating a new `Net::HTTP` object for every lookup.
The overhead of creating a new object compared to the network lookup
call is trivial.
panthomakos added a commit that referenced this issue Jan 27, 2017
`Net::HTTP`, in Ruby's standard library, is not threadsafe. The issue is
easy to sidestep by creating a new `Net::HTTP` object for every lookup.
The overhead of creating a new object compared to the network lookup
call is trivial.
panthomakos added a commit that referenced this issue Jan 27, 2017
`Net::HTTP`, in Ruby's standard library, is not thread-safe. The issue
is easy to sidestep by creating a new `Net::HTTP` object for every
lookup. The overhead of creating a new object compared to the network
lookup call is trivial.
@panthomakos
Copy link
Owner

I'll target the fix for a 1.2.5 (PATCH) release.

@sb8244
Copy link
Author

sb8244 commented Jan 29, 2017

👏 nice!

For informational sake: what type of problems can arise from using Thread.current in this situation?

@panthomakos
Copy link
Owner

@sb8244 The possible problem arises because Thread.current is global in the current thread. That means that two separate lookups (ex: Timezone::Lookup::Geonames and Timezone::Lookup::Google) could end up using the same cached HTTP client.

While this is not a problem in most use-cases for the gem, it certainly arises in tests and is a possible problem if anyone has a custom http client/lookup. An example would be a custom lookup client that falls through from one client to another in the case of failure (for example if the 3rd party service is not responding).

Essentially the issue is that a single HTTP client (w/ configured credentials and URLs) could be used by two separate lookups so you could end up sending Geonames formatted queries to Google.

@sb8244
Copy link
Author

sb8244 commented Jan 30, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants