From 5b2d3bd1eaa3c692051f692c289a57e15b374d5c Mon Sep 17 00:00:00 2001 From: schneems Date: Mon, 30 Apr 2018 17:00:46 -0500 Subject: [PATCH 1/7] Document Puma::Reactor#run_internal --- lib/puma/reactor.rb | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 12366edb0e..0f4881695e 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -11,6 +11,8 @@ def initialize(server, app_pool) @app_pool = app_pool @mutex = Mutex.new + + # Read / Write pipes to wake up internal while loop @ready, @trigger = Puma::Util.pipe @input = [] @sleep_for = DefaultSleepFor @@ -21,6 +23,51 @@ def initialize(server, app_pool) private + + # Until a request is added via the `add` method this method will internally + # loop, waiting on the `sockets` array objects. The only object in this + # array at first is the `@ready` IO object, which is the read end of a pipe + # connected to `@trigger`. When `@trigger` is written to, then the loop + # will break on IO.select and return an array. + # + # ## When a request is added: + # + # When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array. + # Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`. + # + # When that happens the internal while loop stops blocking and returns a reference + # to whatever "woke" it up. On the very first loop the only thing in `sockets` is `@ready`. + # When `@trigger` is written to the loop "wakes" and the `ready` + # variable returns an array of arrays like `[[#], [], []]` where the + # first IO object is the `@ready` object. This first array `[#]` + # is saved as a `reads` array. + # + # The `reads` array is iterated through and read. In the case that the object + # is the same as the `@ready` input pipe, then we know that there was a `trigger` event. + # + # + # If there was a trigger event then one byte of `@ready` is read into memory. In this case of the first request + # it sees that it's a `"*"` and it adds the contents of `@input` into the `sockets` array. + # The while loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition + # to the `@ready` IO object. For example: `[#, #]`. + # + # Since the `Puma::Client` in this example has data that has not been read yet, + # the IO.select is immediately able to "wake" and read from the `Puma::Client`. At this point the + # `ready` output looks like this: `[[#], [], []]`. + # + # Each element in the first entry is iterated over. The `Puma::Client` object is not + # the `@ready` pipe so we check to see if we have the body, or only the header via + # the `Puma::Client#try_to_finish` method. If the full request has been sent, + # then it is passed off to the `@app_pool` thread pool so that a "worker thread" + # can pick up the request and begin to run application logic. This is done + # via `@app_pool << c`. The `Puma::Client` is then removed from the `sockets` array. + # + # If the request body is not present then nothing will happen, and the loop will iterate + # again. When the client sends more data to the socket the `Puma::Client` object will + # wake up the `IO.select` and it can again be checked to see if it's ready to be + # passed to the thread pool. + # + # There is some timeout logic as well def run_internal sockets = @sockets From ea79d42a1cee7e424efe9982644c1fc418f0367e Mon Sep 17 00:00:00 2001 From: schneems Date: Mon, 30 Apr 2018 17:00:56 -0500 Subject: [PATCH 2/7] Document Puma::Reactor class --- lib/puma/reactor.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 0f4881695e..3a282b03a6 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -2,6 +2,28 @@ require 'puma/minissl' module Puma + # Internal Docs, Not a public interface. + # + # The Reactor object is responsible for ensuring that a request has been + # completely received before it starts to be processed. This may be known as read buffering. + # If this is not done and no other read buffering is performed (such as by an application) server + # such as nginx then the application would be subject to a slow client attack. + # + # For a graphical representation see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline). + # + # A request comes into a `Puma::Server`, it is then passed to the reactor. + # The reactor stores the request in an array and calls `IO.select` on the array in a loop. + # When the request is written to by the client then the `IO.select` will "wake up" and + # return the references to any objects that caused it to "wake". The reactor + # then loops through each of these request objects, sees if they're complete. If they + # are complete (have a full header and body) then it passes the request to a thread pool + # where a "worker thread" can run the the application's Ruby code against the request. + # + # If the request is not complete then it stays in the array and the next time any + # data is written to it the loop is woken up and it is checked for completeness again. + # + # A detailed example is given in the docs for `run_internal` which is where the bulk + # of this logic lives. class Reactor DefaultSleepFor = 5 From 0737bb612c3e72f40b2c418b5bcff3209f4bdbd3 Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 1 May 2018 11:19:50 -0500 Subject: [PATCH 3/7] Update class docs for Reactor --- lib/puma/reactor.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 3a282b03a6..659b346a34 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -6,21 +6,24 @@ module Puma # # The Reactor object is responsible for ensuring that a request has been # completely received before it starts to be processed. This may be known as read buffering. - # If this is not done and no other read buffering is performed (such as by an application) server - # such as nginx then the application would be subject to a slow client attack. + # If read buffering is not done, and no other read buffering is performed (such as by an application server + # such as nginx) then the application would be subject to a slow client attack. # - # For a graphical representation see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline). + # For a graphical representation of how the reactor works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline). # - # A request comes into a `Puma::Server`, it is then passed to the reactor. + # ## Reactor Flow + # + # A request comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance. # The reactor stores the request in an array and calls `IO.select` on the array in a loop. + # # When the request is written to by the client then the `IO.select` will "wake up" and # return the references to any objects that caused it to "wake". The reactor - # then loops through each of these request objects, sees if they're complete. If they - # are complete (have a full header and body) then it passes the request to a thread pool - # where a "worker thread" can run the the application's Ruby code against the request. + # then loops through each of these request objects, and sees if they're complete. If they + # have a full header and body then the reactor passes the request to a thread pool. + # Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request. # - # If the request is not complete then it stays in the array and the next time any - # data is written to it the loop is woken up and it is checked for completeness again. + # If the request is not complete, then it stays in the array, and the next time any + # data is written to that socket reference, then the loop is woken up and it is checked for completeness again. # # A detailed example is given in the docs for `run_internal` which is where the bulk # of this logic lives. From 45b96142038c1de798d12252e467af81c8545139 Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 1 May 2018 11:20:28 -0500 Subject: [PATCH 4/7] Update run_internal docs --- lib/puma/reactor.rb | 47 +++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 659b346a34..6e1a97b60c 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -52,39 +52,38 @@ def initialize(server, app_pool) # Until a request is added via the `add` method this method will internally # loop, waiting on the `sockets` array objects. The only object in this # array at first is the `@ready` IO object, which is the read end of a pipe - # connected to `@trigger`. When `@trigger` is written to, then the loop - # will break on IO.select and return an array. + # connected to `@trigger` object. When `@trigger` is written to, then the loop + # will break on `IO.select` and return an array. # # ## When a request is added: # # When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array. # Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`. # - # When that happens the internal while loop stops blocking and returns a reference - # to whatever "woke" it up. On the very first loop the only thing in `sockets` is `@ready`. - # When `@trigger` is written to the loop "wakes" and the `ready` - # variable returns an array of arrays like `[[#], [], []]` where the + # When that happens, the internal loop stops blocking at `IO.select` and returns a reference + # to whatever "woke" it up. On the very first loop, the only thing in `sockets` is `@ready`. + # When `@trigger` is written-to, the loop "wakes" and the `ready` + # variable returns an array of arrays that looks like `[[#], [], []]` where the # first IO object is the `@ready` object. This first array `[#]` - # is saved as a `reads` array. + # is saved as a `reads` variable. # - # The `reads` array is iterated through and read. In the case that the object + # The `reads` variable is iterated through. In the case that the object # is the same as the `@ready` input pipe, then we know that there was a `trigger` event. # - # - # If there was a trigger event then one byte of `@ready` is read into memory. In this case of the first request - # it sees that it's a `"*"` and it adds the contents of `@input` into the `sockets` array. - # The while loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition + # If there was a trigger event, then one byte of `@ready` is read into memory. In the case of the first request, + # the reactor sees that it's a `"*"` value and the reactor adds the contents of `@input` into the `sockets` array. + # The while then loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition # to the `@ready` IO object. For example: `[#, #]`. # # Since the `Puma::Client` in this example has data that has not been read yet, - # the IO.select is immediately able to "wake" and read from the `Puma::Client`. At this point the + # the `IO.select` is immediately able to "wake" and read from the `Puma::Client`. At this point the # `ready` output looks like this: `[[#], [], []]`. # # Each element in the first entry is iterated over. The `Puma::Client` object is not - # the `@ready` pipe so we check to see if we have the body, or only the header via + # the `@ready` pipe, so the reactor checks to see if it has the fully header and body with # the `Puma::Client#try_to_finish` method. If the full request has been sent, - # then it is passed off to the `@app_pool` thread pool so that a "worker thread" - # can pick up the request and begin to run application logic. This is done + # then the request is passed off to the `@app_pool` thread pool so that a "worker thread" + # can pick up the request and begin to execute application logic. This is done # via `@app_pool << c`. The `Puma::Client` is then removed from the `sockets` array. # # If the request body is not present then nothing will happen, and the loop will iterate @@ -92,7 +91,21 @@ def initialize(server, app_pool) # wake up the `IO.select` and it can again be checked to see if it's ready to be # passed to the thread pool. # - # There is some timeout logic as well + # ## Time Out Case + # + # In addition to being woken via a write to one of the sockets the `IO.select` will + # periodically "time out" of the sleep. One of the functions of this is to check for + # any requests that have "timed out". At the end of the loop it's checked to see if + # the first element in the `@timeout` array has exceed it's allowed time. If so, + # the client object is removed from the timeout aray, a 408 response is written. + # Then it's connection is closed, and the object is removed from the `sockets` array + # that watches for new data. + # + # This behavior loops until all the objects that have timed out have been removed. + # + # Once all the timeouts have been processed, the next duration of the `IO.select` sleep + # will be set to be equal to the amount of time it will take for the next timeout to occur. + # This calculation happens in `calculate_sleep`. def run_internal sockets = @sockets From 8c7601122f67f2d8fdc4b8c832a3ad6e3243bf11 Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 1 May 2018 11:21:05 -0500 Subject: [PATCH 5/7] Document initialize arguments --- lib/puma/reactor.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 6e1a97b60c..2dbb0ee9e8 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -30,6 +30,15 @@ module Puma class Reactor DefaultSleepFor = 5 + # Creates an instance of Puma::Reactor + # + # The `server` argument is an instance of `Puma::Server` + # this is used to write a response for "low level errors" + # when there is an exception inside of the reactor. + # + # The `app_pool` is an instance of `Puma::ThreadPool`. + # Once a request is fully formed (header and body are received) + # it will be passed to the `app_pool`. def initialize(server, app_pool) @server = server @events = server.events From 950f9c51ecd4ba4a9e21aef11c7f74ac21ab81d5 Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 1 May 2018 11:21:28 -0500 Subject: [PATCH 6/7] Document Reactor#calculate_sleep --- lib/puma/reactor.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 2dbb0ee9e8..ef0a75b9e3 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -257,6 +257,16 @@ def run_in_thread end end + # The `calculate_sleep` sets the value that the `IO.select` will + # sleep for in the main reactor loop when no sockets are being written to. + # + # The values kept in `@timeouts` are sorted so that the first timeout + # comes first in the array. When there are no timeouts the default timeout is used. + # + # Otherwise a sleep value is set that is the same as the amount of time it + # would take for the first element to time out. + # + # If that value is in the past, then a sleep value of zero is used. def calculate_sleep if @timeouts.empty? @sleep_for = DefaultSleepFor From 1184ce4846c5478cb69b89b9e79389d754f88dca Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 1 May 2018 11:21:37 -0500 Subject: [PATCH 7/7] Document Reactor#add --- lib/puma/reactor.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index ef0a75b9e3..d6fb326ad0 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -281,6 +281,31 @@ def calculate_sleep end end + # This method adds a connection to the reactor + # + # Typically called by `Puma::Server` the value passed in + # is usually a `Puma::Client` object that responds like an IO + # object. + # + # The main body of the reactor loop is in `run_internal` and it + # will sleep on `IO.select`. When a new connection is added to the + # reactor it cannot be added directly to the `sockets` aray, because + # the `IO.select` will not be watching for it yet. + # + # Instead what needs to happen is that `IO.select` needs to be woken up, + # the contents of `@input` added to the `sockets` array, and then + # another call to `IO.select` needs to happen. Since the `Puma::Client` + # object can be read immediately, it does not block, but instead returns + # right away. + # + # This behavior is accomplished by writing to `@trigger` which wakes up + # the `IO.select` and then there is logic to detect the value of `*`, + # pull the contents from `@input` and add them to the sockets array. + # + # If the object passed in has a timeout value in `timeout_at` then + # it is added to a `@timeouts` array. This array is then re-arranged + # so that the first element to timeout will be at the front of the + # array. Then a value to sleep for is derived in the call to `calculate_sleep` def add(c) @mutex.synchronize do @input << c