1
votes

In a Ruby script I'm having a problem with socket connections. What I am doing is the following:

  • I have two threads and each one creates a connection to a different web server
  • Any time thread 1 receives data from server 1, I want thread 1 to post this data to server 2
  • Any time thread 2 receives data from server 2, I want thread 2 to post this data to server 1

Basically I am kind of acting as a bridge between the 2 servers.

Code looks like this:

require 'uri'
require 'net/http'
require 'json'

@connection1 = Net::HTTP.start 'server1.com'
@connection2 = Net::HTTP.start 'server2.com'

# reads data from server 1 as it comes and sends it to server 2     
Thread.new{
  while JSON.parse(@connection1.post('/receive').body) !nil
      @connection2.post '/send', JSON.parse(@connection1.post('/receive').body) 
  end
}


# reads data from server 2 as it comes and sends it to server 2  
while JSON.parse(@connection2.post('/receive').body) !nil
    @connection1.post '/send', JSON.parse(@connection2.post('/receive').body) 
end

# Thread.join 
# not actually needed because the two connections are supposed to continuously stream data

However as soon as one of the two connections receives data and tries sending it to the other connection I'm receiving the following error:

Socket operation on non-socket - Errno::ENOTSOCK

More in deep stack trace:

C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/protocol.rb:176:in wait_readable': socket operation on non-socket. (Errno::ENOTSOCK) from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/protocol.rb:176:in 'rbuf_fill' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/protocol.rb:154:in 'readuntil' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/protocol.rb:164:in 'readline' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http/response.rb:40:in 'read_status_line' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http/response.rb:29:in 'read_new' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1446:in block in 'transport_request' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1443:in 'catch' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1443:in 'transport_request' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1416:in 'request' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1430:in 'send_entity' from C:/Dev/Ruby24-x64/lib/ruby/2.4.0/net/http.rb:1218:in 'post'

So what do you think I am doing wrong?

I should add that for reasons beyond my control the two remote servers are configured to serve data when contacted with a POST rather than with a GET.

1

1 Answers

3
votes

Core problem

You lack any sort of synchronization between both threads and Net::HTTP is not thread-safe. What's possibly happening here is that you call @connection1.post /receive in one thread, that said thread gets paused and the second thread tries to use @connection1.post /send while connection1 is still being used.

Another problem is that your code in inefficient, you issue two /receive requests per thread to get information.

while JSON.parse(@connection1.post('/receive').body) !nil
      @connection2.post '/send', JSON.parse(@connection1.post('/receive').body) 
  end

This makes three requests total

Could be

while True
  result = JSON.parse(@connection1.post('/receive').body)
  break if result.nil?
  @connection2.post '/send', result) 
end

This makes two requests total


Suggested Solution

Use a Mutex to make sure that while connection1 is sending/receiving a request, no other thread touches it.

require 'uri'
require 'net/http'
require 'json'

@connection1 = Net::HTTP.start 'server1.com'
@connection2 = Net::HTTP.start 'server2.com'

connection_1_lock = Mutex.new
connection_2_lock = Mutex.new

# reads data from server 1 as it comes and sends it to server 2
Thread.new do
  while True
    receive_result = nil
    connection_1_lock.synchronize do
      receive_result = JSON.parse(@connection1.post('/receive').body)
    end
    connection_2_lock.synchronize do
      @connection2.post '/send', receive_result
    end
  end
end

Thread.new do
  while True
    receive_result = nil
    connection_2_lock.synchronize do
      receive_result = JSON.parse(@connection2.post('/receive').body)
    end
    connection_1_lock.synchronize do
      @connection1.post '/send', receive_result
    end
  end
end

I believe the code above should fix your problem, although I cannot guarantee it. Concurrent programming is hard.


Further reading:

I suggest you read up on concurrent/multithreaded programming and its pitfalls. There are numerous Ruby resources online.

Since Ruby's documentation on Mutex is notoriously bad, I'll shamelessly plug my own article here and suggest you read it:

https://dev.to/enether/working-with-multithreaded-ruby-part-i-cj3 (The 'How To Protect Yourself' paragraph introduces mutexes)