-
Notifications
You must be signed in to change notification settings - Fork 321
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
Request thread safety issue #558
Comments
Reduced to a simple test case: require "thread"
require "http"
@http = HTTP.headers("X-Test" => "1")
5.times.map do
Thread.new { r = @http.post("https://httpbin.org/post"); puts r.status}
end.map(&:join) This breaks in a similar way:
While modifying the thread block to use the HTTP constant directly works just fine.
I would have expected to be able to configure the HTTP client once and be able to re-use it amongst threads, the same way calling the |
Possibly related to #371 |
To try to answer this:
The original goal of the
|
@tarcieri So do you think the HTTP gem should allow the simple test case in my comment to work or not? (The one where I re-use the same I'll start using the ConnectionPool for this, I just found the behavior surprising. |
It's entirely possible. Maybe Ruby in 2015 couldn't detect this problem so just returned an empty string, and modern Ruby IO notices somehow and is able to raise the IOError. |
Re-reviewing #371 again, I think I was thinking of a different issue... |
@paul so the real question is...
Are you encountering thread-safety problems with it? (i.e. is it your |
FWIW Lines 71 to 76 in 6240672
|
In order to make thread-safe concurrent calls with same HTTP::Client instance, you will need to use mutex, something like this: @mutex = Mutex.new
# ...
res = @mutex.synchronize { http.get(...).flush } In other words you need to guarantee that there's no concurrent request between current request and |
Yes, so If I'm understanding correctly though, @paul is trying to do the first thing, which is share @paul can you confirm that's the case? |
Mmm, but the snippet shared above is absolutely not re-use of options, but re-use of client: @http = HTTP.headers("X-Test" => "1")
5.times.map do
Thread.new { r = @http.post("https://httpbin.org/post"); puts r.status}
end.map(&:join) |
@ixti oh, now I see... https://github.com/httprb/http/blob/master/lib/http/chainable.rb#L250 I guess that changed way back in #7 |
@paul this seems like an unfortunate complication of there not being a proper "builder" type for the client, allowing options which don't require the creation of e.g. sockets to be performed the way you want. It seems like retrofitting an API like that is possible, by splitting up the the |
The only thing that might not work correctly with that idea is keep-alive (persistent) clients... |
@ixti those could still return |
@tarcieri On one hand, yes. On another chances that somebody will try to cache persistent client are also high: GithubClient = HTTP.persistent("https://github.com") |
I might sound like crazy, but why can't we keep |
Ignore me - just imagined the code for that and it's complexity will be pretty bad :(( |
@ixti I think it's ok to tell users of persistent connections they need to use something like the connection_pool gem if they want thread safety, and possibly document it with an example |
@tarcieri I guess I agree! |
FWIW, we are seeing this issue without a persistent connection, just a shared client. |
|
@tarcieri I think we should refactor API a bit. All the options changing methods should return some sort of client builder rather than client instance, just the way it was. With one exception - |
@ixti absolutely. That was the original intent, however as I noted earlier in this issue it seems it's been broken since #7 |
I've added a link to this issue on https://github.com/httprb/http/wiki/Thread-Safety since the text is not valid anymore. |
I stumbled over this today as well, after using the gem for multiple years where i never ran into that. Would it maybe make sense to at least module Chainable
def get(uri, options = {})
# request :get, uri, options
branch(default_options).request(:get, uri, options)
end
# ...
end Then one could at least use http = HTTP.auth(...).timeout(25).headers(...)
10.times { Thread.new { http.get('https://...') } } without running into thread-safety issues. My workaround for now is to do: http = HTTP.auth(...).timeout(25).headers(...)
10.times { Thread.new { http.headers({}).get('https://...') } } But that feels a bit weird and unneccessary. |
We're getting weird connection issues in our exception tracker:
The logs for that single job don't show anything interesting, but if you look at the logs for all the workers in that process:
Job
03e5
started posting to the endpoint, and 300ms later another thread (job1bf0
) posted to the same endpoint with a different body. This somehow interrupted the first request in progress, raising the IOError in the first 1.5ms after the 2nd request started. The 2nd request then finished normally.Here's how we're making that request:
We're using the shoryuken gem as our job runner, which uses concurrent-ruby for the worker thread pool. I'm not clear how making a request in one worker thread could interfere with another. The wiki says "Thread safety comes into play when you Make an HTTP request", but doesn't offer any details, or instruction how to mitigate it.
This happens more often than I would have expected, too:
The text was updated successfully, but these errors were encountered: