Class: FastSend::SocketHandler
- Inherits:
-
Struct
- Object
- Struct
- FastSend::SocketHandler
- 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
-
#aborted_proc ⇒ Object
Returns the value of attribute aborted_proc.
-
#cleanup_proc ⇒ Object
Returns the value of attribute cleanup_proc.
-
#done_proc ⇒ Object
Returns the value of attribute done_proc.
-
#error_proc ⇒ Object
Returns the value of attribute error_proc.
-
#logger ⇒ Object
Returns the value of attribute logger.
-
#started_proc ⇒ Object
Returns the value of attribute started_proc.
-
#stream ⇒ Object
Returns the value of attribute stream.
-
#written_proc ⇒ Object
Returns the value of attribute written_proc.
Instance Method Summary collapse
- #call(socket) ⇒ Object
-
#copy_nio(socket, file) ⇒ void
The closest you can get to sendfile with Java’s NIO www.ibm.com/developerworks/library/j-zerocopy.
-
#copy_stream(socket, file) ⇒ void
Copies the file to the socket using ‘IO.copy_stream`.
-
#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.
-
#sendfile(socket, file) ⇒ void
Copies the file to the socket using sendfile().
Instance Attribute Details
#aborted_proc ⇒ Object
Returns the value of attribute aborted_proc
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def aborted_proc @aborted_proc end |
#cleanup_proc ⇒ Object
Returns the value of attribute cleanup_proc
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def cleanup_proc @cleanup_proc end |
#done_proc ⇒ Object
Returns the value of attribute done_proc
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def done_proc @done_proc end |
#error_proc ⇒ Object
Returns the value of attribute error_proc
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def error_proc @error_proc end |
#logger ⇒ Object
Returns the value of attribute logger
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def logger @logger end |
#started_proc ⇒ Object
Returns the value of attribute started_proc
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def started_proc @started_proc end |
#stream ⇒ Object
Returns the value of attribute stream
3 4 5 |
# File 'lib/fast_send/socket_handler.rb', line 3 def stream @stream end |
#written_proc ⇒ Object
Returns the value of attribute 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
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).
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.
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 |