Dmytro Shteflyuk created THRIFT-6125:
----------------------------------------
Summary: Ruby client leaves transports reusable after uncertain
request sends
Key: THRIFT-6125
URL: https://issues.apache.org/jira/browse/THRIFT-6125
Project: Thrift
Issue Type: Bug
Components: Ruby - Library
Reporter: Dmytro Shteflyuk
Assignee: Dmytro Shteflyuk
h3. Problem
The Ruby client closes its output transport when argument serialization fails,
but not when writing the message envelope, finishing the message, or flushing
the transport fails.
Once message writing has begun, an exception does not reliably indicate whether
zero, some, or all request bytes reached the underlying connection. Leaving
that connection open allows a later call to reuse transport state whose wire
outcome is unknown. A buffered transport may still contain request data, while
an unbuffered transport may already have emitted part or all of the request.
Argument construction is different: when creating or assigning the generated
arguments fails before message writing starts, no bytes have been published and
the connection remains safe to reuse.
h3. Client impact
After a write or flush error, an application may catch the exception and make
another call with the same generated client. Because the Ruby client currently
leaves the transport open, that later request can follow incomplete or retained
data from the failed request. This can desynchronize the client and server or
make it unclear which request a subsequent response belongs to.
The behavior affects both ordinary calls and oneway calls. For ordinary calls,
a failed send must also not add a sequence ID to the pending-reply queue.
h3. Reproduction
The following example uses a transport that accepts request bytes but fails
during flush:
{code:ruby}
require "thrift"
class FailingFlushTransport < Thrift::BaseTransport
attr_reader :closed
def write(_data)
end
def flush
raise Thrift::TransportException.new(
Thrift::TransportException::UNKNOWN,
"flush failed"
)
end
def close
@closed = true
end
end
class EmptyArgs
def write(_protocol)
end
end
class ExampleClient
include Thrift::Client
end
transport = FailingFlushTransport.new
protocol = Thrift::BinaryProtocol.new(transport)
client = ExampleClient.new(protocol)
begin
client.send_message("example", EmptyArgs)
rescue Thrift::TransportException => error
puts "#{error.message}; closed=#{transport.closed.inspect}"
end
{code}
The current implementation prints:
{noformat}
flush failed; closed=nil
{noformat}
The transport therefore remains available for another request even though
delivery of the failed request is uncertain.
h3. Expected behavior
The client should construct and assign arguments before beginning message
output. Failures at that stage should leave the transport open and should not
consume a sequence ID.
After message output begins, an exception from the envelope, argument
serialization, message end, or flush should close the output transport and
re-raise the original exception. A failure while closing must not replace the
original send error. Ordinary calls should enter the pending-reply queue only
after a successful flush, and oneway calls should follow the same
transport-closing rule.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)