Class: FastSend::SocketHandler

Inherits:
Struct
  • Object
show all
Defined in:
lib/fast_send/socket_handler.rb

Overview

Handles the TCP socket within the Rack hijack. Is used instead of a Proc object for better testability and better deallocation

Constant Summary collapse

SOCKET_TIMEOUT =

How many seconds we will wait before considering a client dead.

60
SELECT_TIMEOUT_ON_BLOCK =

The time between select() calls when a socket is blocking on write

5
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

Class.new(StandardError)
CLIENT_DISCONNECT_EXCEPTIONS =

Exceptions that indicate a client being too slow or dropping out due to failing reads/writes

[SlowLoris] + ::FastSend::CLIENT_DISCONNECTS
USE_BLOCKING_SENDFILE =

Whether we are forced to use blocking IO for sendfile()

!!(RUBY_PLATFORM =~ /darwin/)
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

2*1024*1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#aborted_procObject

Returns the value of attribute aborted_proc

Returns:

  • (Object)

    the current value of aborted_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def aborted_proc
  @aborted_proc
end

#cleanup_procObject

Returns the value of attribute cleanup_proc

Returns:

  • (Object)

    the current value of cleanup_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def cleanup_proc
  @cleanup_proc
end

#done_procObject

Returns the value of attribute done_proc

Returns:

  • (Object)

    the current value of done_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def done_proc
  @done_proc
end

#error_procObject

Returns the value of attribute error_proc

Returns:

  • (Object)

    the current value of error_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def error_proc
  @error_proc
end

#loggerObject

Returns the value of attribute logger

Returns:

  • (Object)

    the current value of logger



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def logger
  @logger
end

#started_procObject

Returns the value of attribute started_proc

Returns:

  • (Object)

    the current value of started_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def started_proc
  @started_proc
end

#streamObject

Returns the value of attribute stream

Returns:

  • (Object)

    the current value of stream



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def stream
  @stream
end

#written_procObject

Returns the value of attribute written_proc

Returns:

  • (Object)

    the current value of written_proc



3
4
5
# File 'lib/fast_send/socket_handler.rb', line 3

def written_proc
  @written_proc
end

Instance Method Details

#call(socket) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/fast_send/socket_handler.rb', line 30

def call(socket)
  return if socket.closed?
  
  writer_method_name = if socket.respond_to?(:sendfile)
    :sendfile
  elsif RUBY_PLATFORM == 'java'
    :copy_nio
  else
    :copy_stream
  end
  
  logger.debug { "Will do file-to-socket using %s" % writer_method_name }
  
  begin
    logger.debug { "Starting the response" }
    
    bytes_written = 0
    
    started_proc.call(bytes_written)
    
    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
end

#copy_nio(socket, file) ⇒ void

This method returns an undefined value.

The closest you can get to sendfile with Java’s NIO www.ibm.com/developerworks/library/j-zerocopy

Parameters:

  • socket (Socket)

    the socket to write to

  • file (File)

    the IO you can read from



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/fast_send/socket_handler.rb', line 179

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

#copy_stream(socket, file) ⇒ void

This method returns an undefined value.

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).

Parameters:

  • socket (Socket)

    the socket to write to

  • file (File)

    the IO you can read from



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/fast_send/socket_handler.rb', line 154

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

#fire_timeout_using_select(writable_socket) ⇒ Object

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.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/fast_send/socket_handler.rb', line 86

def fire_timeout_using_select(writable_socket)
  at = Time.now
  loop do
    _, writeables, errored = IO.select(nil, [writable_socket], [writable_socket], SELECT_TIMEOUT_ON_BLOCK)
    if writeables && writeables.include?(writable_socket) 
      return # We can proceed
    end
    if errored && errored.include?(writable_socket)
      raise SlowLoris, "Receiving socket had an error, connection will be dropped"
    end
    if (Time.now - at) > SOCKET_TIMEOUT
      raise SlowLoris, "Receiving socket timed out on sendfile(), probably a dead slow loris"
    end
  end
end

#sendfile(socket, file) ⇒ void

This method returns an undefined value.

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.

Parameters:

  • socket (Socket)

    the socket to write to

  • file (File)

    the IO you can read from



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/fast_send/socket_handler.rb', line 112

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