class FastSend::SocketHandler
Handles the TCP socket within the Rack hijack. Is used instead of a Proc object for better testability and better deallocation
Constants
- CLIENT_DISCONNECT_EXCEPTIONS
Exceptions that indicate a client being too slow or dropping out due to failing reads/writes
- SELECT_TIMEOUT_ON_BLOCK
The time between select() calls when a socket is blocking on write
- SENDFILE_CHUNK_SIZE
The amount of bytes we will try to fit in a single sendfile()/copy_stream() call We need to send it chunks because otherwise we have no way to have throughput stats that we need for load-balancing. Also, the sendfile() call is limited to the size of off_t, which is platform-specific. In general, it helps to stay small on this for more control.C
- SOCKET_TIMEOUT
How many seconds we will wait before considering a client dead.
- SlowLoris
Is raised when it is not possible to send a chunk of data to the client using non-blocking sends for longer than the preset timeout
- USE_BLOCKING_SENDFILE
Whether we are forced to use blocking IO for sendfile()
Public Instance Methods
# File lib/fast_send/socket_handler.rb, line 32 def call(socket) writer_method_name = if socket.respond_to?(:sendfile) :sendfile elsif RUBY_PLATFORM == 'java' :copy_nio else :copy_stream end logger.debug { "Starting the response, writing via #{writer_method_name}" } bytes_written = 0 started_proc.call(bytes_written) return if socket.closed? # Only do this now as we need to have bytes_written set stream.each_file do | file | logger.debug { "Sending %s" % file.inspect } # Run the sending method, depending on the implementation send(writer_method_name, socket, file) do |n_bytes_sent| bytes_written += n_bytes_sent logger.debug { "Written %d bytes" % bytes_written } written_proc.call(n_bytes_sent, bytes_written) end end logger.info { "Response written in full - %d bytes" % bytes_written } done_proc.call(bytes_written) rescue *CLIENT_DISCONNECT_EXCEPTIONS => e logger.warn { "Client closed connection: #{e.class}(#{e.message})" } aborted_proc.call(e) rescue Exception => e logger.fatal { "Aborting response due to error: #{e.class}(#{e.message}) and will propagate" } aborted_proc.call(e) error_proc.call(e) raise e unless StandardError === e # Re-raise system errors, signals and other Exceptions ensure # With rack.hijack the consensus seems to be that the hijack # proc is responsible for closing the socket. We also use no-keepalive # so this should not pose any problems. socket.close unless socket.closed? logger.debug { "Performing cleanup" } cleanup_proc.call(bytes_written) end
The closest you can get to sendfile with Java's NIO www.ibm.com/developerworks/library/j-zerocopy
@param socket the socket to write to @param file the IO you can read from @yields num_bytes_written the number of bytes written on each `IO.copy_stream() call` @return [void]
# File lib/fast_send/socket_handler.rb, line 174 def copy_nio(socket, file) chunk = SENDFILE_CHUNK_SIZE remaining = file.size # We need a Java stream for this, and we cannot really initialize # it from a jRuby File in a convenient way. Since we need it briefly # and we know that the file is on the filesystem at the given path, # we can just open it using the Java means, and go from there input_stream = java.io.FileInputStream.new(file.path) input_channel = input_stream.getChannel output_channel = socket.to_channel loop do break if remaining < 1 # Use exact offsets to avoid boobytraps send_this_time = remaining < chunk ? remaining : chunk read_at = file.size - remaining num_bytes_written = input_channel.transferTo(read_at, send_this_time, output_channel) if num_bytes_written.nonzero? remaining -= num_bytes_written yield(num_bytes_written) end end ensure input_channel.close input_stream.close end
Copies the file to the socket using `IO.copy_stream`. This allows the strings flowing from file to the socket to bypass the Ruby VM and be managed within the calls without allocations. This method gets used when Socket#sendfile is not available on the system we run on (for instance, on Jruby).
@param socket the socket to write to @param file the IO you can read from @yields num_bytes_written the number of bytes written on each `IO.copy_stream() call` @return [void]
# File lib/fast_send/socket_handler.rb, line 149 def copy_stream(socket, file) chunk = SENDFILE_CHUNK_SIZE remaining = file.size loop do break if remaining < 1 # Use exact offsets to avoid boobytraps send_this_time = remaining < chunk ? remaining : chunk num_bytes_written = IO.copy_stream(file, socket, send_this_time) if num_bytes_written.nonzero? remaining -= num_bytes_written yield(num_bytes_written) end end end
This is majorly useful - if the socket is not selectable after a certain timeout, it might be a slow loris or a connection that hung up on us. So if the return from select() is nil, we know that we still cannot write into the socket for some reason. Kill the request, it is dead, jim.
Note that this will not work on OSX due to a sendfile() bug.
# File lib/fast_send/socket_handler.rb, line 82 def fire_timeout_using_select(writable_socket) started_polling_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) select_timeout_secs = 0.25 sleep_after_select_timeout_secs = 0.5 loop do socket_or_nil = writable_socket.wait_writable(SELECT_TIMEOUT_ON_BLOCK) return if socket_or_nil # socket became writable now = Process.clock_gettime(Process::CLOCK_MONOTONIC) if (now - started_polling_at) > SOCKET_TIMEOUT raise SlowLoris, "Receiving socket timed out on sendfile(), probably a dead slow loris" end sleep(sleep_after_select_timeout_secs) end end
Copies the file to the socket using sendfile(). If we are not running on Darwin we are going to use a non-blocking version of sendfile(), and send the socket into a select() wait loop. If no data can be written after 3 minutes the request will be terminated. On Darwin a blocking sendfile() call will be used instead.
@param socket the socket to write to @param file the IO you can read from @yields num_bytes_written the number of bytes written on each `IO.copy_stream() call` @return [void]
# File lib/fast_send/socket_handler.rb, line 107 def sendfile(socket, file) chunk = SENDFILE_CHUNK_SIZE remaining = file.size loop do break if remaining < 1 # Use exact offsets to avoid boobytraps send_this_time = remaining < chunk ? remaining : chunk read_at_offset = file.size - remaining # We have to use blocking "sendfile" on Darwin because the non-blocking version # is buggy # (in an end-to-end test the number of bytes received varies). written = if USE_BLOCKING_SENDFILE socket.sendfile(file, read_at_offset, send_this_time) else socket.trysendfile(file, read_at_offset, send_this_time) end # Will be only triggered when using non-blocking "trysendfile", i.e. on Linux. if written == :wait_writable fire_timeout_using_select(socket) # Used to evict slow lorises elsif written.nil? # Also only relevant for "trysendfile" return # We are done, nil == EOF else remaining -= written yield(written) end end end