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



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def aborted_proc
  @aborted_proc
end

#cleanup_procObject

Returns the value of attribute cleanup_proc

Returns:

  • (Object)

    the current value of cleanup_proc



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def cleanup_proc
  @cleanup_proc
end

#done_procObject

Returns the value of attribute done_proc

Returns:

  • (Object)

    the current value of done_proc



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def done_proc
  @done_proc
end

#error_procObject

Returns the value of attribute error_proc

Returns:

  • (Object)

    the current value of error_proc



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def error_proc
  @error_proc
end

#loggerObject

Returns the value of attribute logger

Returns:

  • (Object)

    the current value of logger



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def logger
  @logger
end

#started_procObject

Returns the value of attribute started_proc

Returns:

  • (Object)

    the current value of started_proc



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def started_proc
  @started_proc
end

#streamObject

Returns the value of attribute stream

Returns:

  • (Object)

    the current value of stream



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def stream
  @stream
end

#written_procObject

Returns the value of attribute written_proc

Returns:

  • (Object)

    the current value of written_proc



5
6
7
# File 'lib/fast_send/socket_handler.rb', line 5

def written_proc
  @written_proc
end

Instance Method Details

#call(socket) ⇒ Object



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
# 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

#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



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 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

#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



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# 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

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



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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

#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



107
108
109
110
111
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
# 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